mirror of
https://github.com/smallstep/certificates.git
synced 2024-11-15 18:12:59 +00:00
2e560caf68
So far the `GenerateConfig` method wasn't tested. This commit adds a couple of basic tests for this method. It's not fully covered yet, nor are all properties being checked, but it provides a starting point for refactoring the CA (configuration) initialization process.
1080 lines
31 KiB
Go
1080 lines
31 KiB
Go
package pki
|
||
|
||
import (
|
||
"context"
|
||
"crypto"
|
||
"crypto/sha256"
|
||
"crypto/x509"
|
||
"crypto/x509/pkix"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"net"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"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/errs"
|
||
"go.step.sm/cli-utils/fileutil"
|
||
"go.step.sm/cli-utils/step"
|
||
"go.step.sm/cli-utils/ui"
|
||
"go.step.sm/crypto/jose"
|
||
"go.step.sm/crypto/kms"
|
||
kmsapi "go.step.sm/crypto/kms/apiv1"
|
||
"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
|
||
)
|
||
|
||
// String returns the string version of the deployment type.
|
||
func (d DeploymentType) String() string {
|
||
switch d {
|
||
case StandaloneDeployment:
|
||
return "standalone"
|
||
case LinkedDeployment:
|
||
return "linked"
|
||
case HostedDeployment:
|
||
return "hosted"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
const (
|
||
// ConfigPath is the directory name under the step path where the configuration
|
||
// files will be stored.
|
||
configPath = "config"
|
||
// PublicPath is the directory name under the step path where the public keys
|
||
// will be stored.
|
||
publicPath = "certs"
|
||
// PublicPath is the directory name under the step path where the private keys
|
||
// will be stored.
|
||
privatePath = "secrets"
|
||
// DBPath is the directory name under the step path where the private keys
|
||
// will be stored.
|
||
dbPath = "db"
|
||
// templatesPath is the directory to store templates
|
||
templatesPath = "templates"
|
||
)
|
||
|
||
// GetDBPath returns the path where the file-system persistence is stored
|
||
// based on the $(step path).
|
||
func GetDBPath() string {
|
||
return filepath.Join(step.Path(), dbPath)
|
||
}
|
||
|
||
// GetConfigPath returns the directory where the configuration files are stored
|
||
// based on the $(step path).
|
||
func GetConfigPath() string {
|
||
return filepath.Join(step.Path(), configPath)
|
||
}
|
||
|
||
// GetProfileConfigPath returns the directory where the profile configuration
|
||
// files are stored based on the $(step path).
|
||
func GetProfileConfigPath() string {
|
||
return filepath.Join(step.ProfilePath(), configPath)
|
||
}
|
||
|
||
// GetPublicPath returns the directory where the public keys are stored based on
|
||
// the $(step path).
|
||
func GetPublicPath() string {
|
||
return filepath.Join(step.Path(), publicPath)
|
||
}
|
||
|
||
// GetSecretsPath returns the directory where the private keys are stored based
|
||
// on the $(step path).
|
||
func GetSecretsPath() string {
|
||
return filepath.Join(step.Path(), privatePath)
|
||
}
|
||
|
||
// GetRootCAPath returns the path where the root CA is stored based on the
|
||
// $(step path).
|
||
func GetRootCAPath() string {
|
||
return filepath.Join(step.Path(), publicPath, "root_ca.crt")
|
||
}
|
||
|
||
// GetOTTKeyPath returns the path where the one-time token key is stored based
|
||
// on the $(step path).
|
||
func GetOTTKeyPath() string {
|
||
return filepath.Join(step.Path(), privatePath, "ott_key")
|
||
}
|
||
|
||
// GetTemplatesPath returns the path where the templates are stored.
|
||
func GetTemplatesPath() string {
|
||
return filepath.Join(step.Path(), templatesPath)
|
||
}
|
||
|
||
// GetProvisioners returns the map of provisioners on the given CA.
|
||
func GetProvisioners(caURL, rootFile string) (provisioner.List, error) {
|
||
if rootFile == "" {
|
||
rootFile = GetRootCAPath()
|
||
}
|
||
client, err := ca.NewClient(caURL, ca.WithRootFile(rootFile))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
cursor := ""
|
||
provisioners := provisioner.List{}
|
||
for {
|
||
resp, err := client.Provisioners(ca.WithProvisionerCursor(cursor), ca.WithProvisionerLimit(100))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
provisioners = append(provisioners, resp.Provisioners...)
|
||
if resp.NextCursor == "" {
|
||
return provisioners, nil
|
||
}
|
||
cursor = resp.NextCursor
|
||
}
|
||
}
|
||
|
||
// GetProvisionerKey returns the encrypted provisioner key with the for the
|
||
// given kid.
|
||
func GetProvisionerKey(caURL, rootFile, kid string) (string, error) {
|
||
if rootFile == "" {
|
||
rootFile = GetRootCAPath()
|
||
}
|
||
client, err := ca.NewClient(caURL, ca.WithRootFile(rootFile))
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
resp, err := client.ProvisionerKey(kid)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return resp.Key, nil
|
||
}
|
||
|
||
type options struct {
|
||
provisioner string
|
||
superAdminSubject string
|
||
pkiOnly bool
|
||
enableACME bool
|
||
enableSSH bool
|
||
enableAdmin bool
|
||
noDB bool
|
||
isHelm bool
|
||
deploymentType DeploymentType
|
||
rootKeyURI string
|
||
intermediateKeyURI string
|
||
hostKeyURI string
|
||
userKeyURI string
|
||
}
|
||
|
||
// Option is the type of a configuration option on the pki constructor.
|
||
type Option func(p *PKI)
|
||
|
||
// WithAddress sets the listen address of step-ca.
|
||
func WithAddress(s string) Option {
|
||
return func(p *PKI) {
|
||
p.Address = s
|
||
}
|
||
}
|
||
|
||
// WithCaURL sets the default ca-url of step-ca.
|
||
func WithCaURL(s string) Option {
|
||
return func(p *PKI) {
|
||
p.Defaults.CaUrl = s
|
||
}
|
||
}
|
||
|
||
// WithDNSNames sets the SANs of step-ca.
|
||
func WithDNSNames(s []string) Option {
|
||
return func(p *PKI) {
|
||
p.DnsNames = s
|
||
}
|
||
}
|
||
|
||
// WithProvisioner defines the name of the default provisioner.
|
||
func WithProvisioner(s string) Option {
|
||
return func(p *PKI) {
|
||
p.options.provisioner = s
|
||
}
|
||
}
|
||
|
||
// WithSuperAdminSubject defines the subject of the first
|
||
// super admin for use with the Admin API. The admin will belong
|
||
// to the first JWK provisioner.
|
||
func WithSuperAdminSubject(s string) Option {
|
||
return func(p *PKI) {
|
||
p.options.superAdminSubject = s
|
||
}
|
||
}
|
||
|
||
// WithPKIOnly will only generate the PKI without the step-ca config files.
|
||
func WithPKIOnly() Option {
|
||
return func(p *PKI) {
|
||
p.options.pkiOnly = true
|
||
}
|
||
}
|
||
|
||
// WithACME enables acme provisioner in step-ca.
|
||
func WithACME() Option {
|
||
return func(p *PKI) {
|
||
p.options.enableACME = true
|
||
}
|
||
}
|
||
|
||
// WithSSH enables ssh in step-ca.
|
||
func WithSSH() Option {
|
||
return func(p *PKI) {
|
||
p.options.enableSSH = true
|
||
}
|
||
}
|
||
|
||
// WithAdmin enables the admin api in step-ca.
|
||
func WithAdmin() Option {
|
||
return func(p *PKI) {
|
||
p.options.enableAdmin = true
|
||
}
|
||
}
|
||
|
||
// WithNoDB disables the db in step-ca.
|
||
func WithNoDB() Option {
|
||
return func(p *PKI) {
|
||
p.options.noDB = true
|
||
}
|
||
}
|
||
|
||
// WithHelm configures the pki to create a helm values.yaml.
|
||
func WithHelm() Option {
|
||
return func(p *PKI) {
|
||
p.options.isHelm = true
|
||
}
|
||
}
|
||
|
||
// WithDeploymentType defines the deployment type of step-ca.
|
||
func WithDeploymentType(dt DeploymentType) Option {
|
||
return func(p *PKI) {
|
||
p.options.deploymentType = dt
|
||
}
|
||
}
|
||
|
||
// WithKMS enables the kms with the given name.
|
||
func WithKMS(name string) Option {
|
||
return func(p *PKI) {
|
||
typ := linkedca.KMS_Type_value[strings.ToUpper(name)]
|
||
p.Configuration.Kms = &linkedca.KMS{
|
||
Type: linkedca.KMS_Type(typ),
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithKeyURIs defines the key uris for X.509 and SSH keys.
|
||
func WithKeyURIs(rootKey, intermediateKey, hostKey, userKey string) Option {
|
||
return func(p *PKI) {
|
||
p.options.rootKeyURI = rootKey
|
||
p.options.intermediateKeyURI = intermediateKey
|
||
p.options.hostKeyURI = hostKey
|
||
p.options.userKeyURI = userKey
|
||
}
|
||
}
|
||
|
||
// PKI represents the Public Key Infrastructure used by a certificate authority.
|
||
type PKI struct {
|
||
linkedca.Configuration
|
||
Defaults linkedca.Defaults
|
||
casOptions apiv1.Options
|
||
caService apiv1.CertificateAuthorityService
|
||
caCreator apiv1.CertificateAuthorityCreator
|
||
keyManager kmsapi.KeyManager
|
||
config string
|
||
defaults string
|
||
profileDefaults string
|
||
ottPublicKey *jose.JSONWebKey
|
||
ottPrivateKey *jose.JSONWebEncryption
|
||
options *options
|
||
}
|
||
|
||
// New creates a new PKI configuration.
|
||
func New(o apiv1.Options, opts ...Option) (*PKI, error) {
|
||
// TODO(hs): invoking `New` with a context active will use values from
|
||
// that CA context while generating the context. Thay may or may not
|
||
// be fully expected and/or what we want. This specific behavior was
|
||
// changed after not relying on the `init` inside of `step`, resulting in
|
||
// the default context being active if `step.Init` isn't called explicitly.
|
||
// It can still result in surprising results, though.
|
||
currentCtx := step.Contexts().GetCurrent()
|
||
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 %q does not implement CertificateAuthorityCreator", o.Type)
|
||
}
|
||
caCreator = creator
|
||
}
|
||
|
||
// get absolute path for dir/name
|
||
getPath := func(dir string, name string) (string, error) {
|
||
s, err := filepath.Abs(filepath.Join(dir, name))
|
||
return s, errors.Wrapf(err, "error getting absolute path for %s", name)
|
||
}
|
||
|
||
p := &PKI{
|
||
Configuration: linkedca.Configuration{
|
||
Address: "127.0.0.1:9000",
|
||
DnsNames: []string{"127.0.0.1"},
|
||
Ssh: &linkedca.SSH{},
|
||
Authority: &linkedca.Authority{},
|
||
Files: make(map[string][]byte),
|
||
},
|
||
casOptions: o,
|
||
caService: caService,
|
||
caCreator: caCreator,
|
||
keyManager: o.KeyManager,
|
||
options: &options{
|
||
provisioner: "step-cli",
|
||
},
|
||
}
|
||
for _, fn := range opts {
|
||
fn(p)
|
||
}
|
||
|
||
// Use default key manager
|
||
if p.keyManager == nil {
|
||
p.keyManager = kms.Default
|
||
}
|
||
|
||
// Use /home/step as the step path in helm configurations.
|
||
// Use the current step path when creating pki in files.
|
||
var public, private, cfg string
|
||
if p.options.isHelm {
|
||
public = "/home/step/certs"
|
||
private = "/home/step/secrets"
|
||
cfg = "/home/step/config"
|
||
} else {
|
||
public = GetPublicPath()
|
||
private = GetSecretsPath()
|
||
cfg = GetConfigPath()
|
||
// Create directories
|
||
dirs := []string{public, private, cfg, GetTemplatesPath()}
|
||
if currentCtx != nil {
|
||
dirs = append(dirs, GetProfileConfigPath())
|
||
}
|
||
for _, name := range dirs {
|
||
if _, err := os.Stat(name); os.IsNotExist(err) {
|
||
if err = os.MkdirAll(name, 0700); err != nil {
|
||
return nil, errs.FileError(err, name)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if p.Defaults.CaUrl == "" {
|
||
p.Defaults.CaUrl = p.DnsNames[0]
|
||
_, port, err := net.SplitHostPort(p.Address)
|
||
if err != nil {
|
||
return nil, errors.Wrapf(err, "error parsing %s", p.Address)
|
||
}
|
||
// On k8s we usually access through a service, and this is configured on
|
||
// port 443 by default.
|
||
if port == "443" || p.options.isHelm {
|
||
p.Defaults.CaUrl = fmt.Sprintf("https://%s", p.Defaults.CaUrl)
|
||
} else {
|
||
p.Defaults.CaUrl = fmt.Sprintf("https://%s", net.JoinHostPort(p.Defaults.CaUrl, port))
|
||
}
|
||
}
|
||
|
||
root, err := getPath(public, "root_ca.crt")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
rootKey, err := getPath(private, "root_ca_key")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
p.Root = []string{root}
|
||
p.RootKey = []string{rootKey}
|
||
p.Defaults.Root = root
|
||
|
||
if p.Intermediate, err = getPath(public, "intermediate_ca.crt"); err != nil {
|
||
return nil, err
|
||
}
|
||
if p.IntermediateKey, err = getPath(private, "intermediate_ca_key"); err != nil {
|
||
return nil, err
|
||
}
|
||
if p.Ssh.HostPublicKey, err = getPath(public, "ssh_host_ca_key.pub"); err != nil {
|
||
return nil, err
|
||
}
|
||
if p.Ssh.UserPublicKey, err = getPath(public, "ssh_user_ca_key.pub"); err != nil {
|
||
return nil, err
|
||
}
|
||
if p.Ssh.HostKey, err = getPath(private, "ssh_host_ca_key"); err != nil {
|
||
return nil, err
|
||
}
|
||
if p.Ssh.UserKey, err = getPath(private, "ssh_user_ca_key"); err != nil {
|
||
return nil, err
|
||
}
|
||
if p.defaults, err = getPath(cfg, "defaults.json"); err != nil {
|
||
return nil, err
|
||
}
|
||
if currentCtx != nil {
|
||
p.profileDefaults = currentCtx.ProfileDefaultsFile()
|
||
}
|
||
|
||
if p.config, err = getPath(cfg, "ca.json"); err != nil {
|
||
return nil, err
|
||
}
|
||
p.Defaults.CaConfig = p.config
|
||
|
||
return p, nil
|
||
}
|
||
|
||
// GetCAConfigPath returns the path of the CA configuration file.
|
||
func (p *PKI) GetCAConfigPath() string {
|
||
return p.config
|
||
}
|
||
|
||
// GetRootFingerprint returns the root fingerprint.
|
||
func (p *PKI) GetRootFingerprint() string {
|
||
return p.Defaults.Fingerprint
|
||
}
|
||
|
||
// GenerateKeyPairs generates the key pairs used by the certificate authority.
|
||
func (p *PKI) GenerateKeyPairs(pass []byte) error {
|
||
var err error
|
||
// Create OTT key pair, the user doesn't need to know about this.
|
||
p.ottPublicKey, p.ottPrivateKey, err = jose.GenerateDefaultKeyPair(pass)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var claims *linkedca.Claims
|
||
if p.options.enableSSH {
|
||
claims = &linkedca.Claims{
|
||
Ssh: &linkedca.SSHClaims{
|
||
Enabled: true,
|
||
},
|
||
}
|
||
}
|
||
|
||
// Add JWK provisioner to the configuration.
|
||
publicKey, err := json.Marshal(p.ottPublicKey)
|
||
if err != nil {
|
||
return errors.Wrap(err, "error marshaling public key")
|
||
}
|
||
encryptedKey, err := p.ottPrivateKey.CompactSerialize()
|
||
if err != nil {
|
||
return errors.Wrap(err, "error serializing private key")
|
||
}
|
||
p.Authority.Provisioners = append(p.Authority.Provisioners, &linkedca.Provisioner{
|
||
Type: linkedca.Provisioner_JWK,
|
||
Name: p.options.provisioner,
|
||
Claims: claims,
|
||
Details: &linkedca.ProvisionerDetails{
|
||
Data: &linkedca.ProvisionerDetails_JWK{
|
||
JWK: &linkedca.JWKProvisioner{
|
||
PublicKey: publicKey,
|
||
EncryptedPrivateKey: []byte(encryptedKey),
|
||
},
|
||
},
|
||
},
|
||
})
|
||
|
||
return nil
|
||
}
|
||
|
||
// GenerateRootCertificate generates a root certificate with the given name
|
||
// and using the default key type.
|
||
func (p *PKI) GenerateRootCertificate(name, org, resource string, pass []byte) (*apiv1.CreateCertificateAuthorityResponse, error) {
|
||
if uri := p.options.rootKeyURI; uri != "" {
|
||
p.RootKey[0] = uri
|
||
}
|
||
|
||
resp, err := p.caCreator.CreateCertificateAuthority(&apiv1.CreateCertificateAuthorityRequest{
|
||
Name: resource + "-Root-CA",
|
||
Type: apiv1.RootCA,
|
||
Lifetime: 10 * 365 * 24 * time.Hour,
|
||
CreateKey: &apiv1.CreateKeyRequest{
|
||
Name: p.RootKey[0],
|
||
SignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm,
|
||
},
|
||
Template: &x509.Certificate{
|
||
Subject: pkix.Name{
|
||
CommonName: name + " Root CA",
|
||
Organization: []string{org},
|
||
},
|
||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||
BasicConstraintsValid: true,
|
||
IsCA: true,
|
||
MaxPathLen: 1,
|
||
MaxPathLenZero: false,
|
||
},
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Replace key name with the one from the key manager if available. On
|
||
// softcas this will be the original filename, on any other kms will be the
|
||
// uri to the key.
|
||
if resp.KeyName != "" {
|
||
p.RootKey[0] = resp.KeyName
|
||
}
|
||
|
||
// PrivateKey will only be set if we have access to it (SoftCAS).
|
||
if err := p.WriteRootCertificate(resp.Certificate, resp.PrivateKey, pass); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return resp, nil
|
||
}
|
||
|
||
// WriteRootCertificate writes to the buffer the given certificate and key if given.
|
||
func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error {
|
||
p.Files[p.Root[0]] = encodeCertificate(rootCrt)
|
||
if rootKey != nil {
|
||
var err error
|
||
p.Files[p.RootKey[0]], err = encodePrivateKey(rootKey, pass)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
sum := sha256.Sum256(rootCrt.Raw)
|
||
p.Defaults.Fingerprint = strings.ToLower(hex.EncodeToString(sum[:]))
|
||
return nil
|
||
}
|
||
|
||
// GenerateIntermediateCertificate generates an intermediate certificate with
|
||
// the given name and using the default key type.
|
||
func (p *PKI) GenerateIntermediateCertificate(name, org, resource string, parent *apiv1.CreateCertificateAuthorityResponse, pass []byte) error {
|
||
if uri := p.options.intermediateKeyURI; uri != "" {
|
||
p.IntermediateKey = uri
|
||
}
|
||
|
||
resp, err := p.caCreator.CreateCertificateAuthority(&apiv1.CreateCertificateAuthorityRequest{
|
||
Name: resource + "-Intermediate-CA",
|
||
Type: apiv1.IntermediateCA,
|
||
Lifetime: 10 * 365 * 24 * time.Hour,
|
||
CreateKey: &apiv1.CreateKeyRequest{
|
||
Name: p.IntermediateKey,
|
||
SignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm,
|
||
},
|
||
Template: &x509.Certificate{
|
||
Subject: pkix.Name{
|
||
CommonName: name + " Intermediate CA",
|
||
Organization: []string{org},
|
||
},
|
||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||
BasicConstraintsValid: true,
|
||
IsCA: true,
|
||
MaxPathLen: 0,
|
||
MaxPathLenZero: true,
|
||
},
|
||
Parent: parent,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
p.casOptions.CertificateAuthority = resp.Name
|
||
p.Files[p.Intermediate] = encodeCertificate(resp.Certificate)
|
||
|
||
// Replace the key name with the one from the key manager. On softcas this
|
||
// will be the original filename, on any other kms will be the uri to the
|
||
// key.
|
||
if resp.KeyName != "" {
|
||
p.IntermediateKey = resp.KeyName
|
||
}
|
||
|
||
// If a kms is used it will not have the private key
|
||
if resp.PrivateKey != nil {
|
||
p.Files[p.IntermediateKey], err = encodePrivateKey(resp.PrivateKey, pass)
|
||
}
|
||
|
||
return err
|
||
}
|
||
|
||
// CreateCertificateAuthorityResponse returns a
|
||
// CreateCertificateAuthorityResponse that can be used as a parent of a
|
||
// CreateCertificateAuthority request.
|
||
func (p *PKI) CreateCertificateAuthorityResponse(cert *x509.Certificate, key crypto.PrivateKey) *apiv1.CreateCertificateAuthorityResponse {
|
||
signer, _ := key.(crypto.Signer)
|
||
return &apiv1.CreateCertificateAuthorityResponse{
|
||
Certificate: cert,
|
||
PrivateKey: key,
|
||
Signer: signer,
|
||
}
|
||
}
|
||
|
||
// GetCertificateAuthority attempts to load the certificate authority from the
|
||
// RA.
|
||
func (p *PKI) GetCertificateAuthority() error {
|
||
srv, ok := p.caService.(apiv1.CertificateAuthorityGetter)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
|
||
resp, err := srv.GetCertificateAuthority(&apiv1.GetCertificateAuthorityRequest{
|
||
Name: p.casOptions.CertificateAuthority,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := p.WriteRootCertificate(resp.RootCertificate, nil, nil); err != nil {
|
||
return err
|
||
}
|
||
|
||
// Issuer is in the RA
|
||
p.Intermediate = ""
|
||
p.IntermediateKey = ""
|
||
|
||
return nil
|
||
}
|
||
|
||
// GenerateSSHSigningKeys generates and encrypts a private key used for signing
|
||
// SSH user certificates and a private key used for signing host certificates.
|
||
func (p *PKI) GenerateSSHSigningKeys(password []byte) error {
|
||
// Enable SSH
|
||
p.options.enableSSH = true // TODO(hs): change this function to not mutate configuration state
|
||
|
||
// Create SSH key used to sign host certificates. Using
|
||
// kmsapi.UnspecifiedSignAlgorithm will default to the default algorithm.
|
||
name := p.Ssh.HostKey
|
||
if uri := p.options.hostKeyURI; uri != "" {
|
||
name = uri
|
||
}
|
||
resp, err := p.keyManager.CreateKey(&kmsapi.CreateKeyRequest{
|
||
Name: name,
|
||
SignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
sshKey, err := ssh.NewPublicKey(resp.PublicKey)
|
||
if err != nil {
|
||
return errors.Wrapf(err, "error converting public key")
|
||
}
|
||
p.Files[p.Ssh.HostPublicKey] = ssh.MarshalAuthorizedKey(sshKey)
|
||
|
||
// On softkms we will have the private key
|
||
if resp.PrivateKey != nil {
|
||
p.Files[p.Ssh.HostKey], err = encodePrivateKey(resp.PrivateKey, password)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
} else {
|
||
p.Ssh.HostKey = resp.Name
|
||
}
|
||
|
||
// Create SSH key used to sign user certificates. Using
|
||
// kmsapi.UnspecifiedSignAlgorithm will default to the default algorithm.
|
||
name = p.Ssh.UserKey
|
||
if uri := p.options.userKeyURI; uri != "" {
|
||
name = uri
|
||
}
|
||
resp, err = p.keyManager.CreateKey(&kmsapi.CreateKeyRequest{
|
||
Name: name,
|
||
SignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
sshKey, err = ssh.NewPublicKey(resp.PublicKey)
|
||
if err != nil {
|
||
return errors.Wrapf(err, "error converting public key")
|
||
}
|
||
p.Files[p.Ssh.UserPublicKey] = ssh.MarshalAuthorizedKey(sshKey)
|
||
|
||
// On softkms we will have the private key
|
||
if resp.PrivateKey != nil {
|
||
p.Files[p.Ssh.UserKey], err = encodePrivateKey(resp.PrivateKey, password)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
} else {
|
||
p.Ssh.UserKey = resp.Name
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// WriteFiles writes on disk the previously generated files.
|
||
func (p *PKI) WriteFiles() error {
|
||
for fn, b := range p.Files {
|
||
if err := fileutil.WriteFile(fn, b, 0600); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (p *PKI) askFeedback() {
|
||
ui.Println()
|
||
ui.Println("\033[1mFEEDBACK\033[0m 😍 🍻")
|
||
ui.Println(" The \033[1mstep\033[0m utility is not instrumented for usage statistics. It does not phone")
|
||
ui.Println(" home. But your feedback is extremely valuable. Any information you can provide")
|
||
ui.Println(" regarding how you’re using `step` helps. Please send us a sentence or two,")
|
||
ui.Println(" good or bad at \033[1mfeedback@smallstep.com\033[0m or join GitHub Discussions")
|
||
ui.Println(" \033[1mhttps://github.com/smallstep/certificates/discussions\033[0m and our Discord ")
|
||
ui.Println(" \033[1mhttps://u.step.sm/discord\033[0m.")
|
||
|
||
if p.options.deploymentType == LinkedDeployment {
|
||
ui.Println()
|
||
ui.Println("\033[1mNEXT STEPS\033[0m")
|
||
ui.Println(" 1. Log in or create a Certificate Manager account at \033[1mhttps://u.step.sm/linked\033[0m")
|
||
ui.Println(" 2. Add a new authority and select \"Link a step-ca instance\"")
|
||
ui.Println(" 3. Follow instructions in browser to start `step-ca` using the `--token` flag")
|
||
ui.Println()
|
||
}
|
||
}
|
||
|
||
func (p *PKI) tellPKI() {
|
||
ui.Println()
|
||
switch {
|
||
case p.casOptions.Is(apiv1.SoftCAS):
|
||
ui.PrintSelected("Root certificate", p.Root[0])
|
||
ui.PrintSelected("Root private key", p.RootKey[0])
|
||
ui.PrintSelected("Root fingerprint", p.Defaults.Fingerprint)
|
||
ui.PrintSelected("Intermediate certificate", p.Intermediate)
|
||
ui.PrintSelected("Intermediate private key", p.IntermediateKey)
|
||
case p.Defaults.Fingerprint != "":
|
||
ui.PrintSelected("Root certificate", p.Root[0])
|
||
ui.PrintSelected("Root fingerprint", p.Defaults.Fingerprint)
|
||
default:
|
||
ui.Printf(`{{ "%s" | red }} {{ "Root certificate:" | bold }} failed to retrieve it from RA`+"\n", ui.IconBad)
|
||
}
|
||
if p.options.enableSSH {
|
||
ui.PrintSelected("SSH user public key", p.Ssh.UserPublicKey)
|
||
ui.PrintSelected("SSH user private key", p.Ssh.UserKey)
|
||
ui.PrintSelected("SSH host public key", p.Ssh.HostPublicKey)
|
||
ui.PrintSelected("SSH host private key", p.Ssh.HostKey)
|
||
}
|
||
}
|
||
|
||
type caDefaults struct {
|
||
CAUrl string `json:"ca-url"`
|
||
CAConfig string `json:"ca-config"`
|
||
Fingerprint string `json:"fingerprint"`
|
||
Root string `json:"root"`
|
||
}
|
||
|
||
// ConfigOption is the type for modifiers over the auth config object.
|
||
type ConfigOption func(c *authconfig.Config) error
|
||
|
||
// GenerateConfig returns the step certificates configuration.
|
||
func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) {
|
||
var authorityOptions *apiv1.Options
|
||
if !p.casOptions.Is(apiv1.SoftCAS) {
|
||
authorityOptions = &p.casOptions
|
||
}
|
||
|
||
cfg := &authconfig.Config{
|
||
Root: p.Root,
|
||
FederatedRoots: p.FederatedRoots,
|
||
IntermediateCert: p.Intermediate,
|
||
IntermediateKey: p.IntermediateKey,
|
||
Address: p.Address,
|
||
DNSNames: p.DnsNames,
|
||
Logger: []byte(`{"format": "text"}`),
|
||
DB: &db.Config{
|
||
Type: "badgerv2",
|
||
DataSource: GetDBPath(),
|
||
},
|
||
AuthorityConfig: &authconfig.AuthConfig{
|
||
Options: authorityOptions,
|
||
DisableIssuedAtCheck: false,
|
||
EnableAdmin: false,
|
||
},
|
||
TLS: &authconfig.DefaultTLSOptions,
|
||
Templates: p.getTemplates(),
|
||
}
|
||
|
||
// Disable the database when WithNoDB() option is passed.
|
||
if p.options.noDB {
|
||
cfg.DB = nil
|
||
}
|
||
|
||
// Add linked as a deployment type to detect it on start and provide a
|
||
// message if the token is not given.
|
||
if p.options.deploymentType == LinkedDeployment {
|
||
cfg.AuthorityConfig.DeploymentType = LinkedDeployment.String()
|
||
}
|
||
|
||
// Enable KMS if necessary
|
||
if p.Kms != nil {
|
||
typ := strings.ToLower(p.Kms.Type.String())
|
||
cfg.KMS = &kmsapi.Options{
|
||
Type: kmsapi.Type(typ),
|
||
}
|
||
}
|
||
|
||
// 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")
|
||
}
|
||
|
||
prov := &provisioner.JWK{
|
||
Name: p.options.provisioner,
|
||
Type: "JWK",
|
||
Key: p.ottPublicKey,
|
||
EncryptedKey: key,
|
||
}
|
||
provisioners = append(provisioners, prov)
|
||
|
||
// Add default ACME provisioner if enabled
|
||
if p.options.enableACME {
|
||
// To prevent name clashes for the default ACME provisioner, we append "-1" to
|
||
// the name if it already exists. See https://github.com/smallstep/cli/issues/1018
|
||
// for the reason.
|
||
acmeProvisionerName := "acme"
|
||
if p.options.provisioner == acmeProvisionerName {
|
||
acmeProvisionerName = fmt.Sprintf("%s-1", acmeProvisionerName)
|
||
}
|
||
provisioners = append(provisioners, &provisioner.ACME{
|
||
Type: "ACME",
|
||
Name: acmeProvisionerName,
|
||
})
|
||
}
|
||
|
||
if p.options.enableSSH {
|
||
enableSSHCA := true
|
||
cfg.SSH = &authconfig.SSHConfig{
|
||
HostKey: p.Ssh.HostKey,
|
||
UserKey: p.Ssh.UserKey,
|
||
}
|
||
// Enable SSH authorization for default JWK provisioner
|
||
prov.Claims = &provisioner.Claims{
|
||
EnableSSHCA: &enableSSHCA,
|
||
}
|
||
|
||
// Add default SSHPOP provisioner. To prevent name clashes for the default
|
||
// SSHPOP provisioner, we append "-1" to the name if it already exists.
|
||
// See https://github.com/smallstep/cli/issues/1018 for the reason.
|
||
sshProvisionerName := "sshpop"
|
||
if p.options.provisioner == sshProvisionerName {
|
||
sshProvisionerName = fmt.Sprintf("%s-1", sshProvisionerName)
|
||
}
|
||
provisioners = append(provisioners, &provisioner.SSHPOP{
|
||
Type: "SSHPOP",
|
||
Name: sshProvisionerName,
|
||
Claims: &provisioner.Claims{
|
||
EnableSSHCA: &enableSSHCA,
|
||
},
|
||
})
|
||
}
|
||
}
|
||
|
||
// Apply configuration modifiers
|
||
for _, o := range opt {
|
||
if err := o(cfg); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// Set authority.enableAdmin to true
|
||
if p.options.enableAdmin {
|
||
cfg.AuthorityConfig.EnableAdmin = true
|
||
}
|
||
|
||
if p.options.deploymentType == StandaloneDeployment {
|
||
if !cfg.AuthorityConfig.EnableAdmin {
|
||
cfg.AuthorityConfig.Provisioners = provisioners
|
||
} else {
|
||
// At this moment this code path is never used because `step ca
|
||
// init` will always set enableAdmin to false for a standalone
|
||
// deployment. Once we move `step beta` commands out of the beta we
|
||
// should probably default to this route.
|
||
//
|
||
// Note that we might want to be able to define the database as a
|
||
// flag in `step ca init` so we can write to the proper place.
|
||
//
|
||
// TODO(hs): the logic for creating the provisioners and the super admin
|
||
// is similar to what's done when automatically migrating the provisioners.
|
||
// This is related to the existing comment above. Refactor this to exist in
|
||
// a single place and ensure it happens only once.
|
||
_db, err := db.New(cfg.DB)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer _db.Shutdown() // free DB resources; unlock BadgerDB file
|
||
|
||
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.
|
||
superAdminSubject := "step"
|
||
if p.options.superAdminSubject != "" {
|
||
superAdminSubject = p.options.superAdminSubject
|
||
}
|
||
if err := adminDB.CreateAdmin(context.Background(), &linkedca.Admin{
|
||
AuthorityId: admin.DefaultAuthorityID,
|
||
Subject: superAdminSubject,
|
||
Type: linkedca.Admin_SUPER_ADMIN,
|
||
ProvisionerId: adminID,
|
||
}); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
}
|
||
|
||
return cfg, nil
|
||
}
|
||
|
||
// Save stores the pki on a json file that will be used as the certificate
|
||
// authority configuration.
|
||
func (p *PKI) Save(opt ...ConfigOption) error {
|
||
// Write generated files
|
||
if err := p.WriteFiles(); err != nil {
|
||
return err
|
||
}
|
||
|
||
// Display the files written
|
||
p.tellPKI()
|
||
|
||
// Generate and write ca.json
|
||
if !p.options.pkiOnly {
|
||
cfg, err := p.GenerateConfig(opt...)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
b, err := json.MarshalIndent(cfg, "", "\t")
|
||
if err != nil {
|
||
return errors.Wrapf(err, "error marshaling %s", p.config)
|
||
}
|
||
if err = fileutil.WriteFile(p.config, b, 0644); err != nil {
|
||
return errs.FileError(err, p.config)
|
||
}
|
||
|
||
// Generate and write defaults.json
|
||
defaults := &caDefaults{
|
||
Root: p.Defaults.Root,
|
||
CAConfig: p.Defaults.CaConfig,
|
||
CAUrl: p.Defaults.CaUrl,
|
||
Fingerprint: p.Defaults.Fingerprint,
|
||
}
|
||
b, err = json.MarshalIndent(defaults, "", "\t")
|
||
if err != nil {
|
||
return errors.Wrapf(err, "error marshaling %s", p.defaults)
|
||
}
|
||
if err = fileutil.WriteFile(p.defaults, b, 0644); err != nil {
|
||
return errs.FileError(err, p.defaults)
|
||
}
|
||
// If we're using contexts then write a blank object to the default profile
|
||
// configuration location.
|
||
if p.profileDefaults != "" {
|
||
if _, err := os.Stat(p.profileDefaults); os.IsNotExist(err) {
|
||
// Write with 0600 to be consistent with directories structure.
|
||
if err = fileutil.WriteFile(p.profileDefaults, []byte("{}"), 0600); err != nil {
|
||
return errs.FileError(err, p.profileDefaults)
|
||
}
|
||
} else if err != nil {
|
||
return errs.FileError(err, p.profileDefaults)
|
||
}
|
||
}
|
||
|
||
// Generate and write templates
|
||
if err := generateTemplates(cfg.Templates); err != nil {
|
||
return err
|
||
}
|
||
|
||
if cfg.DB != nil {
|
||
os.MkdirAll(cfg.DB.DataSource, 0700)
|
||
ui.PrintSelected("Database folder", cfg.DB.DataSource)
|
||
}
|
||
if cfg.Templates != nil {
|
||
ui.PrintSelected("Templates folder", GetTemplatesPath())
|
||
}
|
||
|
||
ui.PrintSelected("Default configuration", p.defaults)
|
||
if p.profileDefaults != "" {
|
||
ui.PrintSelected("Default profile configuration", p.profileDefaults)
|
||
}
|
||
ui.PrintSelected("Certificate Authority configuration", p.config)
|
||
if cfg.AuthorityConfig.EnableAdmin && p.options.deploymentType != LinkedDeployment {
|
||
// TODO(hs): we may want to get this information from the DB, because that's
|
||
// where the admin and provisioner are stored in this case. Requires some
|
||
// refactoring.
|
||
superAdminSubject := "step"
|
||
if p.options.superAdminSubject != "" {
|
||
superAdminSubject = p.options.superAdminSubject
|
||
}
|
||
ui.PrintSelected("Admin provisioner", fmt.Sprintf("%s (JWK)", p.options.provisioner))
|
||
ui.PrintSelected("Super admin subject", superAdminSubject)
|
||
}
|
||
|
||
if p.options.deploymentType != LinkedDeployment {
|
||
ui.Println()
|
||
if p.casOptions.Is(apiv1.SoftCAS) {
|
||
ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.")
|
||
} else {
|
||
ui.Println("Your registration authority is ready to go. To generate certificates for individual services see 'step help ca'.")
|
||
}
|
||
}
|
||
}
|
||
|
||
p.askFeedback()
|
||
return nil
|
||
}
|
||
|
||
func encodeCertificate(c *x509.Certificate) []byte {
|
||
return pem.EncodeToMemory(&pem.Block{
|
||
Type: "CERTIFICATE",
|
||
Bytes: c.Raw,
|
||
})
|
||
}
|
||
|
||
func encodePrivateKey(key crypto.PrivateKey, pass []byte) ([]byte, error) {
|
||
block, err := pemutil.Serialize(key, pemutil.WithPassword(pass))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return pem.EncodeToMemory(block), nil
|
||
}
|