Complete first version of provisioner implementations.

This commit is contained in:
Mariano Cano 2019-03-06 14:54:56 -08:00
parent 7eb6eb1d3e
commit 0dee841a4f
3 changed files with 222 additions and 131 deletions

View file

@ -1,110 +1,53 @@
package authority package provisioner
import ( import (
"crypto/x509"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/cli/crypto/x509util" "github.com/smallstep/cli/crypto/x509util"
"github.com/smallstep/cli/jose"
jose "gopkg.in/square/go-jose.v2"
) )
// ProvisionerClaims so that individual provisioners can override global claims. // jwtPayload extends jwt.Claims with step attributes.
type ProvisionerClaims struct { type jwtPayload struct {
globalClaims *ProvisionerClaims jose.Claims
MinTLSDur *Duration `json:"minTLSCertDuration,omitempty"` SANs []string `json:"sans,omitempty"`
MaxTLSDur *Duration `json:"maxTLSCertDuration,omitempty"`
DefaultTLSDur *Duration `json:"defaultTLSCertDuration,omitempty"`
DisableRenewal *bool `json:"disableRenewal,omitempty"`
} }
// Init initializes and validates the individual provisioner claims. // JWT is the default provisioner, an entity that can sign tokens necessary for
func (pc *ProvisionerClaims) Init(global *ProvisionerClaims) (*ProvisionerClaims, error) { // signature requests.
if pc == nil { type JWT struct {
pc = &ProvisionerClaims{} Name string `json:"name,omitempty"`
} Type string `json:"type,omitempty"`
pc.globalClaims = global Key *jose.JSONWebKey `json:"key,omitempty"`
err := pc.Validate() EncryptedKey string `json:"encryptedKey,omitempty"`
return pc, err Claims *Claims `json:"claims,omitempty"`
} }
// DefaultTLSCertDuration returns the default TLS cert duration for the // GetID returns the provisioner unique identifier. The name and credential id
// provisioner. If the default is not set within the provisioner, then the global // should uniquely identify any JWT provisioner.
// default from the authority configuration will be used. func (p *JWT) GetID() string {
func (pc *ProvisionerClaims) DefaultTLSCertDuration() time.Duration { return p.Name + ":" + p.Key.KeyID
if pc.DefaultTLSDur == nil || pc.DefaultTLSDur.Duration == 0 {
return pc.globalClaims.DefaultTLSCertDuration()
}
return pc.DefaultTLSDur.Duration
} }
// MinTLSCertDuration returns the minimum TLS cert duration for the provisioner. // GetName returns the name of the provisioner
// If the minimum is not set within the provisioner, then the global func (p *JWT) GetName() string {
// minimum from the authority configuration will be used. return p.Name
func (pc *ProvisionerClaims) MinTLSCertDuration() time.Duration {
if pc.MinTLSDur == nil || pc.MinTLSDur.Duration == 0 {
return pc.globalClaims.MinTLSCertDuration()
}
return pc.MinTLSDur.Duration
} }
// MaxTLSCertDuration returns the maximum TLS cert duration for the provisioner. // GetType returns the type of provisioner.
// If the maximum is not set within the provisioner, then the global func (p *JWT) GetType() Type {
// maximum from the authority configuration will be used. return TypeJWK
func (pc *ProvisionerClaims) MaxTLSCertDuration() time.Duration {
if pc.MaxTLSDur == nil || pc.MaxTLSDur.Duration == 0 {
return pc.globalClaims.MaxTLSCertDuration()
}
return pc.MaxTLSDur.Duration
} }
// IsDisableRenewal returns if the renewal flow is disabled for the // GetEncryptedKey returns the base provisioner encrypted key if it's defined.
// provisioner. If the property is not set within the provisioner, then the func (p *JWT) GetEncryptedKey() (string, string, bool) {
// global value from the authority configuration will be used. return p.Key.KeyID, p.EncryptedKey, len(p.EncryptedKey) > 0
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"`
} }
// Init initializes and validates a the fields of Provisioner type. // 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 { switch {
case p.Name == "": case p.Name == "":
return errors.New("provisioner name cannot be empty") return errors.New("provisioner name cannot be empty")
@ -115,30 +58,108 @@ func (p *Provisioner) Init(global *ProvisionerClaims) error {
case p.Key == nil: case p.Key == nil:
return errors.New("provisioner key cannot be empty") return errors.New("provisioner key cannot be empty")
} }
var err error
p.Claims, err = p.Claims.Init(global) p.Claims, err = p.Claims.Init(global)
return err return err
} }
// getTLSApps returns a list of modifiers and validators that will be applied to func (p *JWT) Authorize(token string) ([]SignOption, error) {
// the certificate. jwt, err := jose.ParseSigned(token)
func (p *Provisioner) getTLSApps(so SignOptions) ([]x509util.WithOption, []certClaim, error) { if err != nil {
c := p.Claims return nil, errors.Wrapf(err, "error parsing token")
return []x509util.WithOption{ }
x509util.WithNotBeforeAfterDuration(so.NotBefore,
so.NotAfter, c.DefaultTLSCertDuration()), var claims jwtPayload
withProvisionerOID(p.Name, p.Key.KeyID), if err = jwt.Claims(p.Key, &claims); err != nil {
}, []certClaim{ return nil, errors.Wrap(err, "error parsing claims")
&certTemporalClaim{ }
min: c.MinTLSCertDuration(),
max: c.MaxTLSCertDuration(), // According to "rfc7519 JSON Web Token" acceptable skew should be no
}, // more than a few minutes.
}, nil 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 // AuthorizeRenewal returns an error if the renewal is disabled.
// uniquely identify any provisioner. func (p *JWT) AuthorizeRenewal(cert *x509.Certificate) error {
func (p *Provisioner) ID() string { if p.Claims.IsDisableRenewal() {
return p.Name + ":" + p.Key.KeyID 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
// }

View file

@ -1,6 +1,7 @@
package provisioner package provisioner
import ( import (
"crypto/x509"
"encoding/json" "encoding/json"
"net/http" "net/http"
"time" "time"
@ -9,6 +10,8 @@ import (
"github.com/smallstep/cli/jose" "github.com/smallstep/cli/jose"
) )
// openIDConfiguration contains the necessary properties in the
// `/.well-known/openid-configuration` document.
type openIDConfiguration struct { type openIDConfiguration struct {
Issuer string `json:"issuer"` Issuer string `json:"issuer"`
JWKSetURI string `json:"jwks_uri"` JWKSetURI string `json:"jwks_uri"`
@ -20,7 +23,7 @@ type openIDPayload struct {
AtHash string `json:"at_hash"` AtHash string `json:"at_hash"`
AuthorizedParty string `json:"azp"` AuthorizedParty string `json:"azp"`
Email string `json:"email"` Email string `json:"email"`
EmailVerified string `json:"email_verified"` EmailVerified bool `json:"email_verified"`
Hd string `json:"hd"` Hd string `json:"hd"`
Nonce string `json:"nonce"` Nonce string `json:"nonce"`
} }
@ -32,7 +35,7 @@ type OIDC struct {
ClientID string `json:"clientID"` ClientID string `json:"clientID"`
ConfigurationEndpoint string `json:"configurationEndpoint"` ConfigurationEndpoint string `json:"configurationEndpoint"`
Claims *Claims `json:"claims,omitempty"` Claims *Claims `json:"claims,omitempty"`
Admins []string `json:"admins"` Admins []string `json:"admins,omitempty"`
configuration openIDConfiguration configuration openIDConfiguration
keyStore *keyStore keyStore *keyStore
} }
@ -48,8 +51,29 @@ func (o *OIDC) IsAdmin(email string) bool {
return false return false
} }
// Validate validates and initializes the OIDC provider. // GetID returns the provisioner unique identifier, the OIDC provisioner the
func (o *OIDC) Validate() error { // 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 { switch {
case o.Name == "": case o.Name == "":
return errors.New("name cannot be empty") return errors.New("name cannot be empty")
@ -59,21 +83,22 @@ func (o *OIDC) Validate() error {
return errors.New("configurationEndpoint cannot be empty") return errors.New("configurationEndpoint cannot be empty")
} }
// Decode openid-configuration endpoint // Update claims with global ones
var conf openIDConfiguration if o.Claims, err = o.Claims.Init(global); err != nil {
if err := getAndDecode(o.ConfigurationEndpoint, &conf); err != nil {
return err 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) return errors.Errorf("error parsing %s: jwks_uri cannot be empty", o.ConfigurationEndpoint)
} }
// Get JWK key set // Get JWK key set
keyStore, err := newKeyStore(conf.JWKSetURI) o.keyStore, err = newKeyStore(o.configuration.JWKSetURI)
if err != nil { if err != nil {
return err return err
} }
o.configuration = conf
o.keyStore = keyStore
return nil return nil
} }
@ -102,8 +127,8 @@ func (o *OIDC) Authorize(token string) ([]SignOption, error) {
return nil, errors.Wrapf(err, "error parsing token") return nil, errors.Wrapf(err, "error parsing token")
} }
var claims openIDPayload
// Parse claims to get the kid // Parse claims to get the kid
var claims openIDPayload
if err := jwt.UnsafeClaimsWithoutVerification(&claims); err != nil { if err := jwt.UnsafeClaimsWithoutVerification(&claims); err != nil {
return nil, errors.Wrap(err, "error parsing claims") return nil, errors.Wrap(err, "error parsing claims")
} }
@ -131,9 +156,24 @@ func (o *OIDC) Authorize(token string) ([]SignOption, error) {
return []SignOption{ return []SignOption{
emailOnlyIdentity(claims.Email), emailOnlyIdentity(claims.Email),
newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID),
}, nil }, 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 { func getAndDecode(uri string, v interface{}) error {
resp, err := http.Get(uri) resp, err := http.Get(uri)
if err != nil { if err != nil {

View file

@ -1,6 +1,7 @@
package provisioner package provisioner
import ( import (
"crypto/x509"
"encoding/json" "encoding/json"
"strings" "strings"
@ -9,10 +10,14 @@ import (
// Interface is the interface that all provisioner types must implement. // Interface is the interface that all provisioner types must implement.
type Interface interface { type Interface interface {
ID() string GetID() string
GetName() string
GetType() Type
GetEncryptedKey() (kid string, key string, ok bool) GetEncryptedKey() (kid string, key string, ok bool)
Init(claims *Claims) error Init(claims *Claims) error
Authorize(token string) ([]SignOption, error) Authorize(token string) ([]SignOption, error)
AuthorizeRenewal(cert *x509.Certificate) error
AuthorizeRevoke(token string) error
} }
// Type indicates the provisioner Type. // Type indicates the provisioner Type.
@ -34,14 +39,25 @@ type provisioner struct {
// also implements custom marshalers and unmarshalers so different provisioners // also implements custom marshalers and unmarshalers so different provisioners
// can be represented in a configuration type. // can be represented in a configuration type.
type Provisioner struct { type Provisioner struct {
typ Type
base Interface base Interface
} }
// ID returns the base provisioner unique ID. This identifier is used as the key // New creates a new provisioner from the base provisioner.
// in a provisioner.Collection. func New(base Interface) *Provisioner {
func (p *Provisioner) ID() string { return &Provisioner{
return p.base.ID() 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. // 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() return p.base.GetEncryptedKey()
} }
// Type return the provisioners type. // GetName returns the name of the provisioner
func (p *Provisioner) Type() Type { func (p *Provisioner) GetName() string {
return p.typ 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. // 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) 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. // MarshalJSON implements the json.Marshaler interface on the Provisioner type.
func (p *Provisioner) MarshalJSON() ([]byte, error) { func (p *Provisioner) MarshalJSON() ([]byte, error) {
return json.Marshal(p.base) return json.Marshal(p.base)
@ -79,14 +111,12 @@ func (p *Provisioner) UnmarshalJSON(data []byte) error {
} }
switch strings.ToLower(typ.Type) { switch strings.ToLower(typ.Type) {
case "jwt": case "jwk":
p.typ = TypeJWK
p.base = &JWT{} p.base = &JWT{}
case "oidc": case "oidc":
p.typ = TypeOIDC
p.base = &OIDC{} p.base = &OIDC{}
default: 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 { if err := json.Unmarshal(data, &p.base); err != nil {
return errors.Errorf("error unmarshalling provisioner") return errors.Errorf("error unmarshalling provisioner")