2019-09-12 19:51:07 +00:00
|
|
|
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"
|
2020-10-23 22:04:09 +00:00
|
|
|
"github.com/smallstep/certificates/cas/apiv1"
|
2019-09-13 19:11:46 +00:00
|
|
|
"github.com/smallstep/certificates/pki"
|
2019-09-12 19:51:07 +00:00
|
|
|
"github.com/urfave/cli"
|
2020-10-29 20:10:03 +00:00
|
|
|
"go.step.sm/cli-utils/command"
|
|
|
|
"go.step.sm/cli-utils/errs"
|
|
|
|
"go.step.sm/cli-utils/fileutil"
|
|
|
|
"go.step.sm/cli-utils/ui"
|
2020-08-10 18:26:51 +00:00
|
|
|
"go.step.sm/crypto/randutil"
|
2019-09-12 19:51:07 +00:00
|
|
|
)
|
|
|
|
|
2019-09-24 19:15:41 +00:00
|
|
|
// 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/"
|
|
|
|
|
2019-09-12 19:51:07 +00:00
|
|
|
type onboardingConfiguration struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
DNS string `json:"dns"`
|
|
|
|
Address string `json:"address"`
|
|
|
|
password []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
type onboardingPayload struct {
|
|
|
|
Fingerprint string `json:"fingerprint"`
|
|
|
|
}
|
|
|
|
|
2019-09-12 22:32:48 +00:00
|
|
|
type onboardingError struct {
|
|
|
|
StatusCode int `json:"statusCode"`
|
|
|
|
Message string `json:"message"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e onboardingError) Error() string {
|
|
|
|
return e.Message
|
|
|
|
}
|
|
|
|
|
2019-09-12 19:51:07 +00:00
|
|
|
func init() {
|
|
|
|
command.Register(cli.Command{
|
|
|
|
Name: "onboard",
|
2019-09-24 19:15:41 +00:00
|
|
|
Usage: "configure and run step-ca from the onboarding guide",
|
2019-09-12 19:51:07 +00:00
|
|
|
UsageText: "**step-ca onboard** <token>",
|
|
|
|
Action: onboardAction,
|
2019-09-24 19:15:41 +00:00
|
|
|
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.`,
|
2019-09-12 19:51:07 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2019-09-24 19:15:41 +00:00
|
|
|
onboarding := defaultOnboardingURL
|
2019-09-12 19:51:07 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2019-09-12 22:32:48 +00:00
|
|
|
ui.Println("Connecting to onboarding guide...")
|
2019-09-12 19:51:07 +00:00
|
|
|
|
|
|
|
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 {
|
2019-09-12 22:32:48 +00:00
|
|
|
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")
|
2019-09-12 19:51:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
2019-09-12 22:32:48 +00:00
|
|
|
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()
|
2019-09-12 19:51:07 +00:00
|
|
|
|
|
|
|
caConfig, fp, err := onboardPKI(config)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
payload, err := json.Marshal(onboardingPayload{Fingerprint: fp})
|
|
|
|
if err != nil {
|
2019-09-13 19:11:46 +00:00
|
|
|
return errors.Wrap(err, "error marshaling payload")
|
2019-09-12 19:51:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := http.Post(onboardingURL, "application/json", bytes.NewBuffer(payload))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "error connecting onboarding guide")
|
|
|
|
}
|
2019-09-12 20:01:14 +00:00
|
|
|
if resp.StatusCode >= 400 {
|
2019-09-12 22:32:48 +00:00
|
|
|
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()
|
2019-09-12 20:01:14 +00:00
|
|
|
}
|
2019-09-12 19:51:07 +00:00
|
|
|
|
2019-09-12 22:32:48 +00:00
|
|
|
ui.Println("Initialized!")
|
|
|
|
ui.Println("Step CA is starting. Please return to the onboarding guide in your browser to continue.")
|
2019-09-12 19:51:07 +00:00
|
|
|
|
|
|
|
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) {
|
2020-10-23 22:04:09 +00:00
|
|
|
p, err := pki.New(apiv1.Options{
|
|
|
|
Type: apiv1.SoftCAS,
|
|
|
|
IsCreator: true,
|
|
|
|
})
|
2019-09-12 19:51:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
p.SetAddress(config.Address)
|
|
|
|
p.SetDNSNames([]string{config.DNS})
|
|
|
|
|
2019-09-12 22:32:48 +00:00
|
|
|
ui.Println("Generating root certificate...")
|
2020-10-23 22:04:09 +00:00
|
|
|
root, err := p.GenerateRootCertificate(config.Name, config.Name, config.Name, config.password)
|
2019-09-12 19:51:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
|
2019-09-12 22:32:48 +00:00
|
|
|
ui.Println("Generating intermediate certificate...")
|
2020-10-23 22:04:09 +00:00
|
|
|
err = p.GenerateIntermediateCertificate(config.Name, config.Name, config.Name, root, config.password)
|
2019-09-12 19:51:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate provisioner
|
|
|
|
p.SetProvisioner("admin")
|
2019-09-12 22:32:48 +00:00
|
|
|
ui.Println("Generating admin provisioner...")
|
2019-09-12 19:51:07 +00:00
|
|
|
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())
|
|
|
|
}
|
2020-10-29 20:10:03 +00:00
|
|
|
if err = fileutil.WriteFile(p.GetCAConfigPath(), b, 0666); err != nil {
|
2019-09-12 19:51:07 +00:00
|
|
|
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)
|
|
|
|
}
|