Make pki initialization more flexible.

This commit is contained in:
Mariano Cano 2021-08-02 16:07:30 -07:00
parent 384be6e205
commit 721459210e
2 changed files with 243 additions and 66 deletions

View file

@ -19,12 +19,16 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/admin"
admindb "github.com/smallstep/certificates/authority/admin/db/nosql"
authconfig "github.com/smallstep/certificates/authority/config" authconfig "github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/cas" "github.com/smallstep/certificates/cas"
"github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db" "github.com/smallstep/certificates/db"
"github.com/smallstep/nosql"
"go.step.sm/cli-utils/config" "go.step.sm/cli-utils/config"
"go.step.sm/cli-utils/errs" "go.step.sm/cli-utils/errs"
"go.step.sm/cli-utils/fileutil" "go.step.sm/cli-utils/fileutil"
@ -32,9 +36,26 @@ import (
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil" "go.step.sm/crypto/keyutil"
"go.step.sm/crypto/pemutil" "go.step.sm/crypto/pemutil"
"go.step.sm/linkedca"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
// DeploymentType defines witch type of deployment a user is initializing
type DeploymentType int
const (
// StandaloneDeployment is a deployment where all the components like keys,
// provisioners, admins, certificates and others are managed by the user.
StandaloneDeployment DeploymentType = iota
// LinkedDeployment is a deployment where the keys are managed by the user,
// but provisioners, admins and the record of certificates are managed in
// the cloud.
LinkedDeployment
// HostedDeployment is a deployment where all the components are managed in
// the cloud by smallstep.com/certificate-manager.
HostedDeployment
)
const ( const (
// ConfigPath is the directory name under the step path where the configuration // ConfigPath is the directory name under the step path where the configuration
// files will be stored. // files will be stored.
@ -134,9 +155,88 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) {
return resp.Key, nil return resp.Key, nil
} }
type options struct {
address string
caURL string
dnsNames []string
provisioner string
enableACME bool
enableSSH bool
enableAdmin bool
noDB bool
deploymentType DeploymentType
}
// PKIOption is the type of a configuration option on the pki constructor.
type PKIOption func(o *options)
// WithAddress sets the listen address of step-ca.
func WithAddress(s string) PKIOption {
return func(o *options) {
o.address = s
}
}
// WithCaUrl sets the default ca-url of step-ca.
func WithCaUrl(s string) PKIOption {
return func(o *options) {
o.caURL = s
}
}
// WithDNSNames sets the SANs of step-ca.
func WithDNSNames(s []string) PKIOption {
return func(o *options) {
o.dnsNames = s
}
}
// WithProvisioner defines the name of the default provisioner.
func WithProvisioner(s string) PKIOption {
return func(o *options) {
o.provisioner = s
}
}
// WithACME enables acme provisioner in step-ca.
func WithACME() PKIOption {
return func(o *options) {
o.enableACME = true
}
}
// WithSSH enables ssh in step-ca.
func WithSSH() PKIOption {
return func(o *options) {
o.enableSSH = true
}
}
// WithAdmin enables the admin api in step-ca.
func WithAdmin() PKIOption {
return func(o *options) {
o.enableAdmin = true
}
}
// WithNoDB disables the db in step-ca.
func WithNoDB() PKIOption {
return func(o *options) {
o.noDB = true
}
}
// WithDeploymentType defines the deployment type of step-ca.
func WithDeploymentType(dt DeploymentType) PKIOption {
return func(o *options) {
o.deploymentType = dt
}
}
// PKI represents the Public Key Infrastructure used by a certificate authority. // PKI represents the Public Key Infrastructure used by a certificate authority.
type PKI struct { type PKI struct {
casOptions apiv1.Options casOptions apiv1.Options
caService apiv1.CertificateAuthorityService
caCreator apiv1.CertificateAuthorityCreator caCreator apiv1.CertificateAuthorityCreator
root, rootKey, rootFingerprint string root, rootKey, rootFingerprint string
intermediate, intermediateKey string intermediate, intermediateKey string
@ -145,20 +245,25 @@ type PKI struct {
config, defaults string config, defaults string
ottPublicKey *jose.JSONWebKey ottPublicKey *jose.JSONWebKey
ottPrivateKey *jose.JSONWebEncryption ottPrivateKey *jose.JSONWebEncryption
provisioner string options *options
address string
dnsNames []string
caURL string
enableSSH bool
} }
// New creates a new PKI configuration. // New creates a new PKI configuration.
func New(opts apiv1.Options) (*PKI, error) { func New(o apiv1.Options, opts ...PKIOption) (*PKI, error) {
caCreator, err := cas.NewCreator(context.Background(), opts) caService, err := cas.New(context.Background(), o)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var caCreator apiv1.CertificateAuthorityCreator
if o.IsCreator {
creator, ok := caService.(apiv1.CertificateAuthorityCreator)
if !ok {
return nil, errors.Errorf("cas type '%s' does not implements CertificateAuthorityCreator", o.Type)
}
caCreator = creator
}
public := GetPublicPath() public := GetPublicPath()
private := GetSecretsPath() private := GetSecretsPath()
config := GetConfigPath() config := GetConfigPath()
@ -180,12 +285,19 @@ func New(opts apiv1.Options) (*PKI, error) {
} }
p := &PKI{ p := &PKI{
casOptions: opts, casOptions: o,
caCreator: caCreator, caCreator: caCreator,
provisioner: "step-cli", caService: caService,
address: "127.0.0.1:9000", options: &options{
dnsNames: []string{"127.0.0.1"}, provisioner: "step-cli",
address: "127.0.0.1:9000",
dnsNames: []string{"127.0.0.1"},
},
} }
for _, fn := range opts {
fn(p.options)
}
if p.root, err = getPath(public, "root_ca.crt"); err != nil { if p.root, err = getPath(public, "root_ca.crt"); err != nil {
return nil, err return nil, err
} }
@ -233,23 +345,31 @@ func (p *PKI) GetRootFingerprint() string {
} }
// SetProvisioner sets the provisioner name of the OTT keys. // SetProvisioner sets the provisioner name of the OTT keys.
//
// Deprecated: this method is deprecated in favor of WithProvisioner.
func (p *PKI) SetProvisioner(s string) { func (p *PKI) SetProvisioner(s string) {
p.provisioner = s p.options.provisioner = s
} }
// SetAddress sets the listening address of the CA. // SetAddress sets the listening address of the CA.
//
// Deprecated: this method is deprecated in favor of WithAddress.
func (p *PKI) SetAddress(s string) { func (p *PKI) SetAddress(s string) {
p.address = s p.options.address = s
} }
// SetDNSNames sets the dns names of the CA. // SetDNSNames sets the dns names of the CA.
//
// Deprecated: this method is deprecated in favor of WithDNSNames.
func (p *PKI) SetDNSNames(s []string) { func (p *PKI) SetDNSNames(s []string) {
p.dnsNames = s p.options.dnsNames = s
} }
// SetCAURL sets the ca-url to use in the defaults.json. // SetCAURL sets the ca-url to use in the defaults.json.
//
// Deprecated: this method is deprecated in favor of WithCaUrl.
func (p *PKI) SetCAURL(s string) { func (p *PKI) SetCAURL(s string) {
p.caURL = s p.options.caURL = s
} }
// GenerateKeyPairs generates the key pairs used by the certificate authority. // GenerateKeyPairs generates the key pairs used by the certificate authority.
@ -379,7 +499,7 @@ func (p *PKI) CreateCertificateAuthorityResponse(cert *x509.Certificate, key cry
// GetCertificateAuthority attempts to load the certificate authority from the // GetCertificateAuthority attempts to load the certificate authority from the
// RA. // RA.
func (p *PKI) GetCertificateAuthority() error { func (p *PKI) GetCertificateAuthority() error {
srv, ok := p.caCreator.(apiv1.CertificateAuthorityGetter) srv, ok := p.caService.(apiv1.CertificateAuthorityGetter)
if !ok { if !ok {
return nil return nil
} }
@ -427,7 +547,7 @@ func (p *PKI) GenerateSSHSigningKeys(password []byte) error {
return err return err
} }
} }
p.enableSSH = true p.options.enableSSH = true
return nil return nil
} }
@ -440,7 +560,8 @@ func (p *PKI) askFeedback() {
ui.Println(" phone home. But your feedback is extremely valuable. Any information you") ui.Println(" phone home. But your feedback is extremely valuable. Any information you")
ui.Println(" can provide regarding how youre using `step` helps. Please send us a") ui.Println(" can provide regarding how youre using `step` helps. Please send us a")
ui.Println(" sentence or two, good or bad: \033[1mfeedback@smallstep.com\033[0m or join") ui.Println(" sentence or two, good or bad: \033[1mfeedback@smallstep.com\033[0m or join")
ui.Println(" \033[1mhttps://github.com/smallstep/certificates/discussions\033[0m.") ui.Println(" \033[1mhttps://github.com/smallstep/certificates/discussions\033[0m and our Discord")
ui.Println(" \033[1mhttps://bit.ly/step-discord\033[0m.")
} }
// TellPKI outputs the locations of public and private keys generated // TellPKI outputs the locations of public and private keys generated
@ -465,7 +586,7 @@ func (p *PKI) tellPKI() {
} else { } else {
ui.Printf(`{{ "%s" | red }} {{ "Root certificate:" | bold }} failed to retrieve it from RA`+"\n", ui.IconBad) ui.Printf(`{{ "%s" | red }} {{ "Root certificate:" | bold }} failed to retrieve it from RA`+"\n", ui.IconBad)
} }
if p.enableSSH { if p.options.enableSSH {
ui.PrintSelected("SSH user root certificate", p.sshUserPubKey) ui.PrintSelected("SSH user root certificate", p.sshUserPubKey)
ui.PrintSelected("SSH user root private key", p.sshUserKey) ui.PrintSelected("SSH user root private key", p.sshUserKey)
ui.PrintSelected("SSH host root certificate", p.sshHostPubKey) ui.PrintSelected("SSH host root certificate", p.sshHostPubKey)
@ -485,6 +606,8 @@ type Option func(c *authconfig.Config) error
// WithDefaultDB is a configuration modifier that adds a default DB stanza to // WithDefaultDB is a configuration modifier that adds a default DB stanza to
// the authority config. // the authority config.
//
// Deprecated: this method is deprecated because this is the default behavior.
func WithDefaultDB() Option { func WithDefaultDB() Option {
return func(c *authconfig.Config) error { return func(c *authconfig.Config) error {
c.DB = &db.Config{ c.DB = &db.Config{
@ -497,6 +620,8 @@ func WithDefaultDB() Option {
// WithoutDB is a configuration modifier that adds a default DB stanza to // WithoutDB is a configuration modifier that adds a default DB stanza to
// the authority config. // the authority config.
//
// De[recated: this method is deprecated in favor or WithNoDB.
func WithoutDB() Option { func WithoutDB() Option {
return func(c *authconfig.Config) error { return func(c *authconfig.Config) error {
c.DB = nil c.DB = nil
@ -506,18 +631,6 @@ func WithoutDB() Option {
// GenerateConfig returns the step certificates configuration. // GenerateConfig returns the step certificates configuration.
func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) { func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) {
key, err := p.ottPrivateKey.CompactSerialize()
if err != nil {
return nil, errors.Wrap(err, "error serializing private key")
}
prov := &provisioner.JWK{
Name: p.provisioner,
Type: "JWK",
Key: p.ottPublicKey,
EncryptedKey: key,
}
var authorityOptions *apiv1.Options var authorityOptions *apiv1.Options
if !p.casOptions.Is(apiv1.SoftCAS) { if !p.casOptions.Is(apiv1.SoftCAS) {
authorityOptions = &p.casOptions authorityOptions = &p.casOptions
@ -528,8 +641,8 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) {
FederatedRoots: []string{}, FederatedRoots: []string{},
IntermediateCert: p.intermediate, IntermediateCert: p.intermediate,
IntermediateKey: p.intermediateKey, IntermediateKey: p.intermediateKey,
Address: p.address, Address: p.options.address,
DNSNames: p.dnsNames, DNSNames: p.options.dnsNames,
Logger: []byte(`{"format": "text"}`), Logger: []byte(`{"format": "text"}`),
DB: &db.Config{ DB: &db.Config{
Type: "badger", Type: "badger",
@ -538,44 +651,109 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) {
AuthorityConfig: &authconfig.AuthConfig{ AuthorityConfig: &authconfig.AuthConfig{
Options: authorityOptions, Options: authorityOptions,
DisableIssuedAtCheck: false, DisableIssuedAtCheck: false,
Provisioners: provisioner.List{prov}, EnableAdmin: false,
},
TLS: &authconfig.TLSOptions{
MinVersion: authconfig.DefaultTLSMinVersion,
MaxVersion: authconfig.DefaultTLSMaxVersion,
Renegotiation: authconfig.DefaultTLSRenegotiation,
CipherSuites: authconfig.DefaultTLSCipherSuites,
}, },
TLS: &authconfig.DefaultTLSOptions,
Templates: p.getTemplates(), Templates: p.getTemplates(),
} }
if p.enableSSH {
enableSSHCA := true // On standalone deployments add the provisioners to either the ca.json or
config.SSH = &authconfig.SSHConfig{ // the database.
HostKey: p.sshHostKey, var provisioners []provisioner.Interface
UserKey: p.sshUserKey, if p.options.deploymentType == StandaloneDeployment {
key, err := p.ottPrivateKey.CompactSerialize()
if err != nil {
return nil, errors.Wrap(err, "error serializing private key")
} }
// Enable SSH authorization for default JWK provisioner
prov.Claims = &provisioner.Claims{ prov := &provisioner.JWK{
EnableSSHCA: &enableSSHCA, Name: p.options.provisioner,
Type: "JWK",
Key: p.ottPublicKey,
EncryptedKey: key,
} }
// Add default SSHPOP provisioner provisioners = append(provisioners, prov)
sshpop := &provisioner.SSHPOP{
Type: "SSHPOP", // Add default ACME provisioner if enabled
Name: "sshpop", if p.options.enableACME {
Claims: &provisioner.Claims{ provisioners = append(provisioners, &provisioner.ACME{
Type: "ACME",
Name: "acme",
})
}
if p.options.enableSSH {
enableSSHCA := true
config.SSH = &authconfig.SSHConfig{
HostKey: p.sshHostKey,
UserKey: p.sshUserKey,
}
// Enable SSH authorization for default JWK provisioner
prov.Claims = &provisioner.Claims{
EnableSSHCA: &enableSSHCA, EnableSSHCA: &enableSSHCA,
}, }
// Add default SSHPOP provisioner
provisioners = append(provisioners, &provisioner.SSHPOP{
Type: "SSHPOP",
Name: "sshpop",
Claims: &provisioner.Claims{
EnableSSHCA: &enableSSHCA,
},
})
} }
config.AuthorityConfig.Provisioners = append(config.AuthorityConfig.Provisioners, sshpop)
} }
// Apply configuration modifiers // Apply configuration modifiers
for _, o := range opt { for _, o := range opt {
if err = o(config); err != nil { if err := o(config); err != nil {
return nil, err return nil, err
} }
} }
// Set authority.enableAdmin to true
if p.options.enableAdmin {
config.AuthorityConfig.EnableAdmin = true
}
if p.options.deploymentType == StandaloneDeployment {
if !config.AuthorityConfig.EnableAdmin {
config.AuthorityConfig.Provisioners = provisioners
} else {
db, err := db.New(config.DB)
if err != nil {
return nil, err
}
adminDB, err := admindb.New(db.(nosql.DB), admin.DefaultAuthorityID)
if err != nil {
return nil, err
}
// Add all the provisioners to the db.
var adminID string
for i, p := range provisioners {
prov, err := authority.ProvisionerToLinkedca(p)
if err != nil {
return nil, err
}
if err := adminDB.CreateProvisioner(context.Background(), prov); err != nil {
return nil, err
}
if i == 0 {
adminID = prov.Id
}
}
// Add the first provisioner as an admin.
if err := adminDB.CreateAdmin(context.Background(), &linkedca.Admin{
AuthorityId: admin.DefaultAuthorityID,
Subject: "step",
Type: linkedca.Admin_SUPER_ADMIN,
ProvisionerId: adminID,
}); err != nil {
return nil, err
}
}
}
return config, nil return config, nil
} }
@ -599,17 +777,16 @@ func (p *PKI) Save(opt ...Option) error {
} }
// Generate the CA URL. // Generate the CA URL.
if p.caURL == "" { if p.options.caURL == "" {
p.caURL = p.dnsNames[0] p.options.caURL = p.options.dnsNames[0]
var port string _, port, err := net.SplitHostPort(p.options.address)
_, port, err = net.SplitHostPort(p.address)
if err != nil { if err != nil {
return errors.Wrapf(err, "error parsing %s", p.address) return errors.Wrapf(err, "error parsing %s", p.options.address)
} }
if port == "443" { if port == "443" {
p.caURL = fmt.Sprintf("https://%s", p.caURL) p.options.caURL = fmt.Sprintf("https://%s", p.options.caURL)
} else { } else {
p.caURL = fmt.Sprintf("https://%s:%s", p.caURL, port) p.options.caURL = fmt.Sprintf("https://%s:%s", p.options.caURL, port)
} }
} }
@ -617,7 +794,7 @@ func (p *PKI) Save(opt ...Option) error {
defaults := &caDefaults{ defaults := &caDefaults{
Root: p.root, Root: p.root,
CAConfig: p.config, CAConfig: p.config,
CAUrl: p.caURL, CAUrl: p.options.caURL,
Fingerprint: p.rootFingerprint, Fingerprint: p.rootFingerprint,
} }
b, err = json.MarshalIndent(defaults, "", "\t") b, err = json.MarshalIndent(defaults, "", "\t")

View file

@ -13,7 +13,7 @@ import (
// getTemplates returns all the templates enabled // getTemplates returns all the templates enabled
func (p *PKI) getTemplates() *templates.Templates { func (p *PKI) getTemplates() *templates.Templates {
if !p.enableSSH { if !p.options.enableSSH {
return nil return nil
} }
return &templates.Templates{ return &templates.Templates{