certificates/authority/provisioners.go

1347 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,
TermsOfService: cfg.TermsOfService,
Website: cfg.Website,
CaaIdentities: cfg.CaaIdentities,
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,
TermsOfService: p.TermsOfService,
Website: p.Website,
CaaIdentities: p.CaaIdentities,
RequireEab: p.RequireEAB,
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
}