diff --git a/Gopkg.lock b/Gopkg.lock index c2db46e1..a842e25f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -207,10 +207,11 @@ [[projects]] branch = "master" - digest = "1:cc890dd647fc3b59a000b66a8473154a4d2823012417007c035b81dbfdfeff20" + digest = "1:c26dc5debe7fa23c636a116c336aa033f8d7be0d3464d7b363f4b3355dcccf4f" name = "github.com/smallstep/cli" packages = [ "command", + "command/version", "config", "crypto/keys", "crypto/pemutil", @@ -228,7 +229,7 @@ "utils", ] pruneopts = "UT" - revision = "66f7b458dcade67b577ae0ab5b7205b0d6f8a8f7" + revision = "eeecaac062cb548ee2ab7c7563bc3c2f2160f019" [[projects]] branch = "master" @@ -382,6 +383,8 @@ "github.com/rs/xid", "github.com/sirupsen/logrus", "github.com/smallstep/assert", + "github.com/smallstep/cli/command", + "github.com/smallstep/cli/command/version", "github.com/smallstep/cli/config", "github.com/smallstep/cli/crypto/keys", "github.com/smallstep/cli/crypto/pemutil", @@ -393,7 +396,9 @@ "github.com/smallstep/cli/pkg/x509", "github.com/smallstep/cli/token", "github.com/smallstep/cli/token/provision", + "github.com/smallstep/cli/ui", "github.com/smallstep/cli/usage", + "github.com/smallstep/cli/utils", "github.com/smallstep/nosql", "github.com/smallstep/nosql/database", "github.com/urfave/cli", diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 748d1b64..22b7905d 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -1,25 +1,22 @@ package main import ( - "bytes" "flag" "fmt" "html" - "io/ioutil" "log" + "math/rand" "net/http" "os" "reflect" "regexp" - "runtime" "strconv" "time" - "unicode" - "github.com/pkg/errors" - "github.com/smallstep/certificates/authority" - "github.com/smallstep/certificates/ca" - "github.com/smallstep/cli/errs" + "github.com/smallstep/certificates/commands" + "github.com/smallstep/cli/command" + "github.com/smallstep/cli/command/version" + "github.com/smallstep/cli/config" "github.com/smallstep/cli/usage" "github.com/urfave/cli" ) @@ -30,30 +27,9 @@ var ( Version = "N/A" ) -// Version returns the current version of the binary. -func version() string { - out := Version - if out == "N/A" { - out = "0000000-dev" - } - return fmt.Sprintf("Smallstep CA/%s (%s/%s)", - out, runtime.GOOS, runtime.GOARCH) -} - -// ReleaseDate returns the time of when the binary was built. -func releaseDate() string { - out := BuildTime - if out == "N/A" { - out = time.Now().UTC().Format("2006-01-02 15:04 MST") - } - - return out -} - -// Print version and release date. -func printFullVersion() { - fmt.Printf("%s\n", version()) - fmt.Printf("Release Date: %s\n", releaseDate()) +func init() { + config.Set("Smallstep CA", Version, BuildTime) + rand.Seed(time.Now().UnixNano()) } // appHelpTemplate contains the modified template for the main app @@ -111,7 +87,7 @@ Please send us a sentence or two, good or bad: **feedback@smallstep.com** or joi func main() { // Override global framework components cli.VersionPrinter = func(c *cli.Context) { - printFullVersion() + version.Command(c) } cli.AppHelpTemplate = appHelpTemplate cli.SubcommandHelpTemplate = usage.SubcommandHelpTemplate @@ -119,13 +95,14 @@ func main() { cli.HelpPrinter = usage.HelpPrinter cli.FlagNamePrefixer = usage.FlagNamePrefixer cli.FlagStringer = stringifyFlag + // Configure cli app app := cli.NewApp() app.Name = "step-ca" app.HelpName = "step-ca" - app.Version = version() + app.Version = config.Version() app.Usage = "an online certificate authority for secure automated certificate management" - app.UsageText = `**step-ca** [**--password-file**=] [**--version**]` + app.UsageText = `**step-ca** [**--password-file**=] [**--help**] [**--version**]` app.Description = `**step-ca** runs the Step Online Certificate Authority (Step CA) using the given configuration. @@ -157,36 +134,14 @@ automating deployment: ''' $ step-ca $STEPPATH/config/ca.json --password-file ./password.txt '''` - app.Flags = append(app.Flags, []cli.Flag{ - cli.StringFlag{ - Name: "password-file", - Usage: `path to the containing the password to decrypt the -intermediate private key.`, - }, - }...) + app.Flags = append(app.Flags, commands.AppCommand.Flags...) + app.Flags = append(app.Flags, cli.HelpFlag) app.Copyright = "(c) 2019 Smallstep Labs, Inc." // All non-successful output should be written to stderr app.Writer = os.Stdout app.ErrWriter = os.Stderr - app.Commands = []cli.Command{ - { - Name: "version", - Usage: "Displays the current version of step-ca", - // Command prints out the current version of the tool - Action: func(c *cli.Context) error { - printFullVersion() - return nil - }, - }, - { - Name: "help", - Aliases: []string{"h"}, - Usage: "displays help for the specified command or command group", - ArgsUsage: "", - Action: usage.HelpCommandAction, - }, - } + app.Commands = command.Retrieve() // Start the golang debug logger if environment variable is set. // See https://golang.org/pkg/net/http/pprof/ @@ -199,11 +154,10 @@ intermediate private key.`, app.Action = func(_ *cli.Context) error { // Hack to be able to run a the top action as a subcommand - cmd := cli.Command{Name: "start", Action: startAction, Flags: app.Flags} set := flag.NewFlagSet(app.Name, flag.ContinueOnError) set.Parse(os.Args) ctx := cli.NewContext(app, set, nil) - return cmd.Run(ctx) + return commands.AppCommand.Run(ctx) } if err := app.Run(os.Args); err != nil { @@ -216,55 +170,6 @@ intermediate private key.`, } } -func startAction(ctx *cli.Context) error { - passFile := ctx.String("password-file") - - // If zero cmd line args show help, if >1 cmd line args show error. - if ctx.NArg() == 0 { - return cli.ShowAppHelp(ctx) - } - if err := errs.NumberOfArguments(ctx, 1); err != nil { - return err - } - - configFile := ctx.Args().Get(0) - config, err := authority.LoadConfiguration(configFile) - if err != nil { - fatal(err) - } - - var password []byte - if passFile != "" { - if password, err = ioutil.ReadFile(passFile); err != nil { - fatal(errors.Wrapf(err, "error reading %s", passFile)) - } - password = bytes.TrimRightFunc(password, unicode.IsSpace) - } - - srv, err := ca.New(config, ca.WithConfigFile(configFile), ca.WithPassword(password)) - if err != nil { - fatal(err) - } - - go ca.StopReloaderHandler(srv) - if err = srv.Run(); err != nil && err != http.ErrServerClosed { - fatal(err) - } - return nil -} - -// fatal writes the passed error on the standard error and exits with the exit -// code 1. If the environment variable STEPDEBUG is set to 1 it shows the -// stack trace of the error. -func fatal(err error) { - if os.Getenv("STEPDEBUG") == "1" { - fmt.Fprintf(os.Stderr, "%+v\n", err) - } else { - fmt.Fprintln(os.Stderr, err) - } - os.Exit(2) -} - func flagValue(f cli.Flag) reflect.Value { fv := reflect.ValueOf(f) for fv.Kind() == reflect.Ptr { diff --git a/commands/app.go b/commands/app.go new file mode 100644 index 00000000..36155bd9 --- /dev/null +++ b/commands/app.go @@ -0,0 +1,81 @@ +package commands + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "os" + "unicode" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli/errs" + "github.com/urfave/cli" +) + +// AppCommand is the action used as the top action. +var AppCommand = cli.Command{ + Name: "start", + Action: appAction, + UsageText: `**step-ca** + [**--password-file**=]`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "password-file", + Usage: `path to the containing the password to decrypt the +intermediate private key.`, + }, + }, +} + +// AppAction is the action used when the top command runs. +func appAction(ctx *cli.Context) error { + passFile := ctx.String("password-file") + + // If zero cmd line args show help, if >1 cmd line args show error. + if ctx.NArg() == 0 { + return cli.ShowAppHelp(ctx) + } + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + configFile := ctx.Args().Get(0) + config, err := authority.LoadConfiguration(configFile) + if err != nil { + fatal(err) + } + + var password []byte + if passFile != "" { + if password, err = ioutil.ReadFile(passFile); err != nil { + fatal(errors.Wrapf(err, "error reading %s", passFile)) + } + password = bytes.TrimRightFunc(password, unicode.IsSpace) + } + + srv, err := ca.New(config, ca.WithConfigFile(configFile), ca.WithPassword(password)) + if err != nil { + fatal(err) + } + + go ca.StopReloaderHandler(srv) + if err = srv.Run(); err != nil && err != http.ErrServerClosed { + fatal(err) + } + return nil +} + +// fatal writes the passed error on the standard error and exits with the exit +// code 1. If the environment variable STEPDEBUG is set to 1 it shows the +// stack trace of the error. +func fatal(err error) { + if os.Getenv("STEPDEBUG") == "1" { + fmt.Fprintf(os.Stderr, "%+v\n", err) + } else { + fmt.Fprintln(os.Stderr, err) + } + os.Exit(2) +} diff --git a/commands/onboard.go b/commands/onboard.go new file mode 100644 index 00000000..cc4a1eef --- /dev/null +++ b/commands/onboard.go @@ -0,0 +1,212 @@ +package commands + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" + "os" + + "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/randutil" + "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/ui" + "github.com/smallstep/cli/utils" + "github.com/urfave/cli" +) + +// defaultOnboardingURL is the production onboarding url, to use a development +// url use: +// export STEP_CA_ONBOARDING_URL=http://localhost:3002/onboarding/ +const defaultOnboardingURL = "https://api.smallstep.com/onboarding/" + +type onboardingConfiguration struct { + Name string `json:"name"` + DNS string `json:"dns"` + Address string `json:"address"` + password []byte +} + +type onboardingPayload struct { + Fingerprint string `json:"fingerprint"` +} + +type onboardingError struct { + StatusCode int `json:"statusCode"` + Message string `json:"message"` +} + +func (e onboardingError) Error() string { + return e.Message +} + +func init() { + command.Register(cli.Command{ + Name: "onboard", + Usage: "configure and run step-ca from the onboarding guide", + UsageText: "**step-ca onboard** ", + Action: onboardAction, + Description: `**step-ca onboard** configures step certificates using the onboarding guide. + +Open https://smallstep.com/onboarding in your browser and start the CA with the +given token: +''' +$ step-ca onboard +''' + +## POSITIONAL ARGUMENTS + + +: The token string provided by the onboarding guide.`, + }) +} + +func onboardAction(ctx *cli.Context) error { + if ctx.NArg() == 0 { + return cli.ShowCommandHelp(ctx, "onboard") + } + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + // Get onboarding url + onboarding := defaultOnboardingURL + if v := os.Getenv("STEP_CA_ONBOARDING_URL"); v != "" { + onboarding = v + } + + u, err := url.Parse(onboarding) + if err != nil { + return errors.Wrapf(err, "error parsing %s", onboarding) + } + + ui.Println("Connecting to onboarding guide...") + + token := ctx.Args().Get(0) + onboardingURL := u.ResolveReference(&url.URL{Path: token}).String() + + res, err := http.Get(onboardingURL) + if err != nil { + return errors.Wrap(err, "error connecting onboarding guide") + } + if res.StatusCode >= 400 { + var msg onboardingError + if err := readJSON(res.Body, &msg); err != nil { + return errors.Wrap(err, "error unmarshaling response") + } + return errors.Wrap(msg, "error receiving onboarding guide") + } + + var config onboardingConfiguration + if err := readJSON(res.Body, &config); err != nil { + return errors.Wrap(err, "error unmarshaling response") + } + + password, err := randutil.ASCII(32) + if err != nil { + return err + } + config.password = []byte(password) + + ui.Println("Initializing step-ca with the following configuration:") + ui.PrintSelected("Name", config.Name) + ui.PrintSelected("DNS", config.DNS) + ui.PrintSelected("Address", config.Address) + ui.PrintSelected("Password", password) + ui.Println() + + caConfig, fp, err := onboardPKI(config) + if err != nil { + return err + } + + payload, err := json.Marshal(onboardingPayload{Fingerprint: fp}) + if err != nil { + return errors.Wrap(err, "error marshaling payload") + } + + resp, err := http.Post(onboardingURL, "application/json", bytes.NewBuffer(payload)) + if err != nil { + return errors.Wrap(err, "error connecting onboarding guide") + } + if resp.StatusCode >= 400 { + var msg onboardingError + if err := readJSON(resp.Body, &msg); err != nil { + ui.Printf("%s {{ \"error unmarshalling response: %v\" | yellow }}\n", ui.IconWarn, err) + } else { + ui.Printf("%s {{ \"error posting fingerprint: %s\" | yellow }}\n", ui.IconWarn, msg.Message) + } + } else { + resp.Body.Close() + } + + ui.Println("Initialized!") + ui.Println("Step CA is starting. Please return to the onboarding guide in your browser to continue.") + + srv, err := ca.New(caConfig, ca.WithPassword(config.password)) + if err != nil { + fatal(err) + } + + go ca.StopReloaderHandler(srv) + if err = srv.Run(); err != nil && err != http.ErrServerClosed { + fatal(err) + } + + return nil +} + +func onboardPKI(config onboardingConfiguration) (*authority.Config, string, error) { + p, err := pki.New(pki.GetPublicPath(), pki.GetSecretsPath(), pki.GetConfigPath()) + if err != nil { + return nil, "", err + } + + p.SetAddress(config.Address) + p.SetDNSNames([]string{config.DNS}) + + ui.Println("Generating root certificate...") + rootCrt, rootKey, err := p.GenerateRootCertificate(config.Name+" Root CA", config.password) + if err != nil { + return nil, "", err + } + + ui.Println("Generating intermediate certificate...") + err = p.GenerateIntermediateCertificate(config.Name+" Intermediate CA", rootCrt, rootKey, config.password) + if err != nil { + return nil, "", err + } + + // Generate provisioner + p.SetProvisioner("admin") + ui.Println("Generating admin provisioner...") + if err = p.GenerateKeyPairs(config.password); err != nil { + return nil, "", err + } + + // Generate and write configuration + caConfig, err := p.GenerateConfig() + if err != nil { + return nil, "", err + } + + b, err := json.MarshalIndent(caConfig, "", " ") + if err != nil { + return nil, "", errors.Wrapf(err, "error marshaling %s", p.GetCAConfigPath()) + } + if err = utils.WriteFile(p.GetCAConfigPath(), b, 0666); err != nil { + return nil, "", errs.FileError(err, p.GetCAConfigPath()) + } + + return caConfig, p.GetRootFingerprint(), nil +} + +func readJSON(r io.ReadCloser, v interface{}) error { + defer r.Close() + return json.NewDecoder(r).Decode(v) +} diff --git a/pki/pki.go b/pki/pki.go new file mode 100644 index 00000000..56f3dbbd --- /dev/null +++ b/pki/pki.go @@ -0,0 +1,515 @@ +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") + } + + 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, + }, + } + 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() + + 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 +}