Merge pull request #101 from smallstep/onboarding

Onboarding flow
This commit is contained in:
Mariano Cano 2019-09-26 17:34:15 -07:00 committed by GitHub
commit effb490d27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 831 additions and 113 deletions

9
Gopkg.lock generated
View file

@ -207,10 +207,11 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:cc890dd647fc3b59a000b66a8473154a4d2823012417007c035b81dbfdfeff20" digest = "1:c26dc5debe7fa23c636a116c336aa033f8d7be0d3464d7b363f4b3355dcccf4f"
name = "github.com/smallstep/cli" name = "github.com/smallstep/cli"
packages = [ packages = [
"command", "command",
"command/version",
"config", "config",
"crypto/keys", "crypto/keys",
"crypto/pemutil", "crypto/pemutil",
@ -228,7 +229,7 @@
"utils", "utils",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "66f7b458dcade67b577ae0ab5b7205b0d6f8a8f7" revision = "eeecaac062cb548ee2ab7c7563bc3c2f2160f019"
[[projects]] [[projects]]
branch = "master" branch = "master"
@ -382,6 +383,8 @@
"github.com/rs/xid", "github.com/rs/xid",
"github.com/sirupsen/logrus", "github.com/sirupsen/logrus",
"github.com/smallstep/assert", "github.com/smallstep/assert",
"github.com/smallstep/cli/command",
"github.com/smallstep/cli/command/version",
"github.com/smallstep/cli/config", "github.com/smallstep/cli/config",
"github.com/smallstep/cli/crypto/keys", "github.com/smallstep/cli/crypto/keys",
"github.com/smallstep/cli/crypto/pemutil", "github.com/smallstep/cli/crypto/pemutil",
@ -393,7 +396,9 @@
"github.com/smallstep/cli/pkg/x509", "github.com/smallstep/cli/pkg/x509",
"github.com/smallstep/cli/token", "github.com/smallstep/cli/token",
"github.com/smallstep/cli/token/provision", "github.com/smallstep/cli/token/provision",
"github.com/smallstep/cli/ui",
"github.com/smallstep/cli/usage", "github.com/smallstep/cli/usage",
"github.com/smallstep/cli/utils",
"github.com/smallstep/nosql", "github.com/smallstep/nosql",
"github.com/smallstep/nosql/database", "github.com/smallstep/nosql/database",
"github.com/urfave/cli", "github.com/urfave/cli",

View file

@ -1,25 +1,22 @@
package main package main
import ( import (
"bytes"
"flag" "flag"
"fmt" "fmt"
"html" "html"
"io/ioutil"
"log" "log"
"math/rand"
"net/http" "net/http"
"os" "os"
"reflect" "reflect"
"regexp" "regexp"
"runtime"
"strconv" "strconv"
"time" "time"
"unicode"
"github.com/pkg/errors" "github.com/smallstep/certificates/commands"
"github.com/smallstep/certificates/authority" "github.com/smallstep/cli/command"
"github.com/smallstep/certificates/ca" "github.com/smallstep/cli/command/version"
"github.com/smallstep/cli/errs" "github.com/smallstep/cli/config"
"github.com/smallstep/cli/usage" "github.com/smallstep/cli/usage"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -30,30 +27,9 @@ var (
Version = "N/A" Version = "N/A"
) )
// Version returns the current version of the binary. func init() {
func version() string { config.Set("Smallstep CA", Version, BuildTime)
out := Version rand.Seed(time.Now().UnixNano())
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())
} }
// appHelpTemplate contains the modified template for the main app // 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() { func main() {
// Override global framework components // Override global framework components
cli.VersionPrinter = func(c *cli.Context) { cli.VersionPrinter = func(c *cli.Context) {
printFullVersion() version.Command(c)
} }
cli.AppHelpTemplate = appHelpTemplate cli.AppHelpTemplate = appHelpTemplate
cli.SubcommandHelpTemplate = usage.SubcommandHelpTemplate cli.SubcommandHelpTemplate = usage.SubcommandHelpTemplate
@ -119,13 +95,14 @@ func main() {
cli.HelpPrinter = usage.HelpPrinter cli.HelpPrinter = usage.HelpPrinter
cli.FlagNamePrefixer = usage.FlagNamePrefixer cli.FlagNamePrefixer = usage.FlagNamePrefixer
cli.FlagStringer = stringifyFlag cli.FlagStringer = stringifyFlag
// Configure cli app // Configure cli app
app := cli.NewApp() app := cli.NewApp()
app.Name = "step-ca" app.Name = "step-ca"
app.HelpName = "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.Usage = "an online certificate authority for secure automated certificate management"
app.UsageText = `**step-ca** <config> [**--password-file**=<file>] [**--version**]` app.UsageText = `**step-ca** <config> [**--password-file**=<file>] [**--help**] [**--version**]`
app.Description = `**step-ca** runs the Step Online Certificate Authority app.Description = `**step-ca** runs the Step Online Certificate Authority
(Step CA) using the given configuration. (Step CA) using the given configuration.
@ -157,36 +134,14 @@ automating deployment:
''' '''
$ step-ca $STEPPATH/config/ca.json --password-file ./password.txt $ step-ca $STEPPATH/config/ca.json --password-file ./password.txt
'''` '''`
app.Flags = append(app.Flags, []cli.Flag{ app.Flags = append(app.Flags, commands.AppCommand.Flags...)
cli.StringFlag{ app.Flags = append(app.Flags, cli.HelpFlag)
Name: "password-file",
Usage: `path to the <file> containing the password to decrypt the
intermediate private key.`,
},
}...)
app.Copyright = "(c) 2019 Smallstep Labs, Inc." app.Copyright = "(c) 2019 Smallstep Labs, Inc."
// All non-successful output should be written to stderr // All non-successful output should be written to stderr
app.Writer = os.Stdout app.Writer = os.Stdout
app.ErrWriter = os.Stderr app.ErrWriter = os.Stderr
app.Commands = []cli.Command{ app.Commands = command.Retrieve()
{
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,
},
}
// Start the golang debug logger if environment variable is set. // Start the golang debug logger if environment variable is set.
// See https://golang.org/pkg/net/http/pprof/ // See https://golang.org/pkg/net/http/pprof/
@ -199,11 +154,10 @@ intermediate private key.`,
app.Action = func(_ *cli.Context) error { app.Action = func(_ *cli.Context) error {
// Hack to be able to run a the top action as a subcommand // 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 := flag.NewFlagSet(app.Name, flag.ContinueOnError)
set.Parse(os.Args) set.Parse(os.Args)
ctx := cli.NewContext(app, set, nil) ctx := cli.NewContext(app, set, nil)
return cmd.Run(ctx) return commands.AppCommand.Run(ctx)
} }
if err := app.Run(os.Args); err != nil { 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 { func flagValue(f cli.Flag) reflect.Value {
fv := reflect.ValueOf(f) fv := reflect.ValueOf(f)
for fv.Kind() == reflect.Ptr { for fv.Kind() == reflect.Ptr {

81
commands/app.go Normal file
View file

@ -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** <config>
[**--password-file**=<file>]`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "password-file",
Usage: `path to the <file> 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)
}

212
commands/onboard.go Normal file
View file

@ -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** <token>",
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 <token>
'''
## POSITIONAL ARGUMENTS
<token>
: 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)
}

515
pki/pki.go Normal file
View file

@ -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 youre using `step` helps. Please send us a")
ui.Println(" sentence or two, good or bad: \033[1mfeedback@smallstep.com\033[0m or join")
ui.Println(" \033[1mhttps://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
}