mirror of
https://github.com/smallstep/certificates.git
synced 2024-10-31 03:20:16 +00:00
602 lines
18 KiB
Go
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
|
|
}
|