smallstep-certificates/authority/provisioners.go
2021-07-02 19:05:17 -07:00

602 lines
18 KiB
Go

package authority
import (
"context"
"crypto/x509"
"encoding/json"
"fmt"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
"go.step.sm/crypto/jose"
"go.step.sm/linkedca"
"gopkg.in/square/go-jose.v2/jwt"
)
// GetEncryptedKey returns the JWE key corresponding to the given kid argument.
func (a *Authority) GetEncryptedKey(kid string) (string, error) {
a.adminMutex.RLock()
defer a.adminMutex.RUnlock()
key, ok := a.provisioners.LoadEncryptedKey(kid)
if !ok {
return "", errs.NotFound("encrypted key with kid %s was not found", kid)
}
return key, nil
}
// GetProvisioners returns a map listing each provisioner and the JWK Key Set
// with their public keys.
func (a *Authority) GetProvisioners(cursor string, limit int) (provisioner.List, string, error) {
a.adminMutex.RLock()
defer a.adminMutex.RUnlock()
provisioners, nextCursor := a.provisioners.Find(cursor, limit)
return provisioners, nextCursor, nil
}
// LoadProvisionerByCertificate returns an interface to the provisioner that
// provisioned the certificate.
func (a *Authority) LoadProvisionerByCertificate(crt *x509.Certificate) (provisioner.Interface, error) {
a.adminMutex.RLock()
defer a.adminMutex.RUnlock()
p, ok := a.provisioners.LoadByCertificate(crt)
if !ok {
return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from certificate")
}
return p, nil
}
// LoadProvisionerByToken returns an interface to the provisioner that
// provisioned the token.
func (a *Authority) LoadProvisionerByToken(token *jwt.JSONWebToken, claims *jwt.Claims) (provisioner.Interface, error) {
a.adminMutex.RLock()
defer a.adminMutex.RUnlock()
p, ok := a.provisioners.LoadByToken(token, claims)
if !ok {
return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from token")
}
return p, nil
}
// LoadProvisionerByID returns an interface to the provisioner with the given ID.
func (a *Authority) LoadProvisionerByID(id string) (provisioner.Interface, error) {
a.adminMutex.RLock()
defer a.adminMutex.RUnlock()
p, ok := a.provisioners.Load(id)
if !ok {
return nil, admin.NewError(admin.ErrorNotFoundType, "provisioner %s not found", id)
}
return p, nil
}
// LoadProvisionerByName returns an interface to the provisioner with the given Name.
func (a *Authority) LoadProvisionerByName(name string) (provisioner.Interface, error) {
a.adminMutex.RLock()
defer a.adminMutex.RUnlock()
p, ok := a.provisioners.LoadByName(name)
if !ok {
return nil, admin.NewError(admin.ErrorNotFoundType, "provisioner %s not found", name)
}
return p, nil
}
func (a *Authority) generateProvisionerConfig(ctx context.Context) (*provisioner.Config, error) {
// Merge global and configuration claims
claimer, err := provisioner.NewClaimer(a.config.AuthorityConfig.Claims, config.GlobalProvisionerClaims)
if err != nil {
return nil, err
}
// TODO: should we also be combining the ssh federated roots here?
// If we rotate ssh roots keys, sshpop provisioner will lose ability to
// validate old SSH certificates, unless they are added as federated certs.
sshKeys, err := a.GetSSHRoots(ctx)
if err != nil {
return nil, err
}
return &provisioner.Config{
Claims: claimer.Claims(),
Audiences: a.config.GetAudiences(),
DB: a.db,
SSHKeys: &provisioner.SSHKeys{
UserKeys: sshKeys.UserKeys,
HostKeys: sshKeys.HostKeys,
},
GetIdentityFunc: a.getIdentityFunc,
}, nil
}
// StoreProvisioner stores an provisioner.Interface to the authority.
func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
certProv, err := ProvisionerToCertificates(prov)
if err != nil {
return admin.WrapErrorISE(err,
"error converting to certificates provisioner from linkedca provisioner")
}
if _, ok := a.provisioners.LoadByName(prov.GetName()); ok {
return admin.NewError(admin.ErrorBadRequestType,
"provisioner with name %s already exists", prov.GetName())
}
if _, ok := a.provisioners.LoadByTokenID(certProv.GetIDForToken()); ok {
return admin.NewError(admin.ErrorBadRequestType,
"provisioner with token ID %s already exists", certProv.GetIDForToken())
}
// Store to database -- this will set the ID.
if err := a.adminDB.CreateProvisioner(ctx, prov); err != nil {
return admin.WrapErrorISE(err, "error creating admin")
}
// We need a new conversion that has the newly set ID.
certProv, err = ProvisionerToCertificates(prov)
if err != nil {
return admin.WrapErrorISE(err,
"error converting to certificates provisioner from linkedca provisioner")
}
provisionerConfig, err := a.generateProvisionerConfig(ctx)
if err != nil {
return admin.WrapErrorISE(err, "error generating provisioner config")
}
if err := certProv.Init(*provisionerConfig); err != nil {
return admin.WrapErrorISE(err, "error initializing provisioner %s", prov.Name)
}
if err := a.provisioners.Store(certProv); err != nil {
if err := a.reloadAdminResources(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources on failed provisioner store")
}
return admin.WrapErrorISE(err, "error storing provisioner in authority cache")
}
return nil
}
// UpdateProvisioner stores an provisioner.Interface to the authority.
func (a *Authority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error {
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
certProv, err := ProvisionerToCertificates(nu)
if err != nil {
return admin.WrapErrorISE(err,
"error converting to certificates provisioner from linkedca provisioner")
}
provisionerConfig, err := a.generateProvisionerConfig(ctx)
if err != nil {
return admin.WrapErrorISE(err, "error generating provisioner config")
}
if err := certProv.Init(*provisionerConfig); err != nil {
return admin.WrapErrorISE(err, "error initializing provisioner %s", nu.Name)
}
if err := a.provisioners.Update(certProv); err != nil {
return admin.WrapErrorISE(err, "error updating provisioner '%s' in authority cache", nu.Name)
}
if err := a.adminDB.UpdateProvisioner(ctx, nu); err != nil {
if err := a.reloadAdminResources(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources on failed provisioner update")
}
return admin.WrapErrorISE(err, "error updating provisioner '%s'", nu.Name)
}
return nil
}
// RemoveProvisioner removes an provisioner.Interface from the authority.
func (a *Authority) RemoveProvisioner(ctx context.Context, id string) error {
a.adminMutex.Lock()
defer a.adminMutex.Unlock()
p, ok := a.provisioners.Load(id)
if !ok {
return admin.NewError(admin.ErrorBadRequestType,
"provisioner %s not found", id)
}
provName, provID := p.GetName(), p.GetID()
// Validate
// - Check that there will be SUPER_ADMINs that remain after we
// remove this provisioner.
if a.admins.SuperCount() == a.admins.SuperCountByProvisioner(provName) {
return admin.NewError(admin.ErrorBadRequestType,
"cannot remove provisioner %s because no super admins will remain", provName)
}
// Delete all admins associated with the provisioner.
admins, ok := a.admins.LoadByProvisioner(provName)
if ok {
for _, adm := range admins {
if err := a.removeAdmin(ctx, adm.Id); err != nil {
return admin.WrapErrorISE(err, "error deleting admin %s, as part of provisioner %s deletion", adm.Subject, provName)
}
}
}
// Remove provisioner from authority caches.
if err := a.provisioners.Remove(provID); err != nil {
return admin.WrapErrorISE(err, "error removing admin from authority cache")
}
// Remove provisioner from database.
if err := a.adminDB.DeleteProvisioner(ctx, provID); err != nil {
if err := a.reloadAdminResources(ctx); err != nil {
return admin.WrapErrorISE(err, "error reloading admin resources on failed provisioner remove")
}
return admin.WrapErrorISE(err, "error deleting provisioner %s", provName)
}
return nil
}
func CreateFirstProvisioner(ctx context.Context, db admin.DB, password string) (*linkedca.Provisioner, error) {
jwk, jwe, err := jose.GenerateDefaultKeyPair([]byte(password))
if err != nil {
return nil, admin.WrapErrorISE(err, "error generating JWK key pair")
}
jwkPubBytes, err := jwk.MarshalJSON()
if err != nil {
return nil, admin.WrapErrorISE(err, "error marshaling JWK")
}
jwePrivStr, err := jwe.CompactSerialize()
if err != nil {
return nil, admin.WrapErrorISE(err, "error serializing JWE")
}
p := &linkedca.Provisioner{
Name: "Admin JWK",
Type: linkedca.Provisioner_JWK,
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_JWK{
JWK: &linkedca.JWKProvisioner{
PublicKey: jwkPubBytes,
EncryptedPrivateKey: []byte(jwePrivStr),
},
},
},
Claims: &linkedca.Claims{
X509: &linkedca.X509Claims{
Enabled: true,
Durations: &linkedca.Durations{
Default: "5m",
},
},
},
}
if err := db.CreateProvisioner(ctx, p); err != nil {
return nil, admin.WrapErrorISE(err, "error creating provisioner")
}
return p, nil
}
func ValidateClaims(c *linkedca.Claims) error {
if c == nil {
return nil
}
if c.X509 != nil {
if c.X509.Durations != nil {
if err := ValidateDurations(c.X509.Durations); err != nil {
return err
}
}
}
if c.Ssh != nil {
if c.Ssh.UserDurations != nil {
if err := ValidateDurations(c.Ssh.UserDurations); err != nil {
return err
}
}
if c.Ssh.HostDurations != nil {
if err := ValidateDurations(c.Ssh.HostDurations); err != nil {
return err
}
}
}
return nil
}
func ValidateDurations(d *linkedca.Durations) error {
var (
err error
min, max, def *provisioner.Duration
)
if d.Min != "" {
min, err = provisioner.NewDuration(d.Min)
if err != nil {
return admin.WrapError(admin.ErrorBadRequestType, err, "min duration '%s' is invalid", d.Min)
}
if min.Value() < 0 {
return admin.WrapError(admin.ErrorBadRequestType, err, "min duration '%s' cannot be less than 0", d.Min)
}
}
if d.Max != "" {
max, err = provisioner.NewDuration(d.Max)
if err != nil {
return admin.WrapError(admin.ErrorBadRequestType, err, "max duration '%s' is invalid", d.Max)
}
if max.Value() < 0 {
return admin.WrapError(admin.ErrorBadRequestType, err, "max duration '%s' cannot be less than 0", d.Max)
}
}
if d.Default != "" {
def, err = provisioner.NewDuration(d.Default)
if err != nil {
return admin.WrapError(admin.ErrorBadRequestType, err, "default duration '%s' is invalid", d.Default)
}
if def.Value() < 0 {
return admin.WrapError(admin.ErrorBadRequestType, err, "default duration '%s' cannot be less than 0", d.Default)
}
}
if d.Min != "" && d.Max != "" && min.Value() > max.Value() {
return admin.NewError(admin.ErrorBadRequestType,
"min duration '%s' cannot be greater than max duration '%s'", d.Min, d.Max)
}
if d.Min != "" && d.Default != "" && min.Value() > def.Value() {
return admin.NewError(admin.ErrorBadRequestType,
"min duration '%s' cannot be greater than default duration '%s'", d.Min, d.Default)
}
if d.Default != "" && d.Max != "" && min.Value() > def.Value() {
return admin.NewError(admin.ErrorBadRequestType,
"default duration '%s' cannot be greater than max duration '%s'", d.Default, d.Max)
}
return nil
}
func provisionerListToCertificates(l []*linkedca.Provisioner) (provisioner.List, error) {
var nu provisioner.List
for _, p := range l {
certProv, err := ProvisionerToCertificates(p)
if err != nil {
return nil, err
}
nu = append(nu, certProv)
}
return nu, nil
}
func optionsToCertificates(p *linkedca.Provisioner) *provisioner.Options {
ops := &provisioner.Options{
X509: &provisioner.X509Options{},
SSH: &provisioner.SSHOptions{},
}
if p.X509Template != nil {
ops.X509.Template = string(p.X509Template.Template)
ops.X509.TemplateData = p.X509Template.Data
}
if p.SshTemplate != nil {
ops.SSH.Template = string(p.SshTemplate.Template)
ops.SSH.TemplateData = p.SshTemplate.Data
}
return ops
}
func durationsToCertificates(d *linkedca.Durations) (min, max, def *provisioner.Duration, err error) {
if len(d.Min) > 0 {
min, err = provisioner.NewDuration(d.Min)
if err != nil {
return nil, nil, nil, admin.WrapErrorISE(err, "error parsing minimum duration '%s'", d.Min)
}
}
if len(d.Max) > 0 {
max, err = provisioner.NewDuration(d.Max)
if err != nil {
return nil, nil, nil, admin.WrapErrorISE(err, "error parsing maximum duration '%s'", d.Max)
}
}
if len(d.Default) > 0 {
def, err = provisioner.NewDuration(d.Default)
if err != nil {
return nil, nil, nil, admin.WrapErrorISE(err, "error parsing default duration '%s'", d.Default)
}
}
return
}
// claimsToCertificates converts the linkedca provisioner claims type to the
// certifictes claims type.
func claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) {
if c == nil {
return nil, nil
}
pc := &provisioner.Claims{
DisableRenewal: &c.DisableRenewal,
}
var err error
if xc := c.X509; xc != nil {
if d := xc.Durations; d != nil {
pc.MinTLSDur, pc.MaxTLSDur, pc.DefaultTLSDur, err = durationsToCertificates(d)
if err != nil {
return nil, err
}
}
}
if sc := c.Ssh; sc != nil {
pc.EnableSSHCA = &sc.Enabled
if d := sc.UserDurations; d != nil {
pc.MinUserSSHDur, pc.MaxUserSSHDur, pc.DefaultUserSSHDur, err = durationsToCertificates(d)
if err != nil {
return nil, err
}
}
if d := sc.HostDurations; d != nil {
pc.MinHostSSHDur, pc.MaxHostSSHDur, pc.DefaultHostSSHDur, err = durationsToCertificates(d)
if err != nil {
return nil, err
}
}
}
return pc, nil
}
// ProvisionerToCertificates converts the linkedca provisioner type to the certificates provisioner
// interface.
func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, error) {
claims, err := claimsToCertificates(p.Claims)
if err != nil {
return nil, err
}
details := p.Details.GetData()
if details == nil {
return nil, fmt.Errorf("provisioner does not have any details")
}
options := optionsToCertificates(p)
switch d := details.(type) {
case *linkedca.ProvisionerDetails_JWK:
jwk := new(jose.JSONWebKey)
if err := json.Unmarshal(d.JWK.PublicKey, &jwk); err != nil {
return nil, err
}
return &provisioner.JWK{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
Key: jwk,
EncryptedKey: string(d.JWK.EncryptedPrivateKey),
Claims: claims,
Options: options,
}, nil
case *linkedca.ProvisionerDetails_X5C:
var roots []byte
for i, root := range d.X5C.GetRoots() {
if i > 0 {
roots = append(roots, '\n')
}
roots = append(roots, root...)
}
return &provisioner.X5C{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
Roots: roots,
Claims: claims,
Options: options,
}, nil
case *linkedca.ProvisionerDetails_K8SSA:
var publicKeys []byte
for i, k := range d.K8SSA.GetPublicKeys() {
if i > 0 {
publicKeys = append(publicKeys, '\n')
}
publicKeys = append(publicKeys, k...)
}
return &provisioner.K8sSA{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
PubKeys: publicKeys,
Claims: claims,
Options: options,
}, nil
case *linkedca.ProvisionerDetails_SSHPOP:
return &provisioner.SSHPOP{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
Claims: claims,
}, nil
case *linkedca.ProvisionerDetails_ACME:
cfg := d.ACME
return &provisioner.ACME{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
ForceCN: cfg.ForceCn,
Claims: claims,
Options: options,
}, nil
case *linkedca.ProvisionerDetails_OIDC:
cfg := d.OIDC
return &provisioner.OIDC{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
TenantID: cfg.TenantId,
ClientID: cfg.ClientId,
ClientSecret: cfg.ClientSecret,
ConfigurationEndpoint: cfg.ConfigurationEndpoint,
Admins: cfg.Admins,
Domains: cfg.Domains,
Groups: cfg.Groups,
ListenAddress: cfg.ListenAddress,
Claims: claims,
Options: options,
}, nil
case *linkedca.ProvisionerDetails_AWS:
cfg := d.AWS
instanceAge, err := parseInstanceAge(cfg.InstanceAge)
if err != nil {
return nil, err
}
return &provisioner.AWS{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
Accounts: cfg.Accounts,
DisableCustomSANs: cfg.DisableCustomSans,
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
InstanceAge: instanceAge,
Claims: claims,
Options: options,
}, nil
case *linkedca.ProvisionerDetails_GCP:
cfg := d.GCP
instanceAge, err := parseInstanceAge(cfg.InstanceAge)
if err != nil {
return nil, err
}
return &provisioner.GCP{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
ServiceAccounts: cfg.ServiceAccounts,
ProjectIDs: cfg.ProjectIds,
DisableCustomSANs: cfg.DisableCustomSans,
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
InstanceAge: instanceAge,
Claims: claims,
Options: options,
}, nil
case *linkedca.ProvisionerDetails_Azure:
cfg := d.Azure
return &provisioner.Azure{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
TenantID: cfg.TenantId,
ResourceGroups: cfg.ResourceGroups,
Audience: cfg.Audience,
DisableCustomSANs: cfg.DisableCustomSans,
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
Claims: claims,
Options: options,
}, nil
default:
return nil, fmt.Errorf("provisioner %s not implemented", p.Type)
}
}
func parseInstanceAge(age string) (provisioner.Duration, error) {
var instanceAge provisioner.Duration
if age != "" {
iap, err := provisioner.NewDuration(age)
if err != nil {
return instanceAge, err
}
instanceAge = *iap
}
return instanceAge, nil
}