]> go.fuhry.dev Git - runtime.git/commitdiff
[mtls][mint] tag mint-issued certs with requester identity
authorDan Fuhry <dan@fuhry.com>
Thu, 23 Jul 2026 03:25:49 +0000 (23:25 -0400)
committerDan Fuhry <dan@fuhry.com>
Thu, 23 Jul 2026 03:25:49 +0000 (23:25 -0400)
- 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
mint/client.go
mint/internal/BUILD.bazel [new file with mode: 0644]
mint/internal/requester.go [new file with mode: 0644]
mint/servicer/BUILD.bazel
mint/servicer/servicer.go
mint/servicer/signer.go
mtls/identity.go
proto/mtls/BUILD.bazel [new file with mode: 0644]
proto/mtls/attributes.proto [new file with mode: 0644]
proto/service/mint/mint_types.proto

index 42a98861b7b54d60bb156a01325ce909f79e4d74..737a2bd37a6362833730cea8299c026549c76d4f 100644 (file)
@@ -10,6 +10,7 @@ go_multi_library(
     visibility = ["//visibility:public"],
     deps = [
         "//grpc",
+        "//mint/internal",
         "//mtls",
         "//mtls/certutil",
         "//proto/service/mint:mint_go_proto",
index 6306b96c2ab1782a8eed3c4a9afb931440d566bd..7508e46fbde4dd4ab11c6107bbd81d02023b4372 100644 (file)
@@ -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 (file)
index 0000000..34d2a50
--- /dev/null
@@ -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 (file)
index 0000000..dfe40cf
--- /dev/null
@@ -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
+}
index 6845917d081c7e0ffcffb890c304e1f37324bb1e..e2ca78033c6d4c2c0366b33bb317581dacb90e3b 100644 (file)
@@ -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",
     ],
 )
index 660c719b040b7fe7a8dadcfc7b8bba76090db08b..f216a0a1727b904ea7e82ac00adcbc17242e78e3 100644 (file)
@@ -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)
        }
index 30dc1c7435cfea86d3175c2e6dc5256ab2cb5181..d6e5210316630caabbe79f0da618310ffc4adef9 100644 (file)
@@ -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
        }
index 030bde425c73b9e7e4496e662c141d0a064155d1..4c158a7aec9a5e05baeecab0603e94182d58a42a 100644 (file)
@@ -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 (file)
index 0000000..cead126
--- /dev/null
@@ -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 (file)
index 0000000..3635a4a
--- /dev/null
@@ -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;
+}
+
index 38ba34f4955984bddb934768f8972a9a08088ff2..2ac8352bbfb820e057725e80f60d5f38d26acb0d 100644 (file)
@@ -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;
+    }
+}