forked from TrueCloudLab/certificates
Complete first version of provisioner implementations.
This commit is contained in:
parent
7eb6eb1d3e
commit
0dee841a4f
3 changed files with 222 additions and 131 deletions
|
@ -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
|
||||
// }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue