d1f78cf6d2
If a root certificate is provided to init an authority the fingerprint is not currently stored in the default.json file. This patch simply stores the fingerprint of the supplied certificate.
533 lines
15 KiB
Go
533 lines
15 KiB
Go
package pki
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"html"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
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 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")
|
|
}
|
|
|
|
// GetTemplatesPath returns the path where the templates are stored.
|
|
func GetTemplatesPath() string {
|
|
return filepath.Join(config.StepPath(), templatesPath)
|
|
}
|
|
|
|
// 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() (*PKI, error) {
|
|
public := GetPublicPath()
|
|
private := GetSecretsPath()
|
|
config := GetConfigPath()
|
|
|
|
// Create directories
|
|
dirs := []string{public, private, config, GetTemplatesPath()}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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_ca_key.pub"); err != nil {
|
|
return nil, err
|
|
}
|
|
if p.sshUserPubKey, err = getPath(public, "ssh_user_ca_key.pub"); err != nil {
|
|
return nil, err
|
|
}
|
|
if p.sshHostKey, err = getPath(private, "ssh_host_ca_key"); err != nil {
|
|
return nil, err
|
|
}
|
|
if p.sshUserKey, err = getPath(private, "ssh_user_ca_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
|
|
}
|
|
|
|
sum := sha256.Sum256(rootCrt.Raw)
|
|
p.rootFingerprint = strings.ToLower(hex.EncodeToString(sum[:]))
|
|
|
|
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")
|
|
}
|
|
|
|
prov := &provisioner.JWK{
|
|
Name: p.provisioner,
|
|
Type: "JWK",
|
|
Key: p.ottPublicKey,
|
|
EncryptedKey: 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{prov},
|
|
},
|
|
TLS: &tlsutil.TLSOptions{
|
|
MinVersion: x509util.DefaultTLSMinVersion,
|
|
MaxVersion: x509util.DefaultTLSMaxVersion,
|
|
Renegotiation: x509util.DefaultTLSRenegotiation,
|
|
CipherSuites: x509util.DefaultTLSCipherSuites,
|
|
},
|
|
Templates: p.getTemplates(),
|
|
}
|
|
if p.enableSSH {
|
|
enableSSHCA := true
|
|
config.SSH = &authority.SSHConfig{
|
|
HostKey: p.sshHostKey,
|
|
UserKey: p.sshUserKey,
|
|
}
|
|
prov.Claims = &provisioner.Claims{
|
|
EnableSSHCA: &enableSSHCA,
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
|
|
// Generate and write ca.json
|
|
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, 0644); 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)
|
|
}
|
|
}
|
|
|
|
// Generate and write defaults.json
|
|
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, 0644); err != nil {
|
|
return errs.FileError(err, p.defaults)
|
|
}
|
|
|
|
// Generate and write templates
|
|
if err := generateTemplates(config.Templates); err != nil {
|
|
return err
|
|
}
|
|
|
|
if config.DB != nil {
|
|
ui.PrintSelected("Database folder", config.DB.DataSource)
|
|
}
|
|
if config.Templates != nil {
|
|
ui.PrintSelected("Templates folder", GetTemplatesPath())
|
|
}
|
|
|
|
ui.PrintSelected("Default configuration", p.defaults)
|
|
ui.PrintSelected("Certificate Authority configuration", p.config)
|
|
ui.Println()
|
|
ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.")
|
|
|
|
p.askFeedback()
|
|
|
|
return nil
|
|
}
|