Skip to content

Commit

Permalink
Check cnf claim with CSR or SSH public key fingerprint
Browse files Browse the repository at this point in the history
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
  • Loading branch information
maraino committed Dec 29, 2023
1 parent b75773e commit 59e39b7
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 9 deletions.
21 changes: 19 additions & 2 deletions authority/provisioner/jwk.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@ 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 {
SSH *SignSSHOptions `json:"ssh,omitempty"`
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 {
Expand Down Expand Up @@ -183,13 +188,20 @@ 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,
// modifiers / withOptions
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),
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions authority/provisioner/sign_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/sha256"
"crypto/subtle"
"crypto/x509"
"encoding/base64"
"encoding/json"
"net"
"net/http"
Expand Down Expand Up @@ -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
}
28 changes: 28 additions & 0 deletions authority/provisioner/sign_ssh_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package provisioner

import (
"crypto/rsa"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 16 additions & 3 deletions authority/provisioner/x5c.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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{},
Expand Down Expand Up @@ -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
Expand Down
26 changes: 22 additions & 4 deletions authority/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
Expand All @@ -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,
Expand Down

0 comments on commit 59e39b7

Please sign in to comment.