From 15f2935db11c257e8b43a662203e7b77f2801291 Mon Sep 17 00:00:00 2001 From: Alan Christopher Thomas Date: Tue, 10 Sep 2019 16:29:03 -0600 Subject: [PATCH 01/17] Rough wiring for basics of connecting to onboarding flow --- cmd/step-ca/main.go | 71 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 748d1b64..ebb21830 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/json" "flag" "fmt" "html" @@ -24,6 +25,15 @@ import ( "github.com/urfave/cli" ) +type config struct { + Name string `json:"name"` + DNS string `json:"dns"` + Address string `json:"address"` +} +type onboardingPayload struct { + Fingerprint string `json:"fingerprint"` +} + // commit and buildTime are filled in during build by the Makefile var ( BuildTime = "N/A" @@ -179,6 +189,67 @@ intermediate private key.`, return nil }, }, + { + Name: "start", + Usage: "Starts step-ca with the (optional) specified configuration", + // TODO this should accept an optional config parameter that defaults to ~/.step/config/ca.json + // as well as an optional token parameter for connecting to the onboarding flow + Action: func(c *cli.Context) error { + fmt.Printf("Connecting to onboarding guide...\n\n") + + token := c.Args().Get(0) + + res, err := http.Get("http://localhost:3002/onboarding/" + token) + if err != nil { + log.Fatal(err) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Fatal(err) + } + + configuration := config{} + err = json.Unmarshal(body, &configuration) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Connected! Initializing step-ca with the following configuration...\n\n") + fmt.Printf("Name: %s\n", configuration.Name) + fmt.Printf("DNS: %s\n", configuration.DNS) + fmt.Printf("Address: %s\n", configuration.Address) + // TODO generate this password + fmt.Printf("Provisioner Password: abcdef1234567890\n\n") + + // TODO actually initialize the CA config and start listening + // TODO get the root cert fingerprint to post back to the onboarding guide + payload, err := json.Marshal(onboardingPayload{Fingerprint: "foobarbatbaz"}) + req, err := http.NewRequest("POST", "http://localhost:3002/onboarding/" + token, bytes.NewBuffer(payload)) + req.Header.Set("Content-Type", "application/json") + if err != nil { + log.Fatal(err) + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + resp.Body.Close() + + fmt.Printf("Initialized!\n") + fmt.Printf("Step CA has been started. Please return to the onboarding guide in your browser to continue.\n") + for { + time.Sleep(1 * time.Second); + } + return nil + }, + }, { Name: "help", Aliases: []string{"h"}, From 21baa69473d7dda26f0b647987624788847dbd47 Mon Sep 17 00:00:00 2001 From: Alan Christopher Thomas Date: Tue, 10 Sep 2019 22:56:19 -0600 Subject: [PATCH 02/17] Fix linting errors and remove useless code --- cmd/step-ca/main.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index ebb21830..a742565f 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -26,8 +26,8 @@ import ( ) type config struct { - Name string `json:"name"` - DNS string `json:"dns"` + Name string `json:"name"` + DNS string `json:"dns"` Address string `json:"address"` } type onboardingPayload struct { @@ -225,7 +225,11 @@ intermediate private key.`, // TODO actually initialize the CA config and start listening // TODO get the root cert fingerprint to post back to the onboarding guide payload, err := json.Marshal(onboardingPayload{Fingerprint: "foobarbatbaz"}) - req, err := http.NewRequest("POST", "http://localhost:3002/onboarding/" + token, bytes.NewBuffer(payload)) + if err != nil { + log.Fatal(err) + } + + req, err := http.NewRequest("POST", "http://localhost:3002/onboarding/"+token, bytes.NewBuffer(payload)) req.Header.Set("Content-Type", "application/json") if err != nil { log.Fatal(err) @@ -236,18 +240,13 @@ intermediate private key.`, if err != nil { log.Fatal(err) } - body, err = ioutil.ReadAll(resp.Body) - if err != nil { - log.Fatal(err) - } resp.Body.Close() fmt.Printf("Initialized!\n") fmt.Printf("Step CA has been started. Please return to the onboarding guide in your browser to continue.\n") for { - time.Sleep(1 * time.Second); + time.Sleep(1 * time.Second) } - return nil }, }, { From 7c0622e50e491163c1f135d8151d8f27aa661c67 Mon Sep 17 00:00:00 2001 From: Alan Christopher Thomas Date: Tue, 10 Sep 2019 22:56:30 -0600 Subject: [PATCH 03/17] Make note about adding "admin" JWT provisioner --- cmd/step-ca/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index a742565f..62463b14 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -222,7 +222,8 @@ intermediate private key.`, // TODO generate this password fmt.Printf("Provisioner Password: abcdef1234567890\n\n") - // TODO actually initialize the CA config and start listening + // TODO actually initialize the CA config (automatically add an "admin" JWT provisioner) + // and start listening // TODO get the root cert fingerprint to post back to the onboarding guide payload, err := json.Marshal(onboardingPayload{Fingerprint: "foobarbatbaz"}) if err != nil { From c0d1399c387655fe74ad0f56b3136825f16f4df5 Mon Sep 17 00:00:00 2001 From: Alan Christopher Thomas Date: Wed, 11 Sep 2019 14:54:39 -0600 Subject: [PATCH 04/17] Change onboarding bootstrap command to step-ca onboard cc @sourishkrout @maraino --- cmd/step-ca/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 62463b14..2110be40 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -190,8 +190,8 @@ intermediate private key.`, }, }, { - Name: "start", - Usage: "Starts step-ca with the (optional) specified configuration", + Name: "onboard", + Usage: "Configure and run step-ca from the onboarding guide", // TODO this should accept an optional config parameter that defaults to ~/.step/config/ca.json // as well as an optional token parameter for connecting to the onboarding flow Action: func(c *cli.Context) error { From 0c654d93eac4d0e5dad38f7b015575995541b75a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Sep 2019 17:33:27 -0700 Subject: [PATCH 05/17] Create method for onboard action and clean code. --- cmd/step-ca/main.go | 142 +++++++++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 60 deletions(-) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 2110be40..8bd8ff7c 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "log" "net/http" + "net/url" "os" "reflect" "regexp" @@ -17,6 +18,8 @@ import ( "time" "unicode" + "github.com/smallstep/cli/crypto/randutil" + "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/ca" @@ -25,7 +28,7 @@ import ( "github.com/urfave/cli" ) -type config struct { +type onboardingConfiguration struct { Name string `json:"name"` DNS string `json:"dns"` Address string `json:"address"` @@ -190,65 +193,10 @@ intermediate private key.`, }, }, { - Name: "onboard", - Usage: "Configure and run step-ca from the onboarding guide", - // TODO this should accept an optional config parameter that defaults to ~/.step/config/ca.json - // as well as an optional token parameter for connecting to the onboarding flow - Action: func(c *cli.Context) error { - fmt.Printf("Connecting to onboarding guide...\n\n") - - token := c.Args().Get(0) - - res, err := http.Get("http://localhost:3002/onboarding/" + token) - if err != nil { - log.Fatal(err) - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Fatal(err) - } - - configuration := config{} - err = json.Unmarshal(body, &configuration) - if err != nil { - log.Fatal(err) - } - - fmt.Printf("Connected! Initializing step-ca with the following configuration...\n\n") - fmt.Printf("Name: %s\n", configuration.Name) - fmt.Printf("DNS: %s\n", configuration.DNS) - fmt.Printf("Address: %s\n", configuration.Address) - // TODO generate this password - fmt.Printf("Provisioner Password: abcdef1234567890\n\n") - - // TODO actually initialize the CA config (automatically add an "admin" JWT provisioner) - // and start listening - // TODO get the root cert fingerprint to post back to the onboarding guide - payload, err := json.Marshal(onboardingPayload{Fingerprint: "foobarbatbaz"}) - if err != nil { - log.Fatal(err) - } - - req, err := http.NewRequest("POST", "http://localhost:3002/onboarding/"+token, bytes.NewBuffer(payload)) - req.Header.Set("Content-Type", "application/json") - if err != nil { - log.Fatal(err) - } - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - log.Fatal(err) - } - resp.Body.Close() - - fmt.Printf("Initialized!\n") - fmt.Printf("Step CA has been started. Please return to the onboarding guide in your browser to continue.\n") - for { - time.Sleep(1 * time.Second) - } - }, + Name: "onboard", + Usage: "Configure and run step-ca from the onboarding guide", + UsageText: "**step-ca onboard** ", + Action: onboardAction, }, { Name: "help", @@ -324,6 +272,80 @@ func startAction(ctx *cli.Context) error { return nil } +func onboardAction(ctx *cli.Context) error { + if ctx.NArg() == 0 { + return cli.ShowAppHelp(ctx) + } + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + // Get onboarding url + onboarding := "http://localhost:3002/onboarding/" + 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) + } + + fmt.Printf("Connecting to onboarding guide...\n\n") + + token := ctx.Args().Get(0) + onboardingURL := u.ResolveReference(&url.URL{Path: token}).String() + + res, err := http.Get(onboardingURL) + if err != nil { + return errors.Wrapf(err, "http GET %s failed", onboardingURL) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return errors.Wrap(err, "error reading response") + } + + var config onboardingConfiguration + err = json.Unmarshal(body, &config) + if err != nil { + return errors.Wrap(err, "error unmarshaling response") + } + + password, err := randutil.ASCII(32) + if err != nil { + return err + } + + fmt.Printf("Connected! Initializing step-ca with the following configuration...\n\n") + fmt.Printf("Name: %s\n", config.Name) + fmt.Printf("DNS: %s\n", config.DNS) + fmt.Printf("Address: %s\n", config.Address) + fmt.Printf("Provisioner Password: %s\n\n", password) + + // TODO actually initialize the CA config (automatically add an "admin" JWT provisioner) + // and start listening + // TODO get the root cert fingerprint to post back to the onboarding guide + payload, err := json.Marshal(onboardingPayload{Fingerprint: "foobarbatbaz"}) + if err != nil { + return errors.Wrap(err, "error marshalling payload") + } + + resp, err := http.Post(onboardingURL, "application/json", bytes.NewBuffer(payload)) + if err != nil { + return errors.Wrapf(err, "http POST %s failed", onboardingURL) + } + resp.Body.Close() + + fmt.Printf("Initialized!\n") + fmt.Printf("Step CA has been started. Please return to the onboarding guide in your browser to continue.\n") + for { + time.Sleep(1 * time.Second) + } + + 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. From bca5dcc3264cdfca01b20532a99b35f4487487e0 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Sep 2019 17:36:48 -0700 Subject: [PATCH 06/17] Remove url from error message. --- cmd/step-ca/main.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 8bd8ff7c..25dd7008 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -298,7 +298,7 @@ func onboardAction(ctx *cli.Context) error { res, err := http.Get(onboardingURL) if err != nil { - return errors.Wrapf(err, "http GET %s failed", onboardingURL) + return errors.Wrap(err, "error connecting onboarding guide") } body, err := ioutil.ReadAll(res.Body) @@ -333,7 +333,7 @@ func onboardAction(ctx *cli.Context) error { resp, err := http.Post(onboardingURL, "application/json", bytes.NewBuffer(payload)) if err != nil { - return errors.Wrapf(err, "http POST %s failed", onboardingURL) + return errors.Wrap(err, "error connecting onboarding guide") } resp.Body.Close() @@ -342,8 +342,6 @@ func onboardAction(ctx *cli.Context) error { for { time.Sleep(1 * time.Second) } - - return nil } // fatal writes the passed error on the standard error and exits with the exit From 0efae31a29dd13af9eec7593569a96e8c21703cb Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 11 Sep 2019 19:16:08 -0700 Subject: [PATCH 07/17] Generate PKI and start server using onboarding. --- cmd/step-ca/main.go | 85 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 25dd7008..8d24b1c6 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -18,20 +18,22 @@ import ( "time" "unicode" - "github.com/smallstep/cli/crypto/randutil" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli/crypto/pki" + "github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/errs" "github.com/smallstep/cli/usage" + "github.com/smallstep/cli/utils" "github.com/urfave/cli" ) type onboardingConfiguration struct { - Name string `json:"name"` - DNS string `json:"dns"` - Address string `json:"address"` + Name string `json:"name"` + DNS string `json:"dns"` + Address string `json:"address"` + password []byte } type onboardingPayload struct { Fingerprint string `json:"fingerprint"` @@ -307,8 +309,7 @@ func onboardAction(ctx *cli.Context) error { } var config onboardingConfiguration - err = json.Unmarshal(body, &config) - if err != nil { + if err = json.Unmarshal(body, &config); err != nil { return errors.Wrap(err, "error unmarshaling response") } @@ -316,17 +317,20 @@ func onboardAction(ctx *cli.Context) error { if err != nil { return err } + config.password = []byte(password) + + caConfig, fp, err := onboardPKI(config) + if err != nil { + return err + } fmt.Printf("Connected! Initializing step-ca with the following configuration...\n\n") fmt.Printf("Name: %s\n", config.Name) fmt.Printf("DNS: %s\n", config.DNS) fmt.Printf("Address: %s\n", config.Address) - fmt.Printf("Provisioner Password: %s\n\n", password) + fmt.Printf("Password: %s\n\n", password) - // TODO actually initialize the CA config (automatically add an "admin" JWT provisioner) - // and start listening - // TODO get the root cert fingerprint to post back to the onboarding guide - payload, err := json.Marshal(onboardingPayload{Fingerprint: "foobarbatbaz"}) + payload, err := json.Marshal(onboardingPayload{Fingerprint: fp}) if err != nil { return errors.Wrap(err, "error marshalling payload") } @@ -338,10 +342,61 @@ func onboardAction(ctx *cli.Context) error { resp.Body.Close() fmt.Printf("Initialized!\n") - fmt.Printf("Step CA has been started. Please return to the onboarding guide in your browser to continue.\n") - for { - time.Sleep(1 * time.Second) + fmt.Printf("Step CA is starting. Please return to the onboarding guide in your browser to continue.\n") + + 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}) + + rootCrt, rootKey, err := p.GenerateRootCertificate(config.Name+" Root CA", config.password) + if err != nil { + return nil, "", err + } + + err = p.GenerateIntermediateCertificate(config.Name+" Intermediate CA", rootCrt, rootKey, config.password) + if err != nil { + return nil, "", err + } + + // Generate provisioner + p.SetProvisioner("admin") + 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 } // fatal writes the passed error on the standard error and exits with the exit From 5013f7ffe08f903436943b4da153af3a32b9f1a0 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 Sep 2019 12:51:07 -0700 Subject: [PATCH 08/17] Move ca commands to its own package. --- cmd/step-ca/main.go | 273 +++----------------------------------------- commands/app.go | 79 +++++++++++++ commands/onboard.go | 170 +++++++++++++++++++++++++++ 3 files changed, 265 insertions(+), 257 deletions(-) create mode 100644 commands/app.go create mode 100644 commands/onboard.go diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 8d24b1c6..22b7905d 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -1,74 +1,35 @@ package main import ( - "bytes" - "encoding/json" "flag" "fmt" "html" - "io/ioutil" "log" + "math/rand" "net/http" - "net/url" "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/crypto/pki" - "github.com/smallstep/cli/crypto/randutil" - "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/smallstep/cli/utils" "github.com/urfave/cli" ) -type onboardingConfiguration struct { - Name string `json:"name"` - DNS string `json:"dns"` - Address string `json:"address"` - password []byte -} -type onboardingPayload struct { - Fingerprint string `json:"fingerprint"` -} - // commit and buildTime are filled in during build by the Makefile var ( BuildTime = "N/A" 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 @@ -126,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 @@ -134,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. @@ -172,42 +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: "onboard", - Usage: "Configure and run step-ca from the onboarding guide", - UsageText: "**step-ca onboard** ", - Action: onboardAction, - }, - { - 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/ @@ -220,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 { @@ -237,180 +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 -} - -func onboardAction(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return cli.ShowAppHelp(ctx) - } - if err := errs.NumberOfArguments(ctx, 1); err != nil { - return err - } - - // Get onboarding url - onboarding := "http://localhost:3002/onboarding/" - 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) - } - - fmt.Printf("Connecting to onboarding guide...\n\n") - - 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") - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return errors.Wrap(err, "error reading response") - } - - var config onboardingConfiguration - if err = json.Unmarshal(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) - - caConfig, fp, err := onboardPKI(config) - if err != nil { - return err - } - - fmt.Printf("Connected! Initializing step-ca with the following configuration...\n\n") - fmt.Printf("Name: %s\n", config.Name) - fmt.Printf("DNS: %s\n", config.DNS) - fmt.Printf("Address: %s\n", config.Address) - fmt.Printf("Password: %s\n\n", password) - - payload, err := json.Marshal(onboardingPayload{Fingerprint: fp}) - if err != nil { - return errors.Wrap(err, "error marshalling payload") - } - - resp, err := http.Post(onboardingURL, "application/json", bytes.NewBuffer(payload)) - if err != nil { - return errors.Wrap(err, "error connecting onboarding guide") - } - resp.Body.Close() - - fmt.Printf("Initialized!\n") - fmt.Printf("Step CA is starting. Please return to the onboarding guide in your browser to continue.\n") - - 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}) - - rootCrt, rootKey, err := p.GenerateRootCertificate(config.Name+" Root CA", config.password) - if err != nil { - return nil, "", err - } - - err = p.GenerateIntermediateCertificate(config.Name+" Intermediate CA", rootCrt, rootKey, config.password) - if err != nil { - return nil, "", err - } - - // Generate provisioner - p.SetProvisioner("admin") - 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 -} - -// 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..250a5e41 --- /dev/null +++ b/commands/app.go @@ -0,0 +1,79 @@ +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, + 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..b223ce46 --- /dev/null +++ b/commands/onboard.go @@ -0,0 +1,170 @@ +package commands + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/cli/command" + "github.com/smallstep/cli/crypto/pki" + "github.com/smallstep/cli/crypto/randutil" + "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/utils" + "github.com/urfave/cli" +) + +type onboardingConfiguration struct { + Name string `json:"name"` + DNS string `json:"dns"` + Address string `json:"address"` + password []byte +} + +type onboardingPayload struct { + Fingerprint string `json:"fingerprint"` +} + +func init() { + command.Register(cli.Command{ + Name: "onboard", + Usage: "Configure and run step-ca from the onboarding guide", + UsageText: "**step-ca onboard** ", + Action: onboardAction, + }) +} + +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 := "http://localhost:3002/onboarding/" + 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) + } + + fmt.Printf("Connecting to onboarding guide...\n\n") + + 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 { + res.Body.Close() + return errors.Errorf("error connecting onboarding guide: %s", res.Status) + } + + 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) + + fmt.Printf("Connected! Initializing step-ca with the following configuration...\n\n") + fmt.Printf("Name: %s\n", config.Name) + fmt.Printf("DNS: %s\n", config.DNS) + fmt.Printf("Address: %s\n", config.Address) + fmt.Printf("Password: %s\n\n", password) + + 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 marshalling payload") + } + + resp, err := http.Post(onboardingURL, "application/json", bytes.NewBuffer(payload)) + if err != nil { + return errors.Wrap(err, "error connecting onboarding guide") + } + resp.Body.Close() + + fmt.Printf("Initialized!\n") + fmt.Printf("Step CA is starting. Please return to the onboarding guide in your browser to continue.\n") + + 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}) + + rootCrt, rootKey, err := p.GenerateRootCertificate(config.Name+" Root CA", config.password) + if err != nil { + return nil, "", err + } + + err = p.GenerateIntermediateCertificate(config.Name+" Intermediate CA", rootCrt, rootKey, config.password) + if err != nil { + return nil, "", err + } + + // Generate provisioner + p.SetProvisioner("admin") + 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) +} From c060ceef7874ca27cf4e5e94fe02f55ccb78313d Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 Sep 2019 13:01:14 -0700 Subject: [PATCH 09/17] Show error if POST fails. --- commands/onboard.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/commands/onboard.go b/commands/onboard.go index b223ce46..35a321df 100644 --- a/commands/onboard.go +++ b/commands/onboard.go @@ -105,6 +105,9 @@ func onboardAction(ctx *cli.Context) error { return errors.Wrap(err, "error connecting onboarding guide") } resp.Body.Close() + if resp.StatusCode >= 400 { + fmt.Fprintf(os.Stderr, "error connecting onboarding guide: %s\n", res.Status) + } fmt.Printf("Initialized!\n") fmt.Printf("Step CA is starting. Please return to the onboarding guide in your browser to continue.\n") From a383669d542538c833cf7e2609b0f655ae39ca79 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 Sep 2019 15:32:48 -0700 Subject: [PATCH 10/17] Improve onboard messages. --- commands/onboard.go | 48 +++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/commands/onboard.go b/commands/onboard.go index 35a321df..fcabc83c 100644 --- a/commands/onboard.go +++ b/commands/onboard.go @@ -3,7 +3,6 @@ package commands import ( "bytes" "encoding/json" - "fmt" "io" "net/http" "net/url" @@ -16,6 +15,7 @@ import ( "github.com/smallstep/cli/crypto/pki" "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" ) @@ -31,6 +31,15 @@ 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", @@ -59,7 +68,7 @@ func onboardAction(ctx *cli.Context) error { return errors.Wrapf(err, "error parsing %s", onboarding) } - fmt.Printf("Connecting to onboarding guide...\n\n") + ui.Println("Connecting to onboarding guide...") token := ctx.Args().Get(0) onboardingURL := u.ResolveReference(&url.URL{Path: token}).String() @@ -69,8 +78,11 @@ func onboardAction(ctx *cli.Context) error { return errors.Wrap(err, "error connecting onboarding guide") } if res.StatusCode >= 400 { - res.Body.Close() - return errors.Errorf("error connecting onboarding guide: %s", res.Status) + 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 @@ -84,11 +96,12 @@ func onboardAction(ctx *cli.Context) error { } config.password = []byte(password) - fmt.Printf("Connected! Initializing step-ca with the following configuration...\n\n") - fmt.Printf("Name: %s\n", config.Name) - fmt.Printf("DNS: %s\n", config.DNS) - fmt.Printf("Address: %s\n", config.Address) - fmt.Printf("Password: %s\n\n", 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 { @@ -104,13 +117,19 @@ func onboardAction(ctx *cli.Context) error { if err != nil { return errors.Wrap(err, "error connecting onboarding guide") } - resp.Body.Close() if resp.StatusCode >= 400 { - fmt.Fprintf(os.Stderr, "error connecting onboarding guide: %s\n", res.Status) + 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() } - fmt.Printf("Initialized!\n") - fmt.Printf("Step CA is starting. Please return to the onboarding guide in your browser to continue.\n") + 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 { @@ -134,11 +153,13 @@ func onboardPKI(config onboardingConfiguration) (*authority.Config, string, erro 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 @@ -146,6 +167,7 @@ func onboardPKI(config onboardingConfiguration) (*authority.Config, string, erro // Generate provisioner p.SetProvisioner("admin") + ui.Println("Generating admin provisioner...") if err = p.GenerateKeyPairs(config.password); err != nil { return nil, "", err } From e77b7b0b62e0a996c6b116e52bae2081f83b34fa Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 12 Sep 2019 16:40:32 -0700 Subject: [PATCH 11/17] Update to go1.13 --- .travis.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b602cfc6..991bee27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go go: -- 1.12.x +- 1.13.x addons: apt: packages: diff --git a/Makefile b/Makefile index 164cab35..b3734e53 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ all: build test lint bootstra%: $Q which dep || go get github.com/golang/dep/cmd/dep $Q dep ensure - $Q GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.17.1 + $Q GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.18.0 vendor: Gopkg.lock From 50db67e589cd6c9f8ef52692e518bef4c406698c Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 13 Sep 2019 12:11:46 -0700 Subject: [PATCH 12/17] Make dep work copying pki package from cli. TODO: refactor and use this package from the cli. --- commands/onboard.go | 4 +- pki/pki.go | 506 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 508 insertions(+), 2 deletions(-) create mode 100644 pki/pki.go diff --git a/commands/onboard.go b/commands/onboard.go index fcabc83c..2b174a60 100644 --- a/commands/onboard.go +++ b/commands/onboard.go @@ -11,8 +11,8 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/ca" + "github.com/smallstep/certificates/pki" "github.com/smallstep/cli/command" - "github.com/smallstep/cli/crypto/pki" "github.com/smallstep/cli/crypto/randutil" "github.com/smallstep/cli/errs" "github.com/smallstep/cli/ui" @@ -110,7 +110,7 @@ func onboardAction(ctx *cli.Context) error { payload, err := json.Marshal(onboardingPayload{Fingerprint: fp}) if err != nil { - return errors.Wrap(err, "error marshalling payload") + return errors.Wrap(err, "error marshaling payload") } resp, err := http.Post(onboardingURL, "application/json", bytes.NewBuffer(payload)) diff --git a/pki/pki.go b/pki/pki.go new file mode 100644 index 00000000..1fab714d --- /dev/null +++ b/pki/pki.go @@ -0,0 +1,506 @@ +package pki + +import ( + "crypto" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "html" + "net" + "os" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/crypto/ssh" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/ca" + "github.com/smallstep/certificates/db" + "github.com/smallstep/cli/config" + "github.com/smallstep/cli/crypto/keys" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/crypto/tlsutil" + "github.com/smallstep/cli/crypto/x509util" + "github.com/smallstep/cli/errs" + "github.com/smallstep/cli/jose" + "github.com/smallstep/cli/ui" + "github.com/smallstep/cli/utils" +) + +const ( + // ConfigPath is the directory name under the step path where the configuration + // files will be stored. + configPath = "config" + // PublicPath is the directory name under the step path where the public keys + // will be stored. + publicPath = "certs" + // PublicPath is the directory name under the step path where the private keys + // will be stored. + privatePath = "secrets" + // DBPath is the directory name under the step path where the private keys + // will be stored. + dbPath = "db" +) + +// GetDBPath returns the path where the file-system persistence is stored +// based on the STEPPATH environment variable. +func GetDBPath() string { + return filepath.Join(config.StepPath(), dbPath) +} + +// GetConfigPath returns the directory where the configuration files are stored +// based on the STEPPATH environment variable. +func GetConfigPath() string { + return filepath.Join(config.StepPath(), configPath) +} + +// GetPublicPath returns the directory where the public keys are stored based on +// the STEPPATH environment variable. +func GetPublicPath() string { + return filepath.Join(config.StepPath(), publicPath) +} + +// GetSecretsPath returns the directory where the private keys are stored based +// on the STEPPATH environment variable. +func GetSecretsPath() string { + return filepath.Join(config.StepPath(), privatePath) +} + +// GetRootCAPath returns the path where the root CA is stored based on the +// STEPPATH environment variable. +func GetRootCAPath() string { + return filepath.Join(config.StepPath(), publicPath, "root_ca.crt") +} + +// GetOTTKeyPath returns the path where the one-time token key is stored based +// on the STEPPATH environment variable. +func GetOTTKeyPath() string { + return filepath.Join(config.StepPath(), privatePath, "ott_key") +} + +// GetProvisioners returns the map of provisioners on the given CA. +func GetProvisioners(caURL, rootFile string) (provisioner.List, error) { + if len(rootFile) == 0 { + rootFile = GetRootCAPath() + } + client, err := ca.NewClient(caURL, ca.WithRootFile(rootFile)) + if err != nil { + return nil, err + } + cursor := "" + provisioners := provisioner.List{} + for { + resp, err := client.Provisioners(ca.WithProvisionerCursor(cursor), ca.WithProvisionerLimit(100)) + if err != nil { + return nil, err + } + provisioners = append(provisioners, resp.Provisioners...) + if resp.NextCursor == "" { + return provisioners, nil + } + cursor = resp.NextCursor + } +} + +// GetProvisionerKey returns the encrypted provisioner key with the for the +// given kid. +func GetProvisionerKey(caURL, rootFile, kid string) (string, error) { + if len(rootFile) == 0 { + rootFile = GetRootCAPath() + } + client, err := ca.NewClient(caURL, ca.WithRootFile(rootFile)) + if err != nil { + return "", err + } + resp, err := client.ProvisionerKey(kid) + if err != nil { + return "", err + } + return resp.Key, nil +} + +// PKI represents the Public Key Infrastructure used by a certificate authority. +type PKI struct { + root, rootKey, rootFingerprint string + intermediate, intermediateKey string + sshHostPubKey, sshHostKey string + sshUserPubKey, sshUserKey string + config, defaults string + ottPublicKey *jose.JSONWebKey + ottPrivateKey *jose.JSONWebEncryption + provisioner string + address string + dnsNames []string + caURL string + enableSSH bool +} + +// New creates a new PKI configuration. +func New(public, private, config string) (*PKI, error) { + if _, err := os.Stat(public); os.IsNotExist(err) { + if err = os.MkdirAll(public, 0700); err != nil { + return nil, errs.FileError(err, public) + } + } + if _, err := os.Stat(private); os.IsNotExist(err) { + if err = os.MkdirAll(private, 0700); err != nil { + return nil, errs.FileError(err, private) + } + } + if len(config) > 0 { + if _, err := os.Stat(config); os.IsNotExist(err) { + if err = os.MkdirAll(config, 0700); err != nil { + return nil, errs.FileError(err, config) + } + } + } + + // get absolute path for dir/name + getPath := func(dir string, name string) (string, error) { + s, err := filepath.Abs(filepath.Join(dir, name)) + return s, errors.Wrapf(err, "error getting absolute path for %s", name) + } + + var err error + p := &PKI{ + provisioner: "step-cli", + address: "127.0.0.1:9000", + dnsNames: []string{"127.0.0.1"}, + } + if p.root, err = getPath(public, "root_ca.crt"); err != nil { + return nil, err + } + if p.rootKey, err = getPath(private, "root_ca_key"); err != nil { + return nil, err + } + if p.intermediate, err = getPath(public, "intermediate_ca.crt"); err != nil { + return nil, err + } + if p.intermediateKey, err = getPath(private, "intermediate_ca_key"); err != nil { + return nil, err + } + if p.sshHostPubKey, err = getPath(public, "ssh_host_key.pub"); err != nil { + return nil, err + } + if p.sshUserPubKey, err = getPath(public, "ssh_user_key.pub"); err != nil { + return nil, err + } + if p.sshHostKey, err = getPath(private, "ssh_host_key"); err != nil { + return nil, err + } + if p.sshUserKey, err = getPath(private, "ssh_user_key"); err != nil { + return nil, err + } + if len(config) > 0 { + if p.config, err = getPath(config, "ca.json"); err != nil { + return nil, err + } + if p.defaults, err = getPath(config, "defaults.json"); err != nil { + return nil, err + } + } + + return p, nil +} + +// GetCAConfigPath returns the path of the CA configuration file. +func (p *PKI) GetCAConfigPath() string { + return p.config +} + +// GetRootFingerprint returns the root fingerprint. +func (p *PKI) GetRootFingerprint() string { + return p.rootFingerprint +} + +// SetProvisioner sets the provisioner name of the OTT keys. +func (p *PKI) SetProvisioner(s string) { + p.provisioner = s +} + +// SetAddress sets the listening address of the CA. +func (p *PKI) SetAddress(s string) { + p.address = s +} + +// SetDNSNames sets the dns names of the CA. +func (p *PKI) SetDNSNames(s []string) { + p.dnsNames = s +} + +// SetCAURL sets the ca-url to use in the defaults.json. +func (p *PKI) SetCAURL(s string) { + p.caURL = s +} + +// GenerateKeyPairs generates the key pairs used by the certificate authority. +func (p *PKI) GenerateKeyPairs(pass []byte) error { + var err error + // Create OTT key pair, the user doesn't need to know about this. + p.ottPublicKey, p.ottPrivateKey, err = jose.GenerateDefaultKeyPair(pass) + if err != nil { + return err + } + + return nil +} + +// GenerateRootCertificate generates a root certificate with the given name. +func (p *PKI) GenerateRootCertificate(name string, pass []byte) (*x509.Certificate, interface{}, error) { + rootProfile, err := x509util.NewRootProfile(name) + if err != nil { + return nil, nil, err + } + + rootBytes, err := rootProfile.CreateWriteCertificate(p.root, p.rootKey, string(pass)) + if err != nil { + return nil, nil, err + } + + rootCrt, err := x509.ParseCertificate(rootBytes) + if err != nil { + return nil, nil, errors.Wrap(err, "error parsing root certificate") + } + + sum := sha256.Sum256(rootCrt.Raw) + p.rootFingerprint = strings.ToLower(hex.EncodeToString(sum[:])) + + return rootCrt, rootProfile.SubjectPrivateKey(), nil +} + +// WriteRootCertificate writes to disk the given certificate and key. +func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error { + if err := utils.WriteFile(p.root, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: rootCrt.Raw, + }), 0600); err != nil { + return err + } + + _, err := pemutil.Serialize(rootKey, pemutil.WithPassword([]byte(pass)), pemutil.ToFile(p.rootKey, 0600)) + if err != nil { + return err + } + return nil +} + +// GenerateIntermediateCertificate generates an intermediate certificate with +// the given name. +func (p *PKI) GenerateIntermediateCertificate(name string, rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error { + interProfile, err := x509util.NewIntermediateProfile(name, rootCrt, rootKey) + if err != nil { + return err + } + _, err = interProfile.CreateWriteCertificate(p.intermediate, p.intermediateKey, string(pass)) + return err +} + +// GenerateSSHSigningKeys generates and encrypts a private key used for signing +// SSH user certificates and a private key used for signing host certificates. +func (p *PKI) GenerateSSHSigningKeys(password []byte) error { + var pubNames = []string{p.sshHostPubKey, p.sshUserPubKey} + var privNames = []string{p.sshHostKey, p.sshUserKey} + for i := 0; i < 2; i++ { + pub, priv, err := keys.GenerateDefaultKeyPair() + if err != nil { + return err + } + if _, ok := priv.(crypto.Signer); !ok { + return errors.Errorf("key of type %T is not a crypto.Signer", priv) + } + sshKey, err := ssh.NewPublicKey(pub) + if err != nil { + return errors.Wrapf(err, "error converting public key") + } + _, err = pemutil.Serialize(priv, pemutil.WithFilename(privNames[i]), pemutil.WithPassword(password)) + if err != nil { + return err + } + if err = utils.WriteFile(pubNames[i], ssh.MarshalAuthorizedKey(sshKey), 0600); err != nil { + return err + } + } + p.enableSSH = true + return nil +} + +func (p *PKI) askFeedback() { + ui.Println() + ui.Printf("\033[1mFEEDBACK\033[0m %s %s\n", + html.UnescapeString("&#"+strconv.Itoa(128525)+";"), + html.UnescapeString("&#"+strconv.Itoa(127867)+";")) + ui.Println(" The \033[1mstep\033[0m utility is not instrumented for usage statistics. It does not") + ui.Println(" phone home. But your feedback is extremely valuable. Any information you") + ui.Println(" can provide regarding how you’re using `step` helps. Please send us a") + ui.Println(" sentence or two, good or bad: \033[1mfeedback@smallstep.com\033[0m or join") + ui.Println(" \033[1mhttps://gitter.im/smallstep/community\033[0m.") +} + +// TellPKI outputs the locations of public and private keys generated +// generated for a new PKI. Generally this will consist of a root certificate +// and key and an intermediate certificate and key. +func (p *PKI) TellPKI() { + p.tellPKI() + p.askFeedback() +} + +func (p *PKI) tellPKI() { + ui.Println() + ui.PrintSelected("Root certificate", p.root) + ui.PrintSelected("Root private key", p.rootKey) + ui.PrintSelected("Root fingerprint", p.rootFingerprint) + ui.PrintSelected("Intermediate certificate", p.intermediate) + ui.PrintSelected("Intermediate private key", p.intermediateKey) + if p.enableSSH { + ui.PrintSelected("SSH user root certificate", p.sshUserPubKey) + ui.PrintSelected("SSH user root private key", p.sshUserKey) + ui.PrintSelected("SSH host root certificate", p.sshHostPubKey) + ui.PrintSelected("SSH host root private key", p.sshHostKey) + } +} + +type caDefaults struct { + CAUrl string `json:"ca-url"` + CAConfig string `json:"ca-config"` + Fingerprint string `json:"fingerprint"` + Root string `json:"root"` +} + +// Option is the type for modifiers over the auth config object. +type Option func(c *authority.Config) error + +// WithDefaultDB is a configuration modifier that adds a default DB stanza to +// the authority config. +func WithDefaultDB() Option { + return func(c *authority.Config) error { + c.DB = &db.Config{ + Type: "badger", + DataSource: GetDBPath(), + } + return nil + } +} + +// WithoutDB is a configuration modifier that adds a default DB stanza to +// the authority config. +func WithoutDB() Option { + return func(c *authority.Config) error { + c.DB = nil + return nil + } +} + +// GenerateConfig returns the step certificates configuration. +func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) { + key, err := p.ottPrivateKey.CompactSerialize() + if err != nil { + return nil, errors.Wrap(err, "error serializing private key") + } + + config := &authority.Config{ + Root: []string{p.root}, + FederatedRoots: []string{}, + IntermediateCert: p.intermediate, + IntermediateKey: p.intermediateKey, + Address: p.address, + DNSNames: p.dnsNames, + Logger: []byte(`{"format": "text"}`), + DB: &db.Config{ + Type: "badger", + DataSource: GetDBPath(), + }, + AuthorityConfig: &authority.AuthConfig{ + DisableIssuedAtCheck: false, + Provisioners: provisioner.List{ + &provisioner.JWK{Name: p.provisioner, Type: "jwk", Key: p.ottPublicKey, EncryptedKey: key}, + }, + }, + TLS: &tlsutil.TLSOptions{ + MinVersion: x509util.DefaultTLSMinVersion, + MaxVersion: x509util.DefaultTLSMaxVersion, + Renegotiation: x509util.DefaultTLSRenegotiation, + CipherSuites: x509util.DefaultTLSCipherSuites, + }, + } + if p.enableSSH { + config.SSH = &authority.SSHConfig{ + HostKey: p.sshHostKey, + UserKey: p.sshUserKey, + } + } + + // Apply configuration modifiers + for _, o := range opt { + if err = o(config); err != nil { + return nil, err + } + } + + return config, nil +} + +// Save stores the pki on a json file that will be used as the certificate +// authority configuration. +func (p *PKI) Save(opt ...Option) error { + p.tellPKI() + + config, err := p.GenerateConfig(opt...) + if err != nil { + return err + } + + b, err := json.MarshalIndent(config, "", " ") + if err != nil { + return errors.Wrapf(err, "error marshaling %s", p.config) + } + if err = utils.WriteFile(p.config, b, 0666); err != nil { + return errs.FileError(err, p.config) + } + + // Generate the CA URL. + if p.caURL == "" { + p.caURL = p.dnsNames[0] + var port string + _, port, err = net.SplitHostPort(p.address) + if err != nil { + return errors.Wrapf(err, "error parsing %s", p.address) + } + if port == "443" { + p.caURL = fmt.Sprintf("https://%s", p.caURL) + } else { + p.caURL = fmt.Sprintf("https://%s:%s", p.caURL, port) + } + } + + defaults := &caDefaults{ + Root: p.root, + CAConfig: p.config, + CAUrl: p.caURL, + Fingerprint: p.rootFingerprint, + } + b, err = json.MarshalIndent(defaults, "", " ") + if err != nil { + return errors.Wrapf(err, "error marshaling %s", p.defaults) + } + if err = utils.WriteFile(p.defaults, b, 0666); err != nil { + return errs.FileError(err, p.defaults) + } + + ui.PrintSelected("Default configuration", p.defaults) + ui.PrintSelected("Certificate Authority configuration", p.config) + if config.DB != nil { + ui.PrintSelected("Database", config.DB.DataSource) + } + ui.Println() + ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.") + + p.askFeedback() + + return nil +} From 3766c8a0cf8fd6b2a86de1bd30f5e3038e574811 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 16 Sep 2019 11:14:56 -0700 Subject: [PATCH 13/17] Update dependencies. --- Gopkg.lock | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 87d71d02..3a3bd0f8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -207,10 +207,11 @@ [[projects]] branch = "master" - digest = "1:de4337595176796dc48d7a7597b81a295fc1824edd9f07e05e50006b4f8b710c" + digest = "1:629f6bfed7510441778233305c750cf4e131fe55932cb6904f1ea2ce0d081f07" name = "github.com/smallstep/cli" packages = [ "command", + "command/version", "config", "crypto/keys", "crypto/pemutil", @@ -228,7 +229,7 @@ "utils", ] pruneopts = "UT" - revision = "d75e047d1516db21e66b74064fd5e5d417fc0f67" + revision = "a917283741b3ad35e879f607243eef9eba4c3302" [[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", From d0e5976c06cca0fa62511b34bcbbdc467abb9bdb Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 24 Sep 2019 12:15:41 -0700 Subject: [PATCH 14/17] Use production URL and add description. --- commands/onboard.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/commands/onboard.go b/commands/onboard.go index 2b174a60..cc4a1eef 100644 --- a/commands/onboard.go +++ b/commands/onboard.go @@ -20,6 +20,11 @@ import ( "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"` @@ -43,9 +48,21 @@ func (e onboardingError) Error() string { func init() { command.Register(cli.Command{ Name: "onboard", - Usage: "Configure and run step-ca from the onboarding guide", + 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.`, }) } @@ -58,7 +75,7 @@ func onboardAction(ctx *cli.Context) error { } // Get onboarding url - onboarding := "http://localhost:3002/onboarding/" + onboarding := defaultOnboardingURL if v := os.Getenv("STEP_CA_ONBOARDING_URL"); v != "" { onboarding = v } From 8b8faf1b2d9ac6a01633ab37f0309e44d2b7ad6a Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 26 Sep 2019 15:23:32 -0700 Subject: [PATCH 15/17] Update pki with changes in smallstep/cli --- pki/pki.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pki/pki.go b/pki/pki.go index 1fab714d..56f3dbbd 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -403,6 +403,13 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) { 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{}, @@ -417,9 +424,7 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) { }, AuthorityConfig: &authority.AuthConfig{ DisableIssuedAtCheck: false, - Provisioners: provisioner.List{ - &provisioner.JWK{Name: p.provisioner, Type: "jwk", Key: p.ottPublicKey, EncryptedKey: key}, - }, + Provisioners: provisioner.List{prov}, }, TLS: &tlsutil.TLSOptions{ MinVersion: x509util.DefaultTLSMinVersion, @@ -429,10 +434,14 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) { }, } if p.enableSSH { + enableSSHCA := true config.SSH = &authority.SSHConfig{ HostKey: p.sshHostKey, UserKey: p.sshUserKey, } + prov.Claims = &provisioner.Claims{ + EnableSSHCA: &enableSSHCA, + } } // Apply configuration modifiers From 120ebf394196d1e6663709289a0c696862f4b012 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 26 Sep 2019 15:34:56 -0700 Subject: [PATCH 16/17] Update dependencies. --- Gopkg.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 3a3bd0f8..864d3324 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -207,7 +207,7 @@ [[projects]] branch = "master" - digest = "1:629f6bfed7510441778233305c750cf4e131fe55932cb6904f1ea2ce0d081f07" + digest = "1:c26dc5debe7fa23c636a116c336aa033f8d7be0d3464d7b363f4b3355dcccf4f" name = "github.com/smallstep/cli" packages = [ "command", @@ -229,7 +229,7 @@ "utils", ] pruneopts = "UT" - revision = "a917283741b3ad35e879f607243eef9eba4c3302" + revision = "eeecaac062cb548ee2ab7c7563bc3c2f2160f019" [[projects]] branch = "master" From d3361e7a5865883d98366bfd03a53be91451f432 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 26 Sep 2019 17:02:49 -0700 Subject: [PATCH 17/17] Add UsageText to virtual command. --- commands/app.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/commands/app.go b/commands/app.go index 250a5e41..36155bd9 100644 --- a/commands/app.go +++ b/commands/app.go @@ -19,6 +19,8 @@ import ( var AppCommand = cli.Command{ Name: "start", Action: appAction, + UsageText: `**step-ca** + [**--password-file**=]`, Flags: []cli.Flag{ cli.StringFlag{ Name: "password-file",