From 59e39b7a1d72bcc87d0b6aef4634b1f5a3cba802 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 28 Dec 2023 17:09:39 -0800 Subject: [PATCH] Check cnf claim with CSR or SSH public key fingerprint This commit allows tying tokens with the provided CSR or SSH public key. Tokens with a confirmation claim kid (cnf.kid) will validate that the provided fingerprint (kid) matches the CSR or SSH public key. This check will only be present in JWK and X5C provisioners. Fixes #1637 --- authority/provisioner/jwk.go | 21 +++++++++++++++-- authority/provisioner/sign_options.go | 21 +++++++++++++++++ authority/provisioner/sign_ssh_options.go | 28 +++++++++++++++++++++++ authority/provisioner/x5c.go | 19 ++++++++++++--- authority/ssh.go | 26 +++++++++++++++++---- 5 files changed, 106 insertions(+), 9 deletions(-) diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index 3a7512b8e..2f73c8e5c 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -19,8 +19,9 @@ import ( // jwtPayload extends jwt.Claims with step attributes. type jwtPayload struct { jose.Claims - SANs []string `json:"sans,omitempty"` - Step *stepPayload `json:"step,omitempty"` + SANs []string `json:"sans,omitempty"` + Step *stepPayload `json:"step,omitempty"` + Confirmation *cnfPayload `json:"cnf,omitempty"` } type stepPayload struct { @@ -28,6 +29,10 @@ type stepPayload struct { RA *RAInfo `json:"ra,omitempty"` } +type cnfPayload struct { + Kid string `json:"kid,omitempty"` +} + // JWK is the default provisioner, an entity that can sign tokens necessary for // signature requests. type JWK struct { @@ -183,6 +188,12 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } } + // Check the fingerprint of the certificate request if given. + var fingerprint string + if claims.Confirmation != nil { + fingerprint = claims.Confirmation.Kid + } + return []SignOption{ self, templateOptions, @@ -190,6 +201,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID).WithControllerOptions(p.ctl), profileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()), // validators + fingerprintValidator(fingerprint), commonNameSliceValidator(append([]string{claims.Subject}, claims.SANs...)), defaultPublicKeyValidator{}, newDefaultSANsValidator(ctx, claims.SANs), @@ -229,6 +241,11 @@ func (p *JWK) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e sshCertOptionsValidator(SignSSHOptions{KeyID: claims.Subject}), } + // Check the fingerprint of the certificate request if given. + if claims.Confirmation != nil && claims.Confirmation.Kid != "" { + signOptions = append(signOptions, sshFingerprintValidator(claims.Confirmation.Kid)) + } + // Default template attributes. certType := sshutil.UserCert keyID := claims.Subject diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index fec9b9f6f..62017fad5 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -5,7 +5,10 @@ import ( "crypto/ecdsa" "crypto/ed25519" "crypto/rsa" + "crypto/sha256" + "crypto/subtle" "crypto/x509" + "encoding/base64" "encoding/json" "net" "net/http" @@ -492,3 +495,21 @@ func (o *provisionerExtensionOption) Modify(cert *x509.Certificate, _ SignOption cert.ExtraExtensions = append(cert.ExtraExtensions, ext) return nil } + +// fingerprintValidator is a CertificateRequestValidator that checks the +// fingerprint of the certificate with the provided one. +type fingerprintValidator string + +func (s fingerprintValidator) Valid(cr *x509.CertificateRequest) error { + if s != "" { + expected, err := base64.RawURLEncoding.DecodeString(string(s)) + if err != nil { + return errs.ForbiddenErr(err, "error decoding fingerprint") + } + sum := sha256.Sum256(cr.Raw) + if subtle.ConstantTimeCompare(expected, sum[:]) != 1 { + return errs.Forbidden("certificate request fingerprint does not match %q", s) + } + } + return nil +} diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index ee74ded35..d39447a49 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -2,6 +2,9 @@ package provisioner import ( "crypto/rsa" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" "encoding/binary" "encoding/json" "fmt" @@ -44,6 +47,13 @@ type SSHCertOptionsValidator interface { Valid(got SignSSHOptions) error } +// SSHPublicKeyValidator is the interface used to validate the public key of an +// SSH certificate. +type SSHPublicKeyValidator interface { + SignOption + Valid(got ssh.PublicKey) error +} + // SignSSHOptions contains the options that can be passed to the SignSSH method. type SignSSHOptions struct { CertType string `json:"certType"` @@ -419,6 +429,24 @@ func (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) } } +// sshFingerprintValidator is a SSHCertValidator that checks the +// fingerprint of the public key with the provided one. +type sshFingerprintValidator string + +func (s sshFingerprintValidator) Valid(key ssh.PublicKey) error { + if s != "" { + expected, err := base64.RawURLEncoding.DecodeString(string(s)) + if err != nil { + return errs.ForbiddenErr(err, "error decoding fingerprint") + } + sum := sha256.Sum256(key.Marshal()) + if subtle.ConstantTimeCompare(expected, sum[:]) != 1 { + return errs.Forbidden("ssh public key fingerprint does not match %q", s) + } + } + return nil +} + // sshCertTypeUInt32 func sshCertTypeUInt32(ct string) uint32 { switch ct { diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go index 9b1f2b086..d1cb23c09 100644 --- a/authority/provisioner/x5c.go +++ b/authority/provisioner/x5c.go @@ -21,9 +21,10 @@ import ( // x5cPayload extends jwt.Claims with step attributes. type x5cPayload struct { jose.Claims - SANs []string `json:"sans,omitempty"` - Step *stepPayload `json:"step,omitempty"` - chains [][]*x509.Certificate + SANs []string `json:"sans,omitempty"` + Step *stepPayload `json:"step,omitempty"` + Confirmation *cnfPayload + chains [][]*x509.Certificate } // X5C is the default provisioner, an entity that can sign tokens necessary for @@ -233,6 +234,12 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } } + // Check the fingerprint of the certificate request if given. + var fingerprint string + if claims.Confirmation != nil { + fingerprint = claims.Confirmation.Kid + } + return []SignOption{ self, templateOptions, @@ -243,6 +250,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er x5cLeaf.NotBefore, x5cLeaf.NotAfter, }, // validators + fingerprintValidator(fingerprint), commonNameValidator(claims.Subject), newDefaultSANsValidator(ctx, claims.SANs), defaultPublicKeyValidator{}, @@ -285,6 +293,11 @@ func (p *X5C) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e sshCertOptionsValidator(SignSSHOptions{KeyID: claims.Subject}), } + // Check the fingerprint of the certificate request if given. + if claims.Confirmation != nil && claims.Confirmation.Kid != "" { + signOptions = append(signOptions, sshFingerprintValidator(claims.Confirmation.Kid)) + } + // Default template attributes. certType := sshutil.UserCert keyID := claims.Subject diff --git a/authority/ssh.go b/authority/ssh.go index f9371d60e..868dd0131 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -148,12 +148,16 @@ func (a *Authority) GetSSHBastion(ctx context.Context, user, hostname string) (* // SignSSH creates a signed SSH certificate with the given public key and options. func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { var ( - certOptions []sshutil.Option - mods []provisioner.SSHCertModifier - validators []provisioner.SSHCertValidator + certOptions []sshutil.Option + mods []provisioner.SSHCertModifier + validators []provisioner.SSHCertValidator + keyValidators []provisioner.SSHPublicKeyValidator ) - // Validate given options. + // Validate given key and options + if key == nil { + return nil, errs.BadRequest("ssh public key cannot be nil") + } if err := opts.Validate(); err != nil { return nil, err } @@ -177,6 +181,10 @@ func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provision case provisioner.SSHCertModifier: mods = append(mods, o) + // validate the ssh public key + case provisioner.SSHPublicKeyValidator: + keyValidators = append(keyValidators, o) + // validate the ssh.Certificate case provisioner.SSHCertValidator: validators = append(validators, o) @@ -196,6 +204,16 @@ func (a *Authority) SignSSH(_ context.Context, key ssh.PublicKey, opts provision } } + // Validate public key + for _, v := range keyValidators { + if err := v.Valid(key); err != nil { + return nil, errs.ApplyOptions( + errs.ForbiddenErr(err, err.Error()), + errs.WithKeyVal("signOptions", signOpts), + ) + } + } + // Simulated certificate request with request options. cr := sshutil.CertificateRequest{ Type: opts.CertType,