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"
"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"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/cas"
"github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/nosql"
"go.step.sm/cli-utils/config"
"go.step.sm/cli-utils/errs"
"go.step.sm/cli-utils/fileutil"
@ -32,9 +36,26 @@ import (
"go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/pemutil"
"go.step.sm/linkedca"
"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 (
// ConfigPath is the directory name under the step path where the configuration
// files will be stored.
@ -134,9 +155,88 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) {
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.
type PKI struct {
casOptions apiv1.Options
caService apiv1.CertificateAuthorityService
caCreator apiv1.CertificateAuthorityCreator
root, rootKey, rootFingerprint string
intermediate, intermediateKey string
@ -145,20 +245,25 @@ type PKI struct {
config, defaults string
ottPublicKey *jose.JSONWebKey
ottPrivateKey *jose.JSONWebEncryption
provisioner string
address string
dnsNames []string
caURL string
enableSSH bool
options *options
}
// New creates a new PKI configuration.
func New(opts apiv1.Options) (*PKI, error) {
caCreator, err := cas.NewCreator(context.Background(), opts)
func New(o apiv1.Options, opts ...PKIOption) (*PKI, error) {
caService, err := cas.New(context.Background(), o)
if err != nil {
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()
private := GetSecretsPath()
config := GetConfigPath()
@ -180,12 +285,19 @@ func New(opts apiv1.Options) (*PKI, error) {
}
p := &PKI{
casOptions: opts,
caCreator: caCreator,
provisioner: "step-cli",
address: "127.0.0.1:9000",
dnsNames: []string{"127.0.0.1"},
casOptions: o,
caCreator: caCreator,
caService: caService,
options: &options{
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 {
return nil, err
}
@ -233,23 +345,31 @@ func (p *PKI) GetRootFingerprint() string {
}
// SetProvisioner sets the provisioner name of the OTT keys.
//
// Deprecated: this method is deprecated in favor of WithProvisioner.
func (p *PKI) SetProvisioner(s string) {
p.provisioner = s
p.options.provisioner = s
}
// SetAddress sets the listening address of the CA.
//
// Deprecated: this method is deprecated in favor of WithAddress.
func (p *PKI) SetAddress(s string) {
p.address = s
p.options.address = s
}
// SetDNSNames sets the dns names of the CA.
//
// Deprecated: this method is deprecated in favor of WithDNSNames.
func (p *PKI) SetDNSNames(s []string) {
p.dnsNames = s
p.options.dnsNames = s
}
// 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) {
p.caURL = s
p.options.caURL = s
}
// 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
// RA.
func (p *PKI) GetCertificateAuthority() error {
srv, ok := p.caCreator.(apiv1.CertificateAuthorityGetter)
srv, ok := p.caService.(apiv1.CertificateAuthorityGetter)
if !ok {
return nil
}
@ -427,7 +547,7 @@ func (p *PKI) GenerateSSHSigningKeys(password []byte) error {
return err
}
}
p.enableSSH = true
p.options.enableSSH = true
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(" 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(" \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
@ -465,7 +586,7 @@ func (p *PKI) tellPKI() {
} else {
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 private key", p.sshUserKey)
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
// the authority config.
//
// Deprecated: this method is deprecated because this is the default behavior.
func WithDefaultDB() Option {
return func(c *authconfig.Config) error {
c.DB = &db.Config{
@ -497,6 +620,8 @@ func WithDefaultDB() Option {
// WithoutDB is a configuration modifier that adds a default DB stanza to
// the authority config.
//
// De[recated: this method is deprecated in favor or WithNoDB.
func WithoutDB() Option {
return func(c *authconfig.Config) error {
c.DB = nil
@ -506,18 +631,6 @@ func WithoutDB() Option {
// GenerateConfig returns the step certificates configuration.
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
if !p.casOptions.Is(apiv1.SoftCAS) {
authorityOptions = &p.casOptions
@ -528,8 +641,8 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) {
FederatedRoots: []string{},
IntermediateCert: p.intermediate,
IntermediateKey: p.intermediateKey,
Address: p.address,
DNSNames: p.dnsNames,
Address: p.options.address,
DNSNames: p.options.dnsNames,
Logger: []byte(`{"format": "text"}`),
DB: &db.Config{
Type: "badger",
@ -538,44 +651,109 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) {
AuthorityConfig: &authconfig.AuthConfig{
Options: authorityOptions,
DisableIssuedAtCheck: false,
Provisioners: provisioner.List{prov},
},
TLS: &authconfig.TLSOptions{
MinVersion: authconfig.DefaultTLSMinVersion,
MaxVersion: authconfig.DefaultTLSMaxVersion,
Renegotiation: authconfig.DefaultTLSRenegotiation,
CipherSuites: authconfig.DefaultTLSCipherSuites,
EnableAdmin: false,
},
TLS: &authconfig.DefaultTLSOptions,
Templates: p.getTemplates(),
}
if p.enableSSH {
enableSSHCA := true
config.SSH = &authconfig.SSHConfig{
HostKey: p.sshHostKey,
UserKey: p.sshUserKey,
// On standalone deployments add the provisioners to either the ca.json or
// the database.
var provisioners []provisioner.Interface
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{
EnableSSHCA: &enableSSHCA,
prov := &provisioner.JWK{
Name: p.options.provisioner,
Type: "JWK",
Key: p.ottPublicKey,
EncryptedKey: key,
}
// Add default SSHPOP provisioner
sshpop := &provisioner.SSHPOP{
Type: "SSHPOP",
Name: "sshpop",
Claims: &provisioner.Claims{
provisioners = append(provisioners, prov)
// Add default ACME provisioner if enabled
if p.options.enableACME {
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,
},
}
// 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
for _, o := range opt {
if err = o(config); err != nil {
if err := o(config); err != nil {
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
}
@ -599,17 +777,16 @@ func (p *PKI) Save(opt ...Option) error {
}
// Generate the CA URL.
if p.caURL == "" {
p.caURL = p.dnsNames[0]
var port string
_, port, err = net.SplitHostPort(p.address)
if p.options.caURL == "" {
p.options.caURL = p.options.dnsNames[0]
_, port, err := net.SplitHostPort(p.options.address)
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" {
p.caURL = fmt.Sprintf("https://%s", p.caURL)
p.options.caURL = fmt.Sprintf("https://%s", p.options.caURL)
} 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{
Root: p.root,
CAConfig: p.config,
CAUrl: p.caURL,
CAUrl: p.options.caURL,
Fingerprint: p.rootFingerprint,
}
b, err = json.MarshalIndent(defaults, "", "\t")

View file

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