From 0dee841a4f6ac48d07c36c9cac6f6613f71b1917 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 6 Mar 2019 14:54:56 -0800 Subject: [PATCH] Complete first version of provisioner implementations. --- authority/provisioner/jwt.go | 233 +++++++++++++++------------ authority/provisioner/oidc.go | 64 ++++++-- authority/provisioner/provisioner.go | 56 +++++-- 3 files changed, 222 insertions(+), 131 deletions(-) diff --git a/authority/provisioner/jwt.go b/authority/provisioner/jwt.go index 6dd1b1ac..abfad915 100644 --- a/authority/provisioner/jwt.go +++ b/authority/provisioner/jwt.go @@ -1,110 +1,53 @@ -package authority +package provisioner import ( + "crypto/x509" "time" "github.com/pkg/errors" "github.com/smallstep/cli/crypto/x509util" - - jose "gopkg.in/square/go-jose.v2" + "github.com/smallstep/cli/jose" ) -// ProvisionerClaims so that individual provisioners can override global claims. -type ProvisionerClaims struct { - globalClaims *ProvisionerClaims - MinTLSDur *Duration `json:"minTLSCertDuration,omitempty"` - MaxTLSDur *Duration `json:"maxTLSCertDuration,omitempty"` - DefaultTLSDur *Duration `json:"defaultTLSCertDuration,omitempty"` - DisableRenewal *bool `json:"disableRenewal,omitempty"` +// jwtPayload extends jwt.Claims with step attributes. +type jwtPayload struct { + jose.Claims + SANs []string `json:"sans,omitempty"` } -// Init initializes and validates the individual provisioner claims. -func (pc *ProvisionerClaims) Init(global *ProvisionerClaims) (*ProvisionerClaims, error) { - if pc == nil { - pc = &ProvisionerClaims{} - } - pc.globalClaims = global - err := pc.Validate() - return pc, err +// JWT is the default provisioner, an entity that can sign tokens necessary for +// signature requests. +type JWT struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Key *jose.JSONWebKey `json:"key,omitempty"` + EncryptedKey string `json:"encryptedKey,omitempty"` + Claims *Claims `json:"claims,omitempty"` } -// DefaultTLSCertDuration returns the default TLS cert duration for the -// provisioner. If the default is not set within the provisioner, then the global -// default from the authority configuration will be used. -func (pc *ProvisionerClaims) DefaultTLSCertDuration() time.Duration { - if pc.DefaultTLSDur == nil || pc.DefaultTLSDur.Duration == 0 { - return pc.globalClaims.DefaultTLSCertDuration() - } - return pc.DefaultTLSDur.Duration +// GetID returns the provisioner unique identifier. The name and credential id +// should uniquely identify any JWT provisioner. +func (p *JWT) GetID() string { + return p.Name + ":" + p.Key.KeyID } -// MinTLSCertDuration returns the minimum TLS cert duration for the provisioner. -// If the minimum is not set within the provisioner, then the global -// minimum from the authority configuration will be used. -func (pc *ProvisionerClaims) MinTLSCertDuration() time.Duration { - if pc.MinTLSDur == nil || pc.MinTLSDur.Duration == 0 { - return pc.globalClaims.MinTLSCertDuration() - } - return pc.MinTLSDur.Duration +// GetName returns the name of the provisioner +func (p *JWT) GetName() string { + return p.Name } -// MaxTLSCertDuration returns the maximum TLS cert duration for the provisioner. -// If the maximum is not set within the provisioner, then the global -// maximum from the authority configuration will be used. -func (pc *ProvisionerClaims) MaxTLSCertDuration() time.Duration { - if pc.MaxTLSDur == nil || pc.MaxTLSDur.Duration == 0 { - return pc.globalClaims.MaxTLSCertDuration() - } - return pc.MaxTLSDur.Duration +// GetType returns the type of provisioner. +func (p *JWT) GetType() Type { + return TypeJWK } -// IsDisableRenewal returns if the renewal flow is disabled for the -// provisioner. If the property is not set within the provisioner, then the -// global value from the authority configuration will be used. -func (pc *ProvisionerClaims) IsDisableRenewal() bool { - if pc.DisableRenewal == nil { - return pc.globalClaims.IsDisableRenewal() - } - return *pc.DisableRenewal -} - -// Validate validates and modifies the Claims with default values. -func (pc *ProvisionerClaims) Validate() error { - var ( - min = pc.MinTLSCertDuration() - max = pc.MaxTLSCertDuration() - def = pc.DefaultTLSCertDuration() - ) - switch { - case min == 0: - return errors.Errorf("claims: MinTLSCertDuration cannot be empty") - case max == 0: - return errors.Errorf("claims: MaxTLSCertDuration cannot be empty") - case def == 0: - return errors.Errorf("claims: DefaultTLSCertDuration cannot be empty") - case max < min: - return errors.Errorf("claims: MaxCertDuration cannot be less "+ - "than MinCertDuration: MaxCertDuration - %v, MinCertDuration - %v", max, min) - case def < min: - return errors.Errorf("claims: DefaultCertDuration cannot be less than MinCertDuration: DefaultCertDuration - %v, MinCertDuration - %v", def, min) - case max < def: - return errors.Errorf("claims: MaxCertDuration cannot be less than DefaultCertDuration: MaxCertDuration - %v, DefaultCertDuration - %v", max, def) - default: - return nil - } -} - -// Provisioner - authorized entity that can sign tokens necessary for signature requests. -type Provisioner struct { - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Key *jose.JSONWebKey `json:"key,omitempty"` - EncryptedKey string `json:"encryptedKey,omitempty"` - Claims *ProvisionerClaims `json:"claims,omitempty"` +// GetEncryptedKey returns the base provisioner encrypted key if it's defined. +func (p *JWT) GetEncryptedKey() (string, string, bool) { + return p.Key.KeyID, p.EncryptedKey, len(p.EncryptedKey) > 0 } // Init initializes and validates a the fields of Provisioner type. -func (p *Provisioner) Init(global *ProvisionerClaims) error { +func (p *JWT) Init(global *Claims) (err error) { switch { case p.Name == "": return errors.New("provisioner name cannot be empty") @@ -115,30 +58,108 @@ func (p *Provisioner) Init(global *ProvisionerClaims) error { case p.Key == nil: return errors.New("provisioner key cannot be empty") } - - var err error p.Claims, err = p.Claims.Init(global) return err } -// getTLSApps returns a list of modifiers and validators that will be applied to -// the certificate. -func (p *Provisioner) getTLSApps(so SignOptions) ([]x509util.WithOption, []certClaim, error) { - c := p.Claims - return []x509util.WithOption{ - x509util.WithNotBeforeAfterDuration(so.NotBefore, - so.NotAfter, c.DefaultTLSCertDuration()), - withProvisionerOID(p.Name, p.Key.KeyID), - }, []certClaim{ - &certTemporalClaim{ - min: c.MinTLSCertDuration(), - max: c.MaxTLSCertDuration(), - }, - }, nil +func (p *JWT) Authorize(token string) ([]SignOption, error) { + jwt, err := jose.ParseSigned(token) + if err != nil { + return nil, errors.Wrapf(err, "error parsing token") + } + + var claims jwtPayload + if err = jwt.Claims(p.Key, &claims); err != nil { + return nil, errors.Wrap(err, "error parsing claims") + } + + // According to "rfc7519 JSON Web Token" acceptable skew should be no + // more than a few minutes. + if err = claims.ValidateWithLeeway(jose.Expected{ + Issuer: p.Name, + }, time.Minute); err != nil { + return nil, errors.Wrapf(err, "invalid token") + } + + // Do not accept tokens issued before the start of the ca. + // This check is meant as a stopgap solution to the current lack of a persistence layer. + // if a.config.AuthorityConfig != nil && !a.config.AuthorityConfig.DisableIssuedAtCheck { + // if claims.IssuedAt > 0 && claims.IssuedAt.Time().Before(a.startTime) { + // return nil, &apiError{errors.New("token issued before the bootstrap of certificate authority"), + // http.StatusUnauthorized, errContext} + // } + // } + + // if !matchesAudience(claims.Audience, a.audiences) { + // return nil, &apiError{errors.New("authorize: token audience invalid"), http.StatusUnauthorized, + // errContext} + // } + + if claims.Subject == "" { + return nil, errors.New("token subject cannot be empty") + } + + // NOTE: This is for backwards compatibility with older versions of cli + // and certificates. Older versions added the token subject as the only SAN + // in a CSR by default. + if len(claims.SANs) == 0 { + claims.SANs = []string{claims.Subject} + } + + dnsNames, ips := x509util.SplitSANs(claims.SANs) + if err != nil { + return nil, err + } + + signOps := []SignOption{ + commonNameValidator(claims.Subject), + dnsNamesValidator(dnsNames), + ipAddressesValidator(ips), + // profileWithOption(x509util.WithNotBeforeAfterDuration(so.NotBefore, so.NotAfter, p.Claims.DefaultTLSCertDuration())), + &validityValidator{ + min: p.Claims.MinTLSCertDuration(), + max: p.Claims.MaxTLSCertDuration(), + }, + newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID), + } + + // Store the token to protect against reuse. + // if _, ok := a.ottMap.LoadOrStore(claims.ID, &idUsed{ + // UsedAt: time.Now().Unix(), + // Subject: claims.Subject, + // }); ok { + // return nil, &apiError{errors.Errorf("token already used"), http.StatusUnauthorized, + // errContext} + // } + + return signOps, nil } -// ID returns the provisioner identifier. The name and credential id should -// uniquely identify any provisioner. -func (p *Provisioner) ID() string { - return p.Name + ":" + p.Key.KeyID +// AuthorizeRenewal returns an error if the renewal is disabled. +func (p *JWT) AuthorizeRenewal(cert *x509.Certificate) error { + if p.Claims.IsDisableRenewal() { + return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + } + return nil } + +// AuthorizeRevoke returns an error if the provisioner does not have rights to +// revoke the certificate with serial number in the `sub` property. +func (p *JWT) AuthorizeRevoke(token string) error { + return errors.New("not implemented") +} + +// // getTLSApps returns a list of modifiers and validators that will be applied to +// // the certificate. +// func (p *JWT) getTLSApps(so SignOptions) ([]x509util.WithOption, []certClaim, error) { +// c := p.Claims +// return []x509util.WithOption{ +// x509util.WithNotBeforeAfterDuration(so.NotBefore, so.NotAfter, c.DefaultTLSCertDuration()), +// withProvisionerOID(p.Name, p.Key.KeyID), +// }, []certClaim{ +// &certTemporalClaim{ +// min: c.MinTLSCertDuration(), +// max: c.MaxTLSCertDuration(), +// }, +// }, nil +// } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index 3c401441..a3946ccd 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -1,6 +1,7 @@ package provisioner import ( + "crypto/x509" "encoding/json" "net/http" "time" @@ -9,6 +10,8 @@ import ( "github.com/smallstep/cli/jose" ) +// openIDConfiguration contains the necessary properties in the +// `/.well-known/openid-configuration` document. type openIDConfiguration struct { Issuer string `json:"issuer"` JWKSetURI string `json:"jwks_uri"` @@ -20,7 +23,7 @@ type openIDPayload struct { AtHash string `json:"at_hash"` AuthorizedParty string `json:"azp"` Email string `json:"email"` - EmailVerified string `json:"email_verified"` + EmailVerified bool `json:"email_verified"` Hd string `json:"hd"` Nonce string `json:"nonce"` } @@ -32,7 +35,7 @@ type OIDC struct { ClientID string `json:"clientID"` ConfigurationEndpoint string `json:"configurationEndpoint"` Claims *Claims `json:"claims,omitempty"` - Admins []string `json:"admins"` + Admins []string `json:"admins,omitempty"` configuration openIDConfiguration keyStore *keyStore } @@ -48,8 +51,29 @@ func (o *OIDC) IsAdmin(email string) bool { return false } -// Validate validates and initializes the OIDC provider. -func (o *OIDC) Validate() error { +// GetID returns the provisioner unique identifier, the OIDC provisioner the +// uses the clientID for this. +func (o *OIDC) GetID() string { + return o.ClientID +} + +// GetName returns the name of the provisioner. +func (o *OIDC) GetName() string { + return o.Name +} + +// GetType returns the type of provisioner. +func (o *OIDC) GetType() Type { + return TypeOIDC +} + +// GetEncryptedKey is not available in an OIDC provisioner. +func (o *OIDC) GetEncryptedKey() (kid string, key string, ok bool) { + return "", "", false +} + +// Init validates and initializes the OIDC provider. +func (o *OIDC) Init(global *Claims) (err error) { switch { case o.Name == "": return errors.New("name cannot be empty") @@ -59,21 +83,22 @@ func (o *OIDC) Validate() error { return errors.New("configurationEndpoint cannot be empty") } - // Decode openid-configuration endpoint - var conf openIDConfiguration - if err := getAndDecode(o.ConfigurationEndpoint, &conf); err != nil { + // Update claims with global ones + if o.Claims, err = o.Claims.Init(global); err != nil { return err } - if conf.JWKSetURI == "" { + // Decode openid-configuration endpoint + if err := getAndDecode(o.ConfigurationEndpoint, &o.configuration); err != nil { + return err + } + if o.configuration.JWKSetURI == "" { return errors.Errorf("error parsing %s: jwks_uri cannot be empty", o.ConfigurationEndpoint) } // Get JWK key set - keyStore, err := newKeyStore(conf.JWKSetURI) + o.keyStore, err = newKeyStore(o.configuration.JWKSetURI) if err != nil { return err } - o.configuration = conf - o.keyStore = keyStore return nil } @@ -102,8 +127,8 @@ func (o *OIDC) Authorize(token string) ([]SignOption, error) { return nil, errors.Wrapf(err, "error parsing token") } - var claims openIDPayload // Parse claims to get the kid + var claims openIDPayload if err := jwt.UnsafeClaimsWithoutVerification(&claims); err != nil { return nil, errors.Wrap(err, "error parsing claims") } @@ -131,9 +156,24 @@ func (o *OIDC) Authorize(token string) ([]SignOption, error) { return []SignOption{ emailOnlyIdentity(claims.Email), + newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID), }, nil } +// AuthorizeRenewal returns an error if the renewal is disabled. +func (o *OIDC) AuthorizeRenewal(cert *x509.Certificate) error { + if o.Claims.IsDisableRenewal() { + return errors.Errorf("renew is disabled for provisioner %s", o.GetID()) + } + return nil +} + +// AuthorizeRevoke returns an error if the provisioner does not have rights to +// revoke the certificate with serial number in the `sub` property. +func (o *OIDC) AuthorizeRevoke(token string) error { + return errors.New("not implemented") +} + func getAndDecode(uri string, v interface{}) error { resp, err := http.Get(uri) if err != nil { diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index e064c112..2953fa43 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -1,6 +1,7 @@ package provisioner import ( + "crypto/x509" "encoding/json" "strings" @@ -9,10 +10,14 @@ import ( // Interface is the interface that all provisioner types must implement. type Interface interface { - ID() string + GetID() string + GetName() string + GetType() Type GetEncryptedKey() (kid string, key string, ok bool) Init(claims *Claims) error Authorize(token string) ([]SignOption, error) + AuthorizeRenewal(cert *x509.Certificate) error + AuthorizeRevoke(token string) error } // Type indicates the provisioner Type. @@ -34,14 +39,25 @@ type provisioner struct { // also implements custom marshalers and unmarshalers so different provisioners // can be represented in a configuration type. type Provisioner struct { - typ Type base Interface } -// ID returns the base provisioner unique ID. This identifier is used as the key -// in a provisioner.Collection. -func (p *Provisioner) ID() string { - return p.base.ID() +// New creates a new provisioner from the base provisioner. +func New(base Interface) *Provisioner { + return &Provisioner{ + base: base, + } +} + +// Base returns the base type of the provisioner. +func (p *Provisioner) Base() Interface { + return p.base +} + +// GetID returns the base provisioner unique ID. This identifier is used as the +// key in a provisioner.Collection. +func (p *Provisioner) GetID() string { + return p.base.GetID() } // GetEncryptedKey returns the base provisioner encrypted key if it's defined. @@ -49,9 +65,14 @@ func (p *Provisioner) GetEncryptedKey() (string, string, bool) { return p.base.GetEncryptedKey() } -// Type return the provisioners type. -func (p *Provisioner) Type() Type { - return p.typ +// GetName returns the name of the provisioner +func (p *Provisioner) GetName() string { + return p.base.GetName() +} + +// GetType return the provisioners type. +func (p *Provisioner) GetType() Type { + return p.base.GetType() } // Init initializes the base provisioner with the given claims. @@ -65,6 +86,17 @@ func (p *Provisioner) Authorize(token string) ([]SignOption, error) { return p.base.Authorize(token) } +// AuthorizeRenewal checks if the base provisioner authorizes the renewal. +func (p *Provisioner) AuthorizeRenewal(cert *x509.Certificate) error { + return p.base.AuthorizeRenewal(cert) +} + +// AuthorizeRevoke checks on the base provisioner if the given token has revoke +// access. +func (p *Provisioner) AuthorizeRevoke(token string) error { + return p.base.AuthorizeRevoke(token) +} + // MarshalJSON implements the json.Marshaler interface on the Provisioner type. func (p *Provisioner) MarshalJSON() ([]byte, error) { return json.Marshal(p.base) @@ -79,14 +111,12 @@ func (p *Provisioner) UnmarshalJSON(data []byte) error { } switch strings.ToLower(typ.Type) { - case "jwt": - p.typ = TypeJWK + case "jwk": p.base = &JWT{} case "oidc": - p.typ = TypeOIDC p.base = &OIDC{} default: - return errors.New("provisioner type not supported") + return errors.Errorf("provisioner type %s not supported", typ.Type) } if err := json.Unmarshal(data, &p.base); err != nil { return errors.Errorf("error unmarshalling provisioner")