From 40882c62c9d67f56882ecdef29d6b5e4ed406454 Mon Sep 17 00:00:00 2001 From: Dan Fuhry Date: Wed, 22 Jul 2026 23:25:49 -0400 Subject: [PATCH] [mtls][mint] tag mint-issued certs with requester identity - Add a way to tack arbitrary structured protobuf on to certificates using a `repeated google.protobuf.Any` - Add `fuhry.runtime.service.mint.CertificateRequester` message to represent requester identity - Block RequestCertificate calls if caller is using a cert that came from mint --- mint/BUILD.bazel | 1 + mint/client.go | 7 ++- mint/internal/BUILD.bazel | 15 +++++ mint/internal/requester.go | 31 +++++++++++ mint/servicer/BUILD.bazel | 4 ++ mint/servicer/servicer.go | 10 +++- mint/servicer/signer.go | 86 ++++++++++++++++++++++++++--- mtls/identity.go | 6 +- proto/mtls/BUILD.bazel | 18 ++++++ proto/mtls/attributes.proto | 11 ++++ proto/service/mint/mint_types.proto | 15 +++++ 11 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 mint/internal/BUILD.bazel create mode 100644 mint/internal/requester.go create mode 100644 proto/mtls/BUILD.bazel create mode 100644 proto/mtls/attributes.proto diff --git a/mint/BUILD.bazel b/mint/BUILD.bazel index 42a9886..737a2bd 100644 --- a/mint/BUILD.bazel +++ b/mint/BUILD.bazel @@ -10,6 +10,7 @@ go_multi_library( visibility = ["//visibility:public"], deps = [ "//grpc", + "//mint/internal", "//mtls", "//mtls/certutil", "//proto/service/mint:mint_go_proto", diff --git a/mint/client.go b/mint/client.go index 6306b96..7508e46 100644 --- a/mint/client.go +++ b/mint/client.go @@ -15,6 +15,7 @@ import ( "github.com/quic-go/quic-go" "go.fuhry.dev/runtime/grpc" + "go.fuhry.dev/runtime/mint/internal" "go.fuhry.dev/runtime/mtls" "go.fuhry.dev/runtime/mtls/certutil" mintpb "go.fuhry.dev/runtime/proto/service/mint" @@ -319,7 +320,7 @@ func (m *MintCertificate) scheduleRenew() { renewIn = 0 } - go (func() { + go func() { log.Infof("certificate %q will be renewed in %s", m.service, renewIn.String()) time.Sleep(renewIn) err := m.renew() @@ -328,7 +329,7 @@ func (m *MintCertificate) scheduleRenew() { time.Sleep(renewRetryInterval) m.scheduleRenew() } - })() + }() } // RootCertificate implements mtls.CertificateProvider. @@ -413,6 +414,8 @@ func (m *MintCertificate) tryRenew() error { return nil } +var RequesterFromCertificate = internal.RequesterFromCertificate + func init() { flag.StringVar(&defaultIdentityStr, "mint.mtls.id", "", "identity for fetching certificates from mint service; defaults to the value of -mtls.id") } diff --git a/mint/internal/BUILD.bazel b/mint/internal/BUILD.bazel new file mode 100644 index 0000000..34d2a50 --- /dev/null +++ b/mint/internal/BUILD.bazel @@ -0,0 +1,15 @@ +load("//bazel:go.bzl", "go_multi_library") + +go_multi_library( + name = "internal", + srcs = ["requester.go"], + importpath = "go.fuhry.dev/runtime/mint/internal", + visibility = ["//mint:__subpackages__"], + deps = [ + "//mtls", + "//proto/mtls:mtls_go_proto", + "//proto/service/mint:mint_go_proto", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//types/known/anypb", + ], +) diff --git a/mint/internal/requester.go b/mint/internal/requester.go new file mode 100644 index 0000000..dfe40cf --- /dev/null +++ b/mint/internal/requester.go @@ -0,0 +1,31 @@ +package internal + +import ( + "crypto/x509" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + "go.fuhry.dev/runtime/mtls" + mtls_pb "go.fuhry.dev/runtime/proto/mtls" + mintpb "go.fuhry.dev/runtime/proto/service/mint" +) + +func RequesterFromCertificate(cert *x509.Certificate) *mintpb.CertificateRequester { + for _, ext := range cert.Extensions { + if ext.Id.Equal(mtls.CertificateAttributesOID) { + attributes := new(mtls_pb.MtlsCertificateAttributes) + if err := proto.Unmarshal(ext.Value, attributes); err == nil { + for _, attr := range attributes.GetItems() { + if attrMsg, err := anypb.UnmarshalNew(attr, proto.UnmarshalOptions{}); err == nil { + if requester, ok := attrMsg.(*mintpb.CertificateRequester); ok { + return requester + } + } + } + } + } + } + + return nil +} diff --git a/mint/servicer/BUILD.bazel b/mint/servicer/BUILD.bazel index 6845917..e2ca780 100644 --- a/mint/servicer/BUILD.bazel +++ b/mint/servicer/BUILD.bazel @@ -16,9 +16,11 @@ go_multi_library( "//ephs", "//grpc", "//mint", + "//mint/internal", "//mint/remote_signer", "//mtls", "//mtls/certutil", + "//proto/mtls:mtls_go_proto", "//proto/service/mint:mint_go_proto", "//utils/context", "//utils/hostname", @@ -32,6 +34,8 @@ go_multi_library( "@in_gopkg_yaml_v3//:yaml_v3", "@org_golang_google_grpc//codes", "@org_golang_google_grpc//status", + "@org_golang_google_protobuf//proto", + "@org_golang_google_protobuf//types/known/anypb", "@sm_step_go_crypto//jose", ], ) diff --git a/mint/servicer/servicer.go b/mint/servicer/servicer.go index 660c719..f216a0a 100644 --- a/mint/servicer/servicer.go +++ b/mint/servicer/servicer.go @@ -14,6 +14,7 @@ import ( "go.fuhry.dev/runtime/config_watcher" "go.fuhry.dev/runtime/ephs" "go.fuhry.dev/runtime/grpc" + "go.fuhry.dev/runtime/mint/internal" "go.fuhry.dev/runtime/mint/remote_signer" "go.fuhry.dev/runtime/mtls" "go.fuhry.dev/runtime/mtls/certutil" @@ -123,6 +124,13 @@ func (s *mintServicer) RequestCertificate(ctx context.Context, req *mint_proto.C return nil, status.Errorf(codes.Unauthenticated, "cannot determine peer certificate") } + if requester := internal.RequesterFromCertificate(peerCert); requester != nil { + return nil, status.Errorf( + codes.FailedPrecondition, + "peer certificate was issued by mint, so it cannot be used for certificate requests", + ) + } + if !hostname.ValidHostname.MatchString(req.HostInfo.Hostname) { return nil, status.Errorf(codes.InvalidArgument, "provided hostname %q is invalid", req.HostInfo.Hostname) } @@ -207,7 +215,7 @@ func (s *mintServicer) FinalizeCertificate(ctx context.Context, req *mint_proto. delete(s.pending, string(req.Digest)) signer := remote_signer.LoadRemoteSignature(req.Digest, req.Signature, csr.PublicKey, crypto.Hash(req.Hash)) - sr, err := FinalizeSignRequest(csr, signer, peerCert.NotAfter) + sr, err := FinalizeSignRequest(csr, signer, peerCert) if err != nil { return nil, status.Errorf(codes.Internal, "failed to finalize sign request: %v", err) } diff --git a/mint/servicer/signer.go b/mint/servicer/signer.go index 30dc1c7..d6e5210 100644 --- a/mint/servicer/signer.go +++ b/mint/servicer/signer.go @@ -9,6 +9,7 @@ import ( "crypto/sha256" "crypto/x509" "crypto/x509/pkix" + "encoding/base64" "encoding/hex" "encoding/json" "encoding/pem" @@ -26,17 +27,20 @@ import ( "github.com/smallstep/cli/token" "github.com/smallstep/cli/utils/cautils" "go.step.sm/crypto/jose" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" "go.fuhry.dev/runtime/mint/remote_signer" "go.fuhry.dev/runtime/mtls" + "go.fuhry.dev/runtime/mtls/certutil" + mtls_pb "go.fuhry.dev/runtime/proto/mtls" + mint_pb "go.fuhry.dev/runtime/proto/service/mint" "go.fuhry.dev/runtime/utils/context" "go.fuhry.dev/runtime/utils/hostname" "go.fuhry.dev/runtime/utils/log" ) -var ( - provisionerName, keyPath, keyPw, caURL, rootCADigest string -) +var provisionerName, keyPath, keyPw, caURL, rootCADigest string func newStepCAClient() (*ca.Client, error) { ident := mtls.Anonymous() @@ -110,13 +114,19 @@ func newTokenGenerator() (*cautils.TokenGenerator, error) { ), nil } -func newOTT(subject string, sans []string) (string, error) { +func newOTT(subject string, sans []string, opts ...token.Options) (string, error) { tg, err := newTokenGenerator() if err != nil { return "", err } - return tg.SignToken(subject, sans, withRootSHA(rootCADigest)) + opts = append(opts, withRootSHA(rootCADigest)) + + return tg.SignToken( + subject, + sans, + opts..., + ) } func withRootSHA(hash string) token.Options { @@ -126,6 +136,56 @@ func withRootSHA(hash string) token.Options { } } +func newRequesterMessage(peerCert *x509.Certificate) (*mint_pb.CertificateRequester, error) { + msg := &mint_pb.CertificateRequester{ + Serial: peerCert.SerialNumber.Bytes(), + } + + if spiffe := certutil.SpiffeUrlFromCertificate(peerCert); spiffe != nil { + id, err := mtls.ParseRemoteIdentity(spiffe.String()) + if err == nil { + msg.Principal = &mint_pb.CertificateRequester_Spiffe{ + Spiffe: &mint_pb.CertificateRequester_SpiffeId{ + Domain: id.Domain, + Class: id.Class.String(), + Principal: id.Principal, + }, + } + } else { + return nil, fmt.Errorf("failed to parse SPIFFE ID: %v", err) + } + } else if peerCert.Subject.CommonName != "" { + msg.Principal = &mint_pb.CertificateRequester_Subject{ + Subject: peerCert.Subject.CommonName, + } + } else { + return nil, errors.New("peer cert contains neither common name nor spiffe ID") + } + + return msg, nil +} + +func withCustomAttributes(attrs ...proto.Message) (token.Options, error) { + msg := &mtls_pb.MtlsCertificateAttributes{} + + for _, a := range attrs { + anyMsg, err := anypb.New(a) + if err != nil { + return nil, err + } + msg.Items = append(msg.Items, anyMsg) + } + + msgBinary, err := proto.Marshal(msg) + if err != nil { + return nil, err + } + msgBase64 := base64.StdEncoding.EncodeToString(msgBinary) + return token.WithUserData(map[string]interface{}{ + "mtlsCustomAttributesProto": msgBase64, + }), nil +} + func CreateSignRequest(subject *mtls.RemoteIdentity, extraNames []string, pub crypto.PublicKey) (*x509.CertificateRequest, *remote_signer.SignRequest, error) { var alg x509.PublicKeyAlgorithm switch pub.(type) { @@ -170,7 +230,7 @@ func CreateSignRequest(subject *mtls.RemoteIdentity, extraNames []string, pub cr return csrTemplate, rs.Request(), nil } -func FinalizeSignRequest(csrTemplate *x509.CertificateRequest, signer crypto.Signer, notAfter time.Time) (*api.SignRequest, error) { +func FinalizeSignRequest(csrTemplate *x509.CertificateRequest, signer crypto.Signer, peerCert *x509.Certificate) (*api.SignRequest, error) { csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, signer) if err != nil { return nil, err @@ -187,7 +247,18 @@ func FinalizeSignRequest(csrTemplate *x509.CertificateRequest, signer crypto.Sig sans = append(sans, u.String()) } - ott, err := newOTT(csr.Subject.CommonName, sans) + var opts []token.Options + if idMessage, err := newRequesterMessage(peerCert); err == nil { + if custAttr, err := withCustomAttributes(idMessage); err == nil { + opts = append(opts, custAttr) + } else { + log.Default().Warningf("failed to create custom-attributes message: %v", err) + } + } else { + log.Default().Warningf("failed to create attribution custom-attribute for peer cert %v: %v", peerCert, err) + } + + ott, err := newOTT(csr.Subject.CommonName, sans, opts...) if err != nil { return nil, err } @@ -196,6 +267,7 @@ func FinalizeSignRequest(csrTemplate *x509.CertificateRequest, signer crypto.Sig notBefore := now.Add(-5 * time.Second) // follow notAfter time of peer certificate, with validity cap of 1 day + notAfter := peerCert.NotAfter if oneDay := now.Add(24 * time.Hour); notAfter.After(oneDay) { notAfter = oneDay } diff --git a/mtls/identity.go b/mtls/identity.go index 030bde4..4c158a7 100644 --- a/mtls/identity.go +++ b/mtls/identity.go @@ -2,6 +2,7 @@ package mtls import ( "crypto/tls" + "encoding/asn1" "errors" "flag" "fmt" @@ -44,8 +45,9 @@ const ( ) var ( - defaultMtlsIdentity string - logger log.Logger + defaultMtlsIdentity string + logger log.Logger + CertificateAttributesOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 38160, 3, 0} ) type Identity interface { diff --git a/proto/mtls/BUILD.bazel b/proto/mtls/BUILD.bazel new file mode 100644 index 0000000..cead126 --- /dev/null +++ b/proto/mtls/BUILD.bazel @@ -0,0 +1,18 @@ +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_go//proto:def.bzl", "go_proto_library") + +# gazelle:proto package + +proto_library( + name = "mtls_proto", + srcs = ["attributes.proto"], + visibility = ["//visibility:public"], + deps = ["@com_google_protobuf//:any_proto"], +) + +go_proto_library( + name = "mtls_go_proto", + importpath = "go.fuhry.dev/runtime/proto/mtls", + proto = ":mtls_proto", + visibility = ["//visibility:public"], +) diff --git a/proto/mtls/attributes.proto b/proto/mtls/attributes.proto new file mode 100644 index 0000000..3635a4a --- /dev/null +++ b/proto/mtls/attributes.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; +package fuhry.runtime.mtls; + +option go_package = "go.fuhry.dev/runtime/proto/mtls"; + +import "google/protobuf/any.proto"; + +message MtlsCertificateAttributes { + repeated google.protobuf.Any items = 1; +} + diff --git a/proto/service/mint/mint_types.proto b/proto/service/mint/mint_types.proto index 38ba34f..2ac8352 100644 --- a/proto/service/mint/mint_types.proto +++ b/proto/service/mint/mint_types.proto @@ -28,3 +28,18 @@ message CertificateResponse { bytes leaf = 1; repeated bytes intermediates = 2; } + +message CertificateRequester { + message SpiffeId { + string domain = 1; + string class = 2; + string principal = 3; + } + + bytes serial = 1; + + oneof principal { + string subject = 2; + SpiffeId spiffe = 3; + } +} -- 2.52.0