diff --git a/commands/onboard.go b/commands/onboard.go index fcabc83c..2b174a60 100644 --- a/commands/onboard.go +++ b/commands/onboard.go @@ -11,8 +11,8 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/ca" + "github.com/smallstep/certificates/pki" "github.com/smallstep/cli/command" - "github.com/smallstep/cli/crypto/pki" "github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/errs" "github.com/smallstep/cli/ui" @@ -110,7 +110,7 @@ func onboardAction(ctx *cli.Context) error { payload, err := json.Marshal(onboardingPayload{Fingerprint: fp}) if err != nil { - return errors.Wrap(err, "error marshalling payload") + return errors.Wrap(err, "error marshaling payload") } resp, err := http.Post(onboardingURL, "application/json", bytes.NewBuffer(payload)) diff --git a/pki/pki.go b/pki/pki.go new file mode 100644 index 00000000..1fab714d --- /dev/null +++ b/pki/pki.go @@ -0,0 +1,506 @@ +package pki + +import ( + "crypto" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "html" + "net" + "os" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/crypto/ssh" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/certificates/db" + "github.com/smallstep/cli/config" + "github.com/smallstep/cli/crypto/keys" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/crypto/tlsutil" + "github.com/smallstep/cli/crypto/x509util" + "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/jose" + "github.com/smallstep/cli/ui" + "github.com/smallstep/cli/utils" +) + +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" +) + +// GetDBPath returns the path where the file-system persistence is stored +// based on the STEPPATH environment variable. +func GetDBPath() string { + return filepath.Join(config.StepPath(), dbPath) +} + +// GetConfigPath returns the directory where the configuration files are stored +// based on the STEPPATH environment variable. +func GetConfigPath() string { + return filepath.Join(config.StepPath(), configPath) +} + +// GetPublicPath returns the directory where the public keys are stored based on +// the STEPPATH environment variable. +func GetPublicPath() string { + return filepath.Join(config.StepPath(), publicPath) +} + +// GetSecretsPath returns the directory where the private keys are stored based +// on the STEPPATH environment variable. +func GetSecretsPath() string { + return filepath.Join(config.StepPath(), privatePath) +} + +// GetRootCAPath returns the path where the root CA is stored based on the +// STEPPATH environment variable. +func GetRootCAPath() string { + return filepath.Join(config.StepPath(), publicPath, "root_ca.crt") +} + +// GetOTTKeyPath returns the path where the one-time token key is stored based +// on the STEPPATH environment variable. +func GetOTTKeyPath() string { + return filepath.Join(config.StepPath(), privatePath, "ott_key") +} + +// GetProvisioners returns the map of provisioners on the given CA. +func GetProvisioners(caURL, rootFile string) (provisioner.List, error) { + if len(rootFile) == 0 { + 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 len(rootFile) == 0 { + 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 +} + +// PKI represents the Public Key Infrastructure used by a certificate authority. +type PKI struct { + root, rootKey, rootFingerprint string + intermediate, intermediateKey string + sshHostPubKey, sshHostKey string + sshUserPubKey, sshUserKey string + config, defaults string + ottPublicKey *jose.JSONWebKey + ottPrivateKey *jose.JSONWebEncryption + provisioner string + address string + dnsNames []string + caURL string + enableSSH bool +} + +// New creates a new PKI configuration. +func New(public, private, config string) (*PKI, error) { + if _, err := os.Stat(public); os.IsNotExist(err) { + if err = os.MkdirAll(public, 0700); err != nil { + return nil, errs.FileError(err, public) + } + } + if _, err := os.Stat(private); os.IsNotExist(err) { + if err = os.MkdirAll(private, 0700); err != nil { + return nil, errs.FileError(err, private) + } + } + if len(config) > 0 { + if _, err := os.Stat(config); os.IsNotExist(err) { + if err = os.MkdirAll(config, 0700); err != nil { + return nil, errs.FileError(err, config) + } + } + } + + // 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) + } + + var err error + p := &PKI{ + provisioner: "step-cli", + address: "127.0.0.1:9000", + dnsNames: []string{"127.0.0.1"}, + } + if p.root, err = getPath(public, "root_ca.crt"); err != nil { + return nil, err + } + if p.rootKey, err = getPath(private, "root_ca_key"); err != nil { + return nil, err + } + 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.sshHostPubKey, err = getPath(public, "ssh_host_key.pub"); err != nil { + return nil, err + } + if p.sshUserPubKey, err = getPath(public, "ssh_user_key.pub"); err != nil { + return nil, err + } + if p.sshHostKey, err = getPath(private, "ssh_host_key"); err != nil { + return nil, err + } + if p.sshUserKey, err = getPath(private, "ssh_user_key"); err != nil { + return nil, err + } + if len(config) > 0 { + if p.config, err = getPath(config, "ca.json"); err != nil { + return nil, err + } + if p.defaults, err = getPath(config, "defaults.json"); err != nil { + return nil, err + } + } + + 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.rootFingerprint +} + +// SetProvisioner sets the provisioner name of the OTT keys. +func (p *PKI) SetProvisioner(s string) { + p.provisioner = s +} + +// SetAddress sets the listening address of the CA. +func (p *PKI) SetAddress(s string) { + p.address = s +} + +// SetDNSNames sets the dns names of the CA. +func (p *PKI) SetDNSNames(s []string) { + p.dnsNames = s +} + +// SetCAURL sets the ca-url to use in the defaults.json. +func (p *PKI) SetCAURL(s string) { + p.caURL = s +} + +// 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 + } + + return nil +} + +// GenerateRootCertificate generates a root certificate with the given name. +func (p *PKI) GenerateRootCertificate(name string, pass []byte) (*x509.Certificate, interface{}, error) { + rootProfile, err := x509util.NewRootProfile(name) + if err != nil { + return nil, nil, err + } + + rootBytes, err := rootProfile.CreateWriteCertificate(p.root, p.rootKey, string(pass)) + if err != nil { + return nil, nil, err + } + + rootCrt, err := x509.ParseCertificate(rootBytes) + if err != nil { + return nil, nil, errors.Wrap(err, "error parsing root certificate") + } + + sum := sha256.Sum256(rootCrt.Raw) + p.rootFingerprint = strings.ToLower(hex.EncodeToString(sum[:])) + + return rootCrt, rootProfile.SubjectPrivateKey(), nil +} + +// WriteRootCertificate writes to disk the given certificate and key. +func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error { + if err := utils.WriteFile(p.root, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: rootCrt.Raw, + }), 0600); err != nil { + return err + } + + _, err := pemutil.Serialize(rootKey, pemutil.WithPassword([]byte(pass)), pemutil.ToFile(p.rootKey, 0600)) + if err != nil { + return err + } + return nil +} + +// GenerateIntermediateCertificate generates an intermediate certificate with +// the given name. +func (p *PKI) GenerateIntermediateCertificate(name string, rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error { + interProfile, err := x509util.NewIntermediateProfile(name, rootCrt, rootKey) + if err != nil { + return err + } + _, err = interProfile.CreateWriteCertificate(p.intermediate, p.intermediateKey, string(pass)) + return err +} + +// 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 { + var pubNames = []string{p.sshHostPubKey, p.sshUserPubKey} + var privNames = []string{p.sshHostKey, p.sshUserKey} + for i := 0; i < 2; i++ { + pub, priv, err := keys.GenerateDefaultKeyPair() + if err != nil { + return err + } + if _, ok := priv.(crypto.Signer); !ok { + return errors.Errorf("key of type %T is not a crypto.Signer", priv) + } + sshKey, err := ssh.NewPublicKey(pub) + if err != nil { + return errors.Wrapf(err, "error converting public key") + } + _, err = pemutil.Serialize(priv, pemutil.WithFilename(privNames[i]), pemutil.WithPassword(password)) + if err != nil { + return err + } + if err = utils.WriteFile(pubNames[i], ssh.MarshalAuthorizedKey(sshKey), 0600); err != nil { + return err + } + } + p.enableSSH = true + return nil +} + +func (p *PKI) askFeedback() { + ui.Println() + ui.Printf("\033[1mFEEDBACK\033[0m %s %s\n", + html.UnescapeString("&#"+strconv.Itoa(128525)+";"), + html.UnescapeString("&#"+strconv.Itoa(127867)+";")) + ui.Println(" The \033[1mstep\033[0m utility is not instrumented for usage statistics. It does not") + ui.Println(" phone home. But your feedback is extremely valuable. Any information you") + ui.Println(" can provide regarding how you’re 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://gitter.im/smallstep/community\033[0m.") +} + +// TellPKI outputs the locations of public and private keys generated +// generated for a new PKI. Generally this will consist of a root certificate +// and key and an intermediate certificate and key. +func (p *PKI) TellPKI() { + p.tellPKI() + p.askFeedback() +} + +func (p *PKI) tellPKI() { + ui.Println() + ui.PrintSelected("Root certificate", p.root) + ui.PrintSelected("Root private key", p.rootKey) + ui.PrintSelected("Root fingerprint", p.rootFingerprint) + ui.PrintSelected("Intermediate certificate", p.intermediate) + ui.PrintSelected("Intermediate private key", p.intermediateKey) + if p.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) + ui.PrintSelected("SSH host root private key", p.sshHostKey) + } +} + +type caDefaults struct { + CAUrl string `json:"ca-url"` + CAConfig string `json:"ca-config"` + Fingerprint string `json:"fingerprint"` + Root string `json:"root"` +} + +// Option is the type for modifiers over the auth config object. +type Option func(c *authority.Config) error + +// WithDefaultDB is a configuration modifier that adds a default DB stanza to +// the authority config. +func WithDefaultDB() Option { + return func(c *authority.Config) error { + c.DB = &db.Config{ + Type: "badger", + DataSource: GetDBPath(), + } + return nil + } +} + +// WithoutDB is a configuration modifier that adds a default DB stanza to +// the authority config. +func WithoutDB() Option { + return func(c *authority.Config) error { + c.DB = nil + return nil + } +} + +// GenerateConfig returns the step certificates configuration. +func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) { + key, err := p.ottPrivateKey.CompactSerialize() + if err != nil { + return nil, errors.Wrap(err, "error serializing private key") + } + + config := &authority.Config{ + Root: []string{p.root}, + FederatedRoots: []string{}, + IntermediateCert: p.intermediate, + IntermediateKey: p.intermediateKey, + Address: p.address, + DNSNames: p.dnsNames, + Logger: []byte(`{"format": "text"}`), + DB: &db.Config{ + Type: "badger", + DataSource: GetDBPath(), + }, + AuthorityConfig: &authority.AuthConfig{ + DisableIssuedAtCheck: false, + Provisioners: provisioner.List{ + &provisioner.JWK{Name: p.provisioner, Type: "jwk", Key: p.ottPublicKey, EncryptedKey: key}, + }, + }, + TLS: &tlsutil.TLSOptions{ + MinVersion: x509util.DefaultTLSMinVersion, + MaxVersion: x509util.DefaultTLSMaxVersion, + Renegotiation: x509util.DefaultTLSRenegotiation, + CipherSuites: x509util.DefaultTLSCipherSuites, + }, + } + if p.enableSSH { + config.SSH = &authority.SSHConfig{ + HostKey: p.sshHostKey, + UserKey: p.sshUserKey, + } + } + + // Apply configuration modifiers + for _, o := range opt { + if err = o(config); err != nil { + return nil, err + } + } + + return config, nil +} + +// Save stores the pki on a json file that will be used as the certificate +// authority configuration. +func (p *PKI) Save(opt ...Option) error { + p.tellPKI() + + config, err := p.GenerateConfig(opt...) + if err != nil { + return err + } + + b, err := json.MarshalIndent(config, "", " ") + if err != nil { + return errors.Wrapf(err, "error marshaling %s", p.config) + } + if err = utils.WriteFile(p.config, b, 0666); err != nil { + return errs.FileError(err, p.config) + } + + // Generate the CA URL. + if p.caURL == "" { + p.caURL = p.dnsNames[0] + var port string + _, port, err = net.SplitHostPort(p.address) + if err != nil { + return errors.Wrapf(err, "error parsing %s", p.address) + } + if port == "443" { + p.caURL = fmt.Sprintf("https://%s", p.caURL) + } else { + p.caURL = fmt.Sprintf("https://%s:%s", p.caURL, port) + } + } + + defaults := &caDefaults{ + Root: p.root, + CAConfig: p.config, + CAUrl: p.caURL, + Fingerprint: p.rootFingerprint, + } + b, err = json.MarshalIndent(defaults, "", " ") + if err != nil { + return errors.Wrapf(err, "error marshaling %s", p.defaults) + } + if err = utils.WriteFile(p.defaults, b, 0666); err != nil { + return errs.FileError(err, p.defaults) + } + + ui.PrintSelected("Default configuration", p.defaults) + ui.PrintSelected("Certificate Authority configuration", p.config) + if config.DB != nil { + ui.PrintSelected("Database", config.DB.DataSource) + } + ui.Println() + ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.") + + p.askFeedback() + + return nil +}