Create a way to export ca configurations.

This commit is contained in:
Mariano Cano 2021-07-26 19:01:56 -07:00
parent d0c1530f89
commit dc1ec18b52
4 changed files with 445 additions and 1 deletions

39
authority/export.go Normal file
View file

@ -0,0 +1,39 @@
package authority
import "go.step.sm/linkedca"
func (a *Authority) Export() (*linkedca.Configuration, error) {
var admins []*linkedca.Admin
var provisioners []*linkedca.Provisioner
for {
list, cursor := a.admins.Find("", 100)
admins = append(admins, list...)
if cursor == "" {
break
}
}
for {
list, cursor := a.provisioners.Find("", 100)
for _, p := range list {
lp, err := ProvisionerToLinkedca(p)
if err != nil {
return nil, err
}
provisioners = append(provisioners, lp)
}
if cursor == "" {
break
}
}
// Global claims for all provisioners.
claims := claimsToLinkedca(a.config.AuthorityConfig.Claims)
return &linkedca.Configuration{
Admins: admins,
Provisioners: provisioners,
Claims: claims,
}, nil
}

View file

@ -4,12 +4,16 @@ import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
step "go.step.sm/cli-utils/config"
"go.step.sm/crypto/jose"
"go.step.sm/linkedca"
"gopkg.in/square/go-jose.v2/jwt"
@ -398,6 +402,13 @@ func durationsToCertificates(d *linkedca.Durations) (min, max, def *provisioner.
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) {
@ -438,6 +449,109 @@ func claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) {
return pc, nil
}
func claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims {
if c == nil {
return nil
}
disableRenewal := config.DefaultDisableRenewal
if c.DisableRenewal != nil {
disableRenewal = *c.DisableRenewal
}
lc := &linkedca.Claims{
DisableRenewal: disableRenewal,
}
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, error) {
var err error
var x509Template, sshTemplate *linkedca.Template
if p == nil {
return 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.StepAbs(p.X509.TemplateFile)
if x509Template.Template, err = ioutil.ReadFile(filename); err != nil {
return 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.StepAbs(p.SSH.TemplateFile)
if sshTemplate.Template, err = ioutil.ReadFile(filename); err != nil {
return nil, nil, errors.Wrap(err, "error reading ssh template")
}
}
}
return x509Template, sshTemplate, 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
}
// ProvisionerToCertificates converts the linkedca provisioner type to the certificates provisioner
// interface.
func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, error) {
@ -588,6 +702,223 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
}
}
// 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, 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{
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,
}, nil
case *provisioner.OIDC:
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
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,
}, nil
case *provisioner.GCP:
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
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,
}, nil
case *provisioner.AWS:
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
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,
}, nil
case *provisioner.Azure:
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Type: linkedca.Provisioner_AZURE,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_Azure{
Azure: &linkedca.AzureProvisioner{
TenantId: p.TenantID,
ResourceGroups: p.ResourceGroups,
Audience: p.Audience,
DisableCustomSans: p.DisableCustomSANs,
DisableTrustOnFirstUse: p.DisableTrustOnFirstUse,
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
}, nil
case *provisioner.ACME:
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
Type: linkedca.Provisioner_ACME,
Name: p.GetName(),
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_ACME{
ACME: &linkedca.ACMEProvisioner{
ForceCn: p.ForceCN,
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
}, nil
case *provisioner.X5C:
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
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,
}, nil
case *provisioner.K8sSA:
x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
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,
}, nil
case *provisioner.SSHPOP:
return &linkedca.Provisioner{
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, err := provisionerOptionsToLinkedca(p.Options)
if err != nil {
return nil, err
}
return &linkedca.Provisioner{
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),
},
},
},
Claims: claimsToLinkedca(p.Claims),
X509Template: x509Template,
SshTemplate: sshTemplate,
}, 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 != "" {

74
commands/export.go Normal file
View file

@ -0,0 +1,74 @@
package commands
import (
"bytes"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/config"
"github.com/urfave/cli"
"google.golang.org/protobuf/encoding/protojson"
"go.step.sm/cli-utils/command"
"go.step.sm/cli-utils/errs"
)
func init() {
command.Register(cli.Command{
Name: "export",
Usage: "export the current configuration of step-ca",
UsageText: "**step-ca export** <config>",
Action: exportAction,
Description: `**step-ca export** exports the current configuration of step-ca.
## POSITIONAL ARGUMENTS
<config>
: The ca.json that contains the step-ca configuration.
## EXAMPLES
Export the current configuration:
'''
$ step-ca export $(step path)/config/ca.json
'''`,
})
}
func exportAction(ctx *cli.Context) error {
if err := errs.NumberOfArguments(ctx, 1); err != nil {
return err
}
configFile := ctx.Args().Get(0)
config, err := config.LoadConfiguration(configFile)
if err != nil {
return err
}
auth, err := authority.New(config)
if err != nil {
return err
}
export, err := auth.Export()
if err != nil {
return err
}
b, err := protojson.Marshal(export)
if err != nil {
return errors.Wrap(err, "error marshaling export")
}
var buf bytes.Buffer
if err := json.Indent(&buf, b, "", "\t"); err != nil {
return errors.Wrap(err, "error indenting export")
}
fmt.Println(buf.String())
return nil
}

2
go.mod
View file

@ -42,6 +42,6 @@ require (
// replace github.com/smallstep/nosql => ../nosql
// replace go.step.sm/crypto => ../crypto
// replace go.step.sm/cli-utils => ../cli-utils
// replace go.step.sm/linkedca => ../linkedca
replace go.step.sm/linkedca => ../linkedca
replace go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 => github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568