- 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
visibility = ["//visibility:public"],
deps = [
"//grpc",
+ "//mint/internal",
"//mtls",
"//mtls/certutil",
"//proto/service/mint:mint_go_proto",
"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"
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()
time.Sleep(renewRetryInterval)
m.scheduleRenew()
}
- })()
+ }()
}
// RootCertificate implements mtls.CertificateProvider.
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")
}
--- /dev/null
+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",
+ ],
+)
--- /dev/null
+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
+}
"//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",
"@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",
],
)
"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"
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)
}
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)
}
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
+ "encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"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()
), 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 {
}
}
+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) {
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
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
}
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
}
import (
"crypto/tls"
+ "encoding/asn1"
"errors"
"flag"
"fmt"
)
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 {
--- /dev/null
+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"],
+)
--- /dev/null
+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;
+}
+
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;
+ }
+}