From: Dan Fuhry Date: Sat, 14 Mar 2026 23:50:51 +0000 (-0400) Subject: [mint] Send hostname and domain name, add templates X-Git-Url: https://go.fuhry.dev/?a=commitdiff_plain;h=27e89f2291f43c0a0a4726e3215bd3ab84878a5b;p=runtime.git [mint] Send hostname and domain name, add templates - Add templates for custom SANs derived from host info sent in gRPC req - Supporting changes to proto, client, etc. --- diff --git a/mint/BUILD.bazel b/mint/BUILD.bazel index 22822d3..d5a9662 100644 --- a/mint/BUILD.bazel +++ b/mint/BUILD.bazel @@ -14,6 +14,7 @@ go_library( "//mtls/certutil", "//proto/service/mint", "//utils/context", + "//utils/hostname", "//utils/log", "//utils/option", "@com_github_quic_go_quic_go//:quic-go", diff --git a/mint/client.go b/mint/client.go index 6c1f116..932e544 100644 --- a/mint/client.go +++ b/mint/client.go @@ -19,6 +19,7 @@ import ( "go.fuhry.dev/runtime/mtls/certutil" mintpb "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" "go.fuhry.dev/runtime/utils/option" ) @@ -155,6 +156,10 @@ func (c *clientImpl) GetCertificate(subject string) (*MintCertificate, error) { cr := &mintpb.CertificateRequest{ RequestedIdentity: subject, PublicKey: publicKey, + HostInfo: &mintpb.CertificateRequest_HostInfo{ + Hostname: hostname.Hostname(), + Domain: hostname.DomainName(), + }, } resp, err := rpc.RequestCertificate(c.defaultCtx, cr) diff --git a/mint/servicer/BUILD.bazel b/mint/servicer/BUILD.bazel index 44c0086..874482c 100644 --- a/mint/servicer/BUILD.bazel +++ b/mint/servicer/BUILD.bazel @@ -18,6 +18,7 @@ go_library( "//mtls/certutil", "//proto/service/mint", "//utils/context", + "//utils/hostname", "//utils/log", "//utils/option", "@com_github_smallstep_certificates//api", diff --git a/mint/servicer/acl.go b/mint/servicer/acl.go index 04d9224..f0cce04 100644 --- a/mint/servicer/acl.go +++ b/mint/servicer/acl.go @@ -18,8 +18,14 @@ type AclRule struct { Allow string } +type TemplateRule struct { + Principal stringmatch.NewSyntaxMatchRule `yaml:"principal"` + Names []string `yaml:"names"` +} + type AclRulesFile struct { - Rules []*AclRule `yaml:"rules"` + Rules []*AclRule `yaml:"rules"` + Templates []*TemplateRule `yaml:"templates"` } var _ yaml.Unmarshaler = &AclRule{} @@ -116,6 +122,21 @@ func (f *AclRulesFile) Check(id *mtls.RemoteIdentity, requestedPrincipal string) return false } +func (f *AclRulesFile) Names(requestedPrincipal string) (out []string) { + out = []string{ + "spiffe://{{spiffe.domain}}/service/{{principal}}", + } + + for _, rule := range f.Templates { + if rule.Principal.Match(requestedPrincipal) { + out = rule.Names + break + } + } + + return +} + func init() { flag.StringVar(&defaultAclPath, "mint.acl-path", defaultAclPath, "local or ephs path to rules file that dictates who can request what certificates") } diff --git a/mint/servicer/servicer.go b/mint/servicer/servicer.go index dfbc4fb..aab44f7 100644 --- a/mint/servicer/servicer.go +++ b/mint/servicer/servicer.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "io" "os" + "strings" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -18,6 +19,7 @@ import ( "go.fuhry.dev/runtime/mtls/certutil" mint_proto "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" "go.fuhry.dev/runtime/utils/option" ) @@ -121,6 +123,14 @@ func (s *mintServicer) RequestCertificate(ctx context.Context, req *mint_proto.C return nil, status.Errorf(codes.Unauthenticated, "cannot determine peer certificate") } + if !hostname.ValidHostname.MatchString(req.HostInfo.Hostname) { + return nil, status.Errorf(codes.InvalidArgument, "provided hostname %q is invalid", req.HostInfo.Hostname) + } + + if !hostname.ValidDomainName.MatchString(req.HostInfo.Domain) { + return nil, status.Errorf(codes.InvalidArgument, "provided domain name %q is invalid", req.HostInfo.Domain) + } + spiffeId := certutil.SpiffeUrlFromCertificate(peerCert) if spiffeId == nil { return nil, status.Errorf(codes.Unauthenticated, "cannot find SPIFFE ID in peer certificate") @@ -142,6 +152,21 @@ func (s *mintServicer) RequestCertificate(ctx context.Context, req *mint_proto.C req.RequestedIdentity) } + template := s.acl.Names(req.RequestedIdentity) + kv := map[string]string{ + "spiffe.domain": remoteId.Domain, + "spiffe.class": remoteId.Class.String(), + "spiffe.principal": remoteId.Principal, + "principal": req.RequestedIdentity, + "hostname": req.HostInfo.Hostname, + "domain": req.HostInfo.Domain, + } + for i := range template { + for k, v := range kv { + template[i] = strings.ReplaceAll(template[i], "{{"+k+"}}", v) + } + } + pub, err := x509.ParsePKIXPublicKey(req.PublicKey) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "failed to parse public key: %v", err) @@ -153,7 +178,7 @@ func (s *mintServicer) RequestCertificate(ctx context.Context, req *mint_proto.C Principal: req.RequestedIdentity, } - csr, sr, err := CreateSignRequest(requestedId, pub) + csr, sr, err := CreateSignRequest(requestedId, template, pub) if err != nil { return nil, status.Errorf(codes.Internal, "failed to create sign request: %v", err) } diff --git a/mint/servicer/signer.go b/mint/servicer/signer.go index b0b07bf..30dc1c7 100644 --- a/mint/servicer/signer.go +++ b/mint/servicer/signer.go @@ -30,6 +30,7 @@ import ( "go.fuhry.dev/runtime/mint/remote_signer" "go.fuhry.dev/runtime/mtls" "go.fuhry.dev/runtime/utils/context" + "go.fuhry.dev/runtime/utils/hostname" "go.fuhry.dev/runtime/utils/log" ) @@ -125,7 +126,7 @@ func withRootSHA(hash string) token.Options { } } -func CreateSignRequest(subject *mtls.RemoteIdentity, pub crypto.PublicKey) (*x509.CertificateRequest, *remote_signer.SignRequest, error) { +func CreateSignRequest(subject *mtls.RemoteIdentity, extraNames []string, pub crypto.PublicKey) (*x509.CertificateRequest, *remote_signer.SignRequest, error) { var alg x509.PublicKeyAlgorithm switch pub.(type) { case *ecdsa.PublicKey: @@ -152,6 +153,14 @@ func CreateSignRequest(subject *mtls.RemoteIdentity, pub crypto.PublicKey) (*x50 }, } + for _, n := range extraNames { + if nUrl, err := url.Parse(n); err == nil && nUrl.Scheme != "" { + csrTemplate.URIs = append(csrTemplate.URIs, nUrl) + } else if hostname.ValidDomainName.MatchString(n) { + csrTemplate.DNSNames = append(csrTemplate.DNSNames, n) + } + } + rs := remote_signer.NewSigner(pub) _, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, rs) if !errors.Is(err, remote_signer.ErrSignerNonlocal) { diff --git a/proto/service/mint/mint_types.pb.go b/proto/service/mint/mint_types.pb.go index 48731be..8bf8328 100644 --- a/proto/service/mint/mint_types.pb.go +++ b/proto/service/mint/mint_types.pb.go @@ -22,9 +22,10 @@ const ( ) type CertificateRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestedIdentity string `protobuf:"bytes,1,opt,name=requested_identity,json=requestedIdentity,proto3" json:"requested_identity,omitempty"` - PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RequestedIdentity string `protobuf:"bytes,1,opt,name=requested_identity,json=requestedIdentity,proto3" json:"requested_identity,omitempty"` + PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + HostInfo *CertificateRequest_HostInfo `protobuf:"bytes,3,opt,name=host_info,json=hostInfo,proto3" json:"host_info,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -73,6 +74,13 @@ func (x *CertificateRequest) GetPublicKey() []byte { return nil } +func (x *CertificateRequest) GetHostInfo() *CertificateRequest_HostInfo { + if x != nil { + return x.HostInfo + } + return nil +} + type CertificatePreSignResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Digest []byte `protobuf:"bytes,1,opt,name=digest,proto3" json:"digest,omitempty"` @@ -237,15 +245,71 @@ func (x *CertificateResponse) GetIntermediates() [][]byte { return nil } +type CertificateRequest_HostInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CertificateRequest_HostInfo) Reset() { + *x = CertificateRequest_HostInfo{} + mi := &file_mint_types_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CertificateRequest_HostInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificateRequest_HostInfo) ProtoMessage() {} + +func (x *CertificateRequest_HostInfo) ProtoReflect() protoreflect.Message { + mi := &file_mint_types_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificateRequest_HostInfo.ProtoReflect.Descriptor instead. +func (*CertificateRequest_HostInfo) Descriptor() ([]byte, []int) { + return file_mint_types_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *CertificateRequest_HostInfo) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *CertificateRequest_HostInfo) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + var File_mint_types_proto protoreflect.FileDescriptor const file_mint_types_proto_rawDesc = "" + "\n" + - "\x10mint_types.proto\x12\x1afuhry.runtime.service.mint\"b\n" + + "\x10mint_types.proto\x12\x1afuhry.runtime.service.mint\"\xf8\x01\n" + "\x12CertificateRequest\x12-\n" + "\x12requested_identity\x18\x01 \x01(\tR\x11requestedIdentity\x12\x1d\n" + "\n" + - "public_key\x18\x02 \x01(\fR\tpublicKey\"H\n" + + "public_key\x18\x02 \x01(\fR\tpublicKey\x12T\n" + + "\thost_info\x18\x03 \x01(\v27.fuhry.runtime.service.mint.CertificateRequest.HostInfoR\bhostInfo\x1a>\n" + + "\bHostInfo\x12\x1a\n" + + "\bhostname\x18\x01 \x01(\tR\bhostname\x12\x16\n" + + "\x06domain\x18\x02 \x01(\tR\x06domain\"H\n" + "\x1aCertificatePreSignResponse\x12\x16\n" + "\x06digest\x18\x01 \x01(\fR\x06digest\x12\x12\n" + "\x04hash\x18\x02 \x01(\x05R\x04hash\"[\n" + @@ -269,19 +333,21 @@ func file_mint_types_proto_rawDescGZIP() []byte { return file_mint_types_proto_rawDescData } -var file_mint_types_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_mint_types_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_mint_types_proto_goTypes = []any{ - (*CertificateRequest)(nil), // 0: fuhry.runtime.service.mint.CertificateRequest - (*CertificatePreSignResponse)(nil), // 1: fuhry.runtime.service.mint.CertificatePreSignResponse - (*FinalizeRequest)(nil), // 2: fuhry.runtime.service.mint.FinalizeRequest - (*CertificateResponse)(nil), // 3: fuhry.runtime.service.mint.CertificateResponse + (*CertificateRequest)(nil), // 0: fuhry.runtime.service.mint.CertificateRequest + (*CertificatePreSignResponse)(nil), // 1: fuhry.runtime.service.mint.CertificatePreSignResponse + (*FinalizeRequest)(nil), // 2: fuhry.runtime.service.mint.FinalizeRequest + (*CertificateResponse)(nil), // 3: fuhry.runtime.service.mint.CertificateResponse + (*CertificateRequest_HostInfo)(nil), // 4: fuhry.runtime.service.mint.CertificateRequest.HostInfo } var file_mint_types_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name + 4, // 0: fuhry.runtime.service.mint.CertificateRequest.host_info:type_name -> fuhry.runtime.service.mint.CertificateRequest.HostInfo + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name } func init() { file_mint_types_proto_init() } @@ -295,7 +361,7 @@ func file_mint_types_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_mint_types_proto_rawDesc), len(file_mint_types_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 5, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/service/mint/mint_types.proto b/proto/service/mint/mint_types.proto index 63c29f0..38ba34f 100644 --- a/proto/service/mint/mint_types.proto +++ b/proto/service/mint/mint_types.proto @@ -4,8 +4,13 @@ package fuhry.runtime.service.mint; option go_package = "go.fuhry.dev/runtime/proto/service/mint"; message CertificateRequest { + message HostInfo { + string hostname = 1; + string domain = 2; + } string requested_identity = 1; bytes public_key = 2; + HostInfo host_info = 3; } message CertificatePreSignResponse {