diff --git a/authority/export.go b/authority/export.go new file mode 100644 index 00000000..e8f7298e --- /dev/null +++ b/authority/export.go @@ -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 +} diff --git a/authority/provisioners.go b/authority/provisioners.go index d2581e76..3e2d1276 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -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 != "" { diff --git a/commands/export.go b/commands/export.go new file mode 100644 index 00000000..bb11fea7 --- /dev/null +++ b/commands/export.go @@ -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** ", + Action: exportAction, + Description: `**step-ca export** exports the current configuration of step-ca. + +## POSITIONAL ARGUMENTS + + +: 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 +} diff --git a/go.mod b/go.mod index 2c7a0687..98e7dbdb 100644 --- a/go.mod +++ b/go.mod @@ -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