certificates/authority/provisioners.go
Mariano Cano c7f226bcec
Add support for renew when using stepcas
It supports renewing X.509 certificates when an RA is configured with stepcas.
This will only work when the renewal uses a token, and it won't work with mTLS.

The audience cannot be properly verified when an RA is used, to avoid this we
will get from the database if an RA was used to issue the initial certificate
and we will accept the renew token.

Fixes #1021 for stepcas
2022-11-04 16:42:07 -07:00

1340 lines
41 KiB
Go

package authority
import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"github.com/pkg/errors"
"gopkg.in/square/go-jose.v2/jwt"
"go.step.sm/cli-utils/step"
"go.step.sm/cli-utils/ui"
"go.step.sm/crypto/jose"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/policy"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/errs"
)
type raProvisioner interface {
RAInfo() *provisioner.RAInfo
}
type attProvisioner interface {
AttestationData() *provisioner.AttestationData
}
// wrapProvisioner wraps the given provisioner with RA information and
// attestation data.
func wrapProvisioner(p provisioner.Interface, attData *provisioner.AttestationData) *wrappedProvisioner {
var raInfo *provisioner.RAInfo
if rap, ok := p.(raProvisioner); ok {
raInfo = rap.RAInfo()
}
return &wrappedProvisioner{
Interface: p,
attestationData: attData,
raInfo: raInfo,
}
}
// wrapRAProvisioner wraps the given provisioner with RA information.
func wrapRAProvisioner(p provisioner.Interface, raInfo *provisioner.RAInfo) *wrappedProvisioner {
return &wrappedProvisioner{
Interface: p,
raInfo: raInfo,
}
}
// isRAProvisioner returns if the given provisioner is an RA provisioner.
func isRAProvisioner(p provisioner.Interface) bool {
if rap, ok := p.(raProvisioner); ok {
return rap.RAInfo() != nil
}
return false
}
// wrappedProvisioner implements raProvisioner and attProvisioner.
type wrappedProvisioner struct {
provisioner.Interface
attestationData *provisioner.AttestationData
raInfo *provisioner.RAInfo
}
func (p *wrappedProvisioner) AttestationData() *provisioner.AttestationData {
return p.attestationData
}
func (p *wrappedProvisioner) RAInfo() *provisioner.RAInfo {
return p.raInfo
}
// 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()
if p, err := a.unsafeLoadProvisionerFromDatabase(crt); err == nil {
return p, nil
}
return a.unsafeLoadProvisionerFromExtension(crt)
}
func (a *Authority) unsafeLoadProvisionerFromExtension(crt *x509.Certificate) (provisioner.Interface, error) {
p, ok := a.provisioners.LoadByCertificate(crt)
if !ok || p.GetType() == 0 {
return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from certificate")
}
return p, nil
}
func (a *Authority) unsafeLoadProvisionerFromDatabase(crt *x509.Certificate) (provisioner.Interface, error) {
// certificateDataGetter is an interface that can be used to retrieve the
// provisioner from a db or a linked ca.
type certificateDataGetter interface {
GetCertificateData(string) (*db.CertificateData, error)
}
var err error
var data *db.CertificateData
if cdg, ok := a.adminDB.(certificateDataGetter); ok {
data, err = cdg.GetCertificateData(crt.SerialNumber.String())
} else if cdg, ok := a.db.(certificateDataGetter); ok {
data, err = cdg.GetCertificateData(crt.SerialNumber.String())
}
if err == nil && data != nil && data.Provisioner != nil {
if p, ok := a.provisioners.Load(data.Provisioner.ID); ok {
if data.RaInfo != nil {
return wrapRAProvisioner(p, data.RaInfo), nil
}
return p, nil
}
}
return nil, admin.NewError(admin.ErrorNotFoundType, "unable to load provisioner from certificate")
}
// 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 provisioner.Config{}, 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 provisioner.Config{}, err
}
return provisioner.Config{
Claims: claimer.Claims(),
Audiences: a.config.GetAudiences(),
SSHKeys: &provisioner.SSHKeys{
UserKeys: sshKeys.UserKeys,
HostKeys: sshKeys.HostKeys,
},
GetIdentityFunc: a.getIdentityFunc,
AuthorizeRenewFunc: a.authorizeRenewFunc,
AuthorizeSSHRenewFunc: a.authorizeSSHRenewFunc,
WebhookClient: a.webhookClient,
}, nil
}
// StoreProvisioner stores a provisioner 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())
}
provisionerConfig, err := a.generateProvisionerConfig(ctx)
if err != nil {
return admin.WrapErrorISE(err, "error generating provisioner config")
}
if err := a.checkProvisionerPolicy(ctx, prov.Name, prov.Policy); err != nil {
return err
}
if err := certProv.Init(provisionerConfig); err != nil {
return admin.WrapError(admin.ErrorBadRequestType, err, "error validating configuration for provisioner %s", prov.Name)
}
// Store to database -- this will set the ID.
if err := a.adminDB.CreateProvisioner(ctx, prov); err != nil {
return admin.WrapErrorISE(err, "error creating provisioner")
}
// 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")
}
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 := a.checkProvisionerPolicy(ctx, nu.Name, nu.Policy); err != nil {
return err
}
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()
if a.IsAdminAPIEnabled() {
// Validate
// - Check that there will be SUPER_ADMINs that remain after we
// remove this provisioner.
if a.IsAdminAPIEnabled() && 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 provisioner 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
}
// CreateFirstProvisioner creates and stores the first provisioner when using
// admin database provisioner storage.
func CreateFirstProvisioner(ctx context.Context, adminDB admin.DB, password string) (*linkedca.Provisioner, error) {
if password == "" {
pass, err := ui.PromptPasswordGenerate("Please enter the password to encrypt your first provisioner, leave empty and we'll generate one")
if err != nil {
return nil, err
}
password = string(pass)
}
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 := adminDB.CreateProvisioner(ctx, p); err != nil {
return nil, admin.WrapErrorISE(err, "error creating provisioner")
}
return p, nil
}
// ValidateClaims validates the Claims type.
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
}
// ValidateDurations validates the Durations type.
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
}
if pol := p.GetPolicy(); pol != nil {
if x := pol.GetX509(); x != nil {
if allow := x.GetAllow(); allow != nil {
ops.X509.AllowedNames = &policy.X509NameOptions{
DNSDomains: allow.Dns,
IPRanges: allow.Ips,
EmailAddresses: allow.Emails,
URIDomains: allow.Uris,
}
}
if deny := x.GetDeny(); deny != nil {
ops.X509.DeniedNames = &policy.X509NameOptions{
DNSDomains: deny.Dns,
IPRanges: deny.Ips,
EmailAddresses: deny.Emails,
URIDomains: deny.Uris,
}
}
}
if ssh := pol.GetSsh(); ssh != nil {
if host := ssh.GetHost(); host != nil {
ops.SSH.Host = &policy.SSHHostCertificateOptions{}
if allow := host.GetAllow(); allow != nil {
ops.SSH.Host.AllowedNames = &policy.SSHNameOptions{
DNSDomains: allow.Dns,
IPRanges: allow.Ips,
Principals: allow.Principals,
}
}
if deny := host.GetDeny(); deny != nil {
ops.SSH.Host.DeniedNames = &policy.SSHNameOptions{
DNSDomains: deny.Dns,
IPRanges: deny.Ips,
Principals: deny.Principals,
}
}
}
if user := ssh.GetUser(); user != nil {
ops.SSH.User = &policy.SSHUserCertificateOptions{}
if allow := user.GetAllow(); allow != nil {
ops.SSH.User.AllowedNames = &policy.SSHNameOptions{
EmailAddresses: allow.Emails,
Principals: allow.Principals,
}
}
if deny := user.GetDeny(); deny != nil {
ops.SSH.User.DeniedNames = &policy.SSHNameOptions{
EmailAddresses: deny.Emails,
Principals: deny.Principals,
}
}
}
}
}
for _, wh := range p.Webhooks {
whCert := webhookToCertificates(wh)
ops.Webhooks = append(ops.Webhooks, whCert)
}
return ops
}
func webhookToCertificates(wh *linkedca.Webhook) *provisioner.Webhook {
pwh := &provisioner.Webhook{
ID: wh.Id,
Name: wh.Name,
URL: wh.Url,
Kind: wh.Kind.String(),
Secret: wh.Secret,
DisableTLSClientAuth: wh.DisableTlsClientAuth,
CertType: wh.CertType.String(),
}
switch a := wh.GetAuth().(type) {
case *linkedca.Webhook_BearerToken:
pwh.BearerToken = a.BearerToken.BearerToken
case *linkedca.Webhook_BasicAuth:
pwh.BasicAuth.Username = a.BasicAuth.Username
pwh.BasicAuth.Password = a.BasicAuth.Password
}
return pwh
}
func provisionerWebhookToLinkedca(pwh *provisioner.Webhook) *linkedca.Webhook {
lwh := &linkedca.Webhook{
Id: pwh.ID,
Name: pwh.Name,
Url: pwh.URL,
Kind: linkedca.Webhook_Kind(linkedca.Webhook_Kind_value[pwh.Kind]),
Secret: pwh.Secret,
DisableTlsClientAuth: pwh.DisableTLSClientAuth,
CertType: linkedca.Webhook_CertType(linkedca.Webhook_CertType_value[pwh.CertType]),
}
if pwh.BearerToken != "" {
lwh.Auth = &linkedca.Webhook_BearerToken{
BearerToken: &linkedca.BearerToken{
BearerToken: pwh.BearerToken,
},
}
} else if pwh.BasicAuth.Username != "" || pwh.BasicAuth.Password != "" {
lwh.Auth = &linkedca.Webhook_BasicAuth{
BasicAuth: &linkedca.BasicAuth{
Username: pwh.BasicAuth.Username,
Password: pwh.BasicAuth.Password,
},
}
}
return lwh
}
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
}
func durationsToLinkedca(d *provisioner.Duration) string {
if d == nil {
return ""
}
return d.Duration.String()
}
// claimsToCertificates converts the linkedca provisioner claims type to the
// certifictes claims type.
func claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) {
if c == nil {
//nolint:nilnil // nil claims do not pose an issue.
return nil, nil
}
pc := &provisioner.Claims{
DisableRenewal: &c.DisableRenewal,
AllowRenewalAfterExpiry: &c.AllowRenewalAfterExpiry,
}
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
}
func claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims {
if c == nil {
return nil
}
disableRenewal := config.DefaultDisableRenewal
allowRenewalAfterExpiry := config.DefaultAllowRenewalAfterExpiry
if c.DisableRenewal != nil {
disableRenewal = *c.DisableRenewal
}
if c.AllowRenewalAfterExpiry != nil {
allowRenewalAfterExpiry = *c.AllowRenewalAfterExpiry
}
lc := &linkedca.Claims{
DisableRenewal: disableRenewal,
AllowRenewalAfterExpiry: allowRenewalAfterExpiry,
}
if c.DefaultTLSDur != nil || c.MinTLSDur != nil || c.MaxTLSDur != nil {
lc.X509 = &linkedca.X509Claims{
Enabled: true,
Durations: &linkedca.Durations{
Default: durationsToLinkedca(c.DefaultTLSDur),
Min: durationsToLinkedca(c.MinTLSDur),
Max: durationsToLinkedca(c.MaxTLSDur),
},
}
}
if c.EnableSSHCA != nil && *c.EnableSSHCA {
lc.Ssh = &linkedca.SSHClaims{
Enabled: true,
}
if c.DefaultUserSSHDur != nil || c.MinUserSSHDur != nil || c.MaxUserSSHDur != nil {
lc.Ssh.UserDurations = &linkedca.Durations{
Default: durationsToLinkedca(c.DefaultUserSSHDur),
Min: durationsToLinkedca(c.MinUserSSHDur),
Max: durationsToLinkedca(c.MaxUserSSHDur),
}
}
if c.DefaultHostSSHDur != nil || c.MinHostSSHDur != nil || c.MaxHostSSHDur != nil {
lc.Ssh.HostDurations = &linkedca.Durations{
Default: durationsToLinkedca(c.DefaultHostSSHDur),
Min: durationsToLinkedca(c.MinHostSSHDur),
Max: durationsToLinkedca(c.MaxHostSSHDur),
}
}
}
return lc
}
func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *linkedca.Template, []*linkedca.Webhook, error) {
var err error
var x509Template, sshTemplate *linkedca.Template
if p == nil {
return nil, nil, nil, nil
}
if p.X509 != nil && p.X509.HasTemplate() {
x509Template = &linkedca.Template{
Template: nil,
Data: nil,
}
if p.X509.Template != "" {
x509Template.Template = []byte(p.SSH.Template)
} else if p.X509.TemplateFile != "" {
filename := step.Abs(p.X509.TemplateFile)
if x509Template.Template, err = os.ReadFile(filename); err != nil {
return nil, nil, nil, errors.Wrap(err, "error reading x509 template")
}
}
}
if p.SSH != nil && p.SSH.HasTemplate() {
sshTemplate = &linkedca.Template{
Template: nil,
Data: nil,
}
if p.SSH.Template != "" {
sshTemplate.Template = []byte(p.SSH.Template)
} else if p.SSH.TemplateFile != "" {
filename := step.Abs(p.SSH.TemplateFile)
if sshTemplate.Template, err = os.ReadFile(filename); err != nil {
return nil, nil, nil, errors.Wrap(err, "error reading ssh template")
}
}
}
var webhooks []*linkedca.Webhook
for _, pwh := range p.Webhooks {
webhooks = append(webhooks, provisionerWebhookToLinkedca(pwh))
}
return x509Template, sshTemplate, webhooks, nil
}
func provisionerPEMToLinkedca(b []byte) [][]byte {
var roots [][]byte
var block *pem.Block
for {
if block, b = pem.Decode(b); block == nil {
break
}
roots = append(roots, pem.EncodeToMemory(block))
}
return roots
}
func provisionerPEMToCertificates(bs [][]byte) []byte {
var roots []byte
for i, root := range bs {
if i > 0 && !bytes.HasSuffix(root, []byte{'\n'}) {
roots = append(roots, '\n')
}
roots = append(roots, root...)
}
return roots
}
// 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, errors.New("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, errors.Wrap(err, "error unmarshaling public key")
}
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,
RequireEAB: cfg.RequireEab,
Challenges: challengesToCertificates(cfg.Challenges),
AttestationFormats: attestationFormatsToCertificates(cfg.AttestationFormats),
AttestationRoots: provisionerPEMToCertificates(cfg.AttestationRoots),
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,
SubscriptionIDs: cfg.SubscriptionIds,
ObjectIDs: cfg.ObjectIds,
Audience: cfg.Audience,
DisableCustomSANs: cfg.DisableCustomSans,
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
Claims: claims,
Options: options,
}, nil
case *linkedca.ProvisionerDetails_SCEP:
cfg := d.SCEP
return &provisioner.SCEP{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
ForceCN: cfg.ForceCn,
ChallengePassword: cfg.Challenge,
Capabilities: cfg.Capabilities,
IncludeRoot: cfg.IncludeRoot,
MinimumPublicKeyLength: int(cfg.MinimumPublicKeyLength),
EncryptionAlgorithmIdentifier: int(cfg.EncryptionAlgorithmIdentifier),
Claims: claims,
Options: options,
}, nil
case *linkedca.ProvisionerDetails_Nebula:
var roots []byte
for i, root := range d.Nebula.GetRoots() {
if i > 0 && !bytes.HasSuffix(root, []byte{'\n'}) {
roots = append(roots, '\n')
}
roots = append(roots, root...)
}
return &provisioner.Nebula{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
Roots: roots,
Claims: claims,
Options: options,
}, nil
default:
return nil, fmt.Errorf("provisioner %s not implemented", p.Type)
}
}
// ProvisionerToLinkedca converts a provisioner.Interface to a
// linkedca.Provisioner type.
func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, error) {
switch p := p.(type) {
case *provisioner.JWK:
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
publicKey, err := json.Marshal(p.Key)
if err != nil {
return nil, errors.Wrap(err, "error marshaling key")
}
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_JWK,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_JWK{
JWK: &linkedca.JWKProvisioner{
PublicKey: publicKey,
EncryptedPrivateKey: []byte(p.EncryptedKey),
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
Webhooks: webhooks,
}, nil
case *provisioner.OIDC:
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_OIDC,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_OIDC{
OIDC: &linkedca.OIDCProvisioner{
ClientId: p.ClientID,
ClientSecret: p.ClientSecret,
ConfigurationEndpoint: p.ConfigurationEndpoint,
Admins: p.Admins,
Domains: p.Domains,
Groups: p.Groups,
ListenAddress: p.ListenAddress,
TenantId: p.TenantID,
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
Webhooks: webhooks,
}, nil
case *provisioner.GCP:
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_GCP,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_GCP{
GCP: &linkedca.GCPProvisioner{
ServiceAccounts: p.ServiceAccounts,
ProjectIds: p.ProjectIDs,
DisableCustomSans: p.DisableCustomSANs,
DisableTrustOnFirstUse: p.DisableTrustOnFirstUse,
InstanceAge: p.InstanceAge.String(),
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
Webhooks: webhooks,
}, nil
case *provisioner.AWS:
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_AWS,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_AWS{
AWS: &linkedca.AWSProvisioner{
Accounts: p.Accounts,
DisableCustomSans: p.DisableCustomSANs,
DisableTrustOnFirstUse: p.DisableTrustOnFirstUse,
InstanceAge: p.InstanceAge.String(),
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
Webhooks: webhooks,
}, nil
case *provisioner.Azure:
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_AZURE,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_Azure{
Azure: &linkedca.AzureProvisioner{
TenantId: p.TenantID,
ResourceGroups: p.ResourceGroups,
SubscriptionIds: p.SubscriptionIDs,
ObjectIds: p.ObjectIDs,
Audience: p.Audience,
DisableCustomSans: p.DisableCustomSANs,
DisableTrustOnFirstUse: p.DisableTrustOnFirstUse,
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
Webhooks: webhooks,
}, nil
case *provisioner.ACME:
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_ACME,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_ACME{
ACME: &linkedca.ACMEProvisioner{
ForceCn: p.ForceCN,
Challenges: challengesToLinkedca(p.Challenges),
AttestationFormats: attestationFormatsToLinkedca(p.AttestationFormats),
AttestationRoots: provisionerPEMToLinkedca(p.AttestationRoots),
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
Webhooks: webhooks,
}, nil
case *provisioner.X5C:
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_X5C,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_X5C{
X5C: &linkedca.X5CProvisioner{
Roots: provisionerPEMToLinkedca(p.Roots),
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
Webhooks: webhooks,
}, nil
case *provisioner.K8sSA:
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_K8SSA,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_K8SSA{
K8SSA: &linkedca.K8SSAProvisioner{
PublicKeys: provisionerPEMToLinkedca(p.PubKeys),
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
Webhooks: webhooks,
}, nil
case *provisioner.SSHPOP:
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_SSHPOP,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_SSHPOP{
SSHPOP: &linkedca.SSHPOPProvisioner{},
},
},
Claims: claimsToLinkedca(p.Claims),
}, nil
case *provisioner.SCEP:
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_SCEP,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_SCEP{
SCEP: &linkedca.SCEPProvisioner{
ForceCn: p.ForceCN,
Challenge: p.GetChallengePassword(),
Capabilities: p.Capabilities,
MinimumPublicKeyLength: int32(p.MinimumPublicKeyLength),
IncludeRoot: p.IncludeRoot,
EncryptionAlgorithmIdentifier: int32(p.EncryptionAlgorithmIdentifier),
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
Webhooks: webhooks,
}, nil
case *provisioner.Nebula:
x509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Id: p.ID,
Type: linkedca.Provisioner_NEBULA,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_Nebula{
Nebula: &linkedca.NebulaProvisioner{
Roots: provisionerPEMToLinkedca(p.Roots),
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
Webhooks: webhooks,
}, nil
default:
return nil, fmt.Errorf("provisioner %s not implemented", p.GetType())
}
}
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
}
// challengesToCertificates converts linkedca challenges to provisioner ones
// skipping the unknown ones.
func challengesToCertificates(challenges []linkedca.ACMEProvisioner_ChallengeType) []provisioner.ACMEChallenge {
ret := make([]provisioner.ACMEChallenge, 0, len(challenges))
for _, ch := range challenges {
switch ch {
case linkedca.ACMEProvisioner_HTTP_01:
ret = append(ret, provisioner.HTTP_01)
case linkedca.ACMEProvisioner_DNS_01:
ret = append(ret, provisioner.DNS_01)
case linkedca.ACMEProvisioner_TLS_ALPN_01:
ret = append(ret, provisioner.TLS_ALPN_01)
case linkedca.ACMEProvisioner_DEVICE_ATTEST_01:
ret = append(ret, provisioner.DEVICE_ATTEST_01)
}
}
return ret
}
// challengesToLinkedca converts provisioner challenges to linkedca ones
// skipping the unknown ones.
func challengesToLinkedca(challenges []provisioner.ACMEChallenge) []linkedca.ACMEProvisioner_ChallengeType {
ret := make([]linkedca.ACMEProvisioner_ChallengeType, 0, len(challenges))
for _, ch := range challenges {
switch provisioner.ACMEChallenge(ch.String()) {
case provisioner.HTTP_01:
ret = append(ret, linkedca.ACMEProvisioner_HTTP_01)
case provisioner.DNS_01:
ret = append(ret, linkedca.ACMEProvisioner_DNS_01)
case provisioner.TLS_ALPN_01:
ret = append(ret, linkedca.ACMEProvisioner_TLS_ALPN_01)
case provisioner.DEVICE_ATTEST_01:
ret = append(ret, linkedca.ACMEProvisioner_DEVICE_ATTEST_01)
}
}
return ret
}
// attestationFormatsToCertificates converts linkedca attestation formats to
// provisioner ones skipping the unknown ones.
func attestationFormatsToCertificates(formats []linkedca.ACMEProvisioner_AttestationFormatType) []provisioner.ACMEAttestationFormat {
ret := make([]provisioner.ACMEAttestationFormat, 0, len(formats))
for _, f := range formats {
switch f {
case linkedca.ACMEProvisioner_APPLE:
ret = append(ret, provisioner.APPLE)
case linkedca.ACMEProvisioner_STEP:
ret = append(ret, provisioner.STEP)
case linkedca.ACMEProvisioner_TPM:
ret = append(ret, provisioner.TPM)
}
}
return ret
}
// attestationFormatsToLinkedca converts provisioner attestation formats to
// linkedca ones skipping the unknown ones.
func attestationFormatsToLinkedca(formats []provisioner.ACMEAttestationFormat) []linkedca.ACMEProvisioner_AttestationFormatType {
ret := make([]linkedca.ACMEProvisioner_AttestationFormatType, 0, len(formats))
for _, f := range formats {
switch provisioner.ACMEAttestationFormat(f.String()) {
case provisioner.APPLE:
ret = append(ret, linkedca.ACMEProvisioner_APPLE)
case provisioner.STEP:
ret = append(ret, linkedca.ACMEProvisioner_STEP)
case provisioner.TPM:
ret = append(ret, linkedca.ACMEProvisioner_TPM)
}
}
return ret
}