forked from TrueCloudLab/certificates
commit
effb490d27
5 changed files with 831 additions and 113 deletions
9
Gopkg.lock
generated
9
Gopkg.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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** <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
|
||||
(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 <file> 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 {
|
||||
|
|
81
commands/app.go
Normal file
81
commands/app.go
Normal 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
212
commands/onboard.go
Normal 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
515
pki/pki.go
Normal 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 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
|
||||
}
|
Loading…
Reference in a new issue