Skip to content

Commit

Permalink
Merge pull request #66 from smallstep/mariano/non-interactive
Browse files Browse the repository at this point in the history
Add password-file flag on key creation and independent provisioner password
  • Loading branch information
maraino authored Jan 19, 2019
2 parents a329403 + 7dd55c2 commit 3f37257
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 37 deletions.
36 changes: 27 additions & 9 deletions command/ca/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func initCommand() cli.Command {
Name: "password-file",
Usage: `The path to the <file> containing the password to encrypt the keys.`,
},
cli.StringFlag{
Name: "provisioner-password-file",
Usage: `The path to the <file> containing the password to encrypt the provisioner key.`,
},
cli.StringFlag{
Name: "with-ca-url",
Usage: `<URI> of the Step Certificate Authority to write in defaults.json`,
Expand All @@ -68,12 +72,11 @@ func initCommand() cli.Command {
}
}

func initAction(ctx *cli.Context) error {
func initAction(ctx *cli.Context) (err error) {
if err := assertCryptoRand(); err != nil {
return err
}

var password string
var rootCrt *stepx509.Certificate
var rootKey interface{}

Expand All @@ -96,13 +99,22 @@ func initAction(ctx *cli.Context) error {
}
}

passwordFile := ctx.String("password-file")
if passwordFile != "" {
b, err := utils.ReadPasswordFromFile(passwordFile)
var password string
if passwordFile := ctx.String("password-file"); passwordFile != "" {
password, err = utils.ReadStringPasswordFromFile(passwordFile)
if err != nil {
return err
}
}

// Provisioner password will be equal to the certificate private keys if
// --provisioner-password-file is not provided.
var provisionerPassword []byte
if passwordFile := ctx.String("provisioner-password-file"); passwordFile != "" {
provisionerPassword, err = utils.ReadPasswordFromFile(passwordFile)
if err != nil {
return err
}
password = string(b)
}

p, err := pki.New(pki.GetPublicPath(), pki.GetSecretsPath(), pki.GetConfigPath())
Expand Down Expand Up @@ -157,9 +169,15 @@ func initAction(ctx *cli.Context) error {
}

if configure {
// Generate ott key pairs.
if err := p.GenerateKeyPairs(pass); err != nil {
return err
// Generate provisioner key pairs.
if len(provisionerPassword) > 0 {
if err := p.GenerateKeyPairs(provisionerPassword); err != nil {
return err
}
} else {
if err := p.GenerateKeyPairs(pass); err != nil {
return err
}
}
}

Expand Down
17 changes: 14 additions & 3 deletions command/ca/provisioner/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/cli/errs"
"github.com/smallstep/cli/flags"
"github.com/smallstep/cli/jose"
"github.com/smallstep/cli/ui"
"github.com/smallstep/cli/utils"
"github.com/urfave/cli"
)

Expand All @@ -15,7 +17,7 @@ func addCommand() cli.Command {
Action: cli.ActionFunc(addAction),
Usage: "add one or more provisioners the CA configuration",
UsageText: `**step ca provisioner add** <name> <jwk-file> [<jwk-file> ...]
[**--ca-config**=<file>] [**--create**]`,
[**--ca-config**=<file>] [**--create**] [**--password-file**=<file>]`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "ca-config",
Expand All @@ -25,6 +27,7 @@ func addCommand() cli.Command {
Name: "create",
Usage: `Create a new ECDSA key pair using curve P-256 and populate a new provisioner.`,
},
flags.PasswordFile,
},
Description: `**step ca provisioner add** adds one or more provisioners
to the configuration and writes the new configuration back to the CA config.
Expand Down Expand Up @@ -58,7 +61,7 @@ $ step ca provisioner add [email protected] ./max-laptop.jwk ./max-phone.pem ./m
}
}

func addAction(ctx *cli.Context) error {
func addAction(ctx *cli.Context) (err error) {
if ctx.NArg() < 1 {
return errs.TooFewArguments(ctx)
}
Expand All @@ -71,6 +74,14 @@ func addAction(ctx *cli.Context) error {
return errs.RequiredFlag(ctx, "ca-config")
}

var password string
if passwordFile := ctx.String("password-file"); len(passwordFile) > 0 {
password, err = utils.ReadStringPasswordFromFile(passwordFile)
if err != nil {
return err
}
}

c, err := authority.LoadConfiguration(config)
if err != nil {
return errors.Wrapf(err, "error loading configuration")
Expand All @@ -89,7 +100,7 @@ func addAction(ctx *cli.Context) error {
if ctx.NArg() > 1 {
return errs.IncompatibleFlag(ctx, "create", "<jwk-path> positional arg")
}
pass, err := ui.PromptPasswordGenerate("Please enter a password to encrypt the provisioner private key? [leave empty and we'll generate one]")
pass, err := ui.PromptPasswordGenerate("Please enter a password to encrypt the provisioner private key? [leave empty and we'll generate one]", ui.WithValue(password))
if err != nil {
return err
}
Expand Down
28 changes: 18 additions & 10 deletions command/crypto/jwk/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func createCommand() cli.Command {
UsageText: `**step crypto jwk create** <public-jwk-file> <private-jwk-file>
[**--kty**=<type>] [**--alg**=<algorithm>] [**--use**=<use>]
[**--size**=<size>] [**--crv**=<curve>] [**--kid**=<kid>]
[**--from-pem**=<pem-file>]`,
[**--from-pem**=<pem-file>] [**--password-file**=<file>]`,
Description: `**step crypto jwk create** generates a new JWK (JSON Web Key) or constructs a
JWK from an existing key. The generated JWK conforms to RFC7517 and can be used
to sign and encrypt data using JWT, JWS, and JWE.
Expand Down Expand Up @@ -393,28 +393,28 @@ related.`,
Usage: `Create a JWK representing the key encoded in an
existing <pem-file> instead of creating a new key.`,
},
cli.BoolFlag{
Name: "no-password",
Usage: `Do not ask for a password to encrypt the JWK. Sensitive
key material will be written to disk unencrypted. This is not
recommended. Requires **--insecure** flag.`,
},
flags.PasswordFile,
flags.NoPassword,
flags.Subtle,
flags.Insecure,
flags.Force,
},
}
}

func createAction(ctx *cli.Context) error {
func createAction(ctx *cli.Context) (err error) {
// require public and private files
if err := errs.NumberOfArguments(ctx, 2); err != nil {
return err
}

// Use password to protect private JWK by default
usePassword := true
passwordFile := ctx.String("password-file")
if ctx.Bool("no-password") {
if len(passwordFile) > 0 {
return errs.IncompatibleFlag(ctx, "no-password", "password-file")
}
if ctx.Bool("insecure") {
usePassword = false
} else {
Expand All @@ -428,6 +428,15 @@ func createAction(ctx *cli.Context) error {
return errs.EqualArguments(ctx, "public-jwk-file", "private-jwk-file")
}

// Read password if necessary
var password string
if len(passwordFile) > 0 {
password, err = utils.ReadStringPasswordFromFile(passwordFile)
if err != nil {
return err
}
}

kty := ctx.String("kty")
crv := ctx.String("crv")
alg := ctx.String("alg")
Expand Down Expand Up @@ -476,7 +485,6 @@ func createAction(ctx *cli.Context) error {
}

// Generate or read secrets
var err error
var jwk *jose.JSONWebKey
switch {
case pemFile != "":
Expand Down Expand Up @@ -539,7 +547,7 @@ func createAction(ctx *cli.Context) error {
var rcpt jose.Recipient
// Generate JWE encryption key.
if jose.SupportsPBKDF2 {
key, err := ui.PromptPassword("Please enter the password to encrypt the private JWK")
key, err := ui.PromptPassword("Please enter the password to encrypt the private JWK", ui.WithValue(password))
if err != nil {
return errors.Wrap(err, "error reading password")
}
Expand Down
35 changes: 20 additions & 15 deletions command/crypto/keypair.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ func createKeyPairCommand() cli.Command {
Action: command.ActionFunc(createAction),
Usage: "generate a public / private keypair in PEM format",
UsageText: `**step crypto keypair** <pub_file> <priv_file>
[**--curve**=<curve>] [**--no-password**] [**--size**=<size>]
[**--kty**=<key-type>]`,
[**--kty**=<key-type>] [**--curve**=<curve>] [**--size**=<size>]
[**--password-file**=<file>] [**--no-password**]`,
Description: `**step crypto keypair** generates a raw public /
private keypair in PEM format. These keys can be used by other operations
to sign and encrypt data, and the public key can be bound to an identity
Expand Down Expand Up @@ -125,22 +125,15 @@ unset, default is P-256 for EC keys and Ed25519 for OKP keys.
Usage: `Create a PEM representing the key encoded in an
existing <jwk-file> instead of creating a new key.`,
},
cli.BoolFlag{
Name: "insecure",
Hidden: true,
},
cli.BoolFlag{
Name: "no-password",
Usage: `Do not ask for a password to encrypt the private key.
Sensitive key material will be written to disk unencrypted. This is not
recommended. Requires **--insecure** flag.`,
},
flags.PasswordFile,
flags.NoPassword,
flags.Insecure,
flags.Force,
},
}
}

func createAction(ctx *cli.Context) error {
func createAction(ctx *cli.Context) (err error) {
if err := errs.NumberOfArguments(ctx, 2); err != nil {
return err
}
Expand All @@ -153,11 +146,23 @@ func createAction(ctx *cli.Context) error {

insecure := ctx.Bool("insecure")
noPass := ctx.Bool("no-password")
passwordFile := ctx.String("password-file")
if noPass && len(passwordFile) > 0 {
return errs.IncompatibleFlag(ctx, "no-password", "password-file")
}
if noPass && !insecure {
return errs.RequiredWithFlag(ctx, "insecure", "no-password")
}

var err error
// Read password if necessary
var password string
if len(passwordFile) > 0 {
password, err = utils.ReadStringPasswordFromFile(passwordFile)
if err != nil {
return err
}
}

var pub, priv interface{}
fromJWK := ctx.String("from-jwk")
if len(fromJWK) > 0 {
Expand Down Expand Up @@ -212,7 +217,7 @@ func createAction(ctx *cli.Context) error {
return err
}
} else {
pass, err := ui.PromptPassword("Please enter the password to encrypt the private key")
pass, err := ui.PromptPassword("Please enter the password to encrypt the private key", ui.WithValue(password))
if err != nil {
return errors.Wrap(err, "error reading password")
}
Expand Down
15 changes: 15 additions & 0 deletions flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ var Force = cli.BoolFlag{
Usage: "Force the overwrite of files without asking.",
}

// PasswordFile is a cli.Flag used to pass a file to encrypt or decrypt a
// private key.
var PasswordFile = cli.StringFlag{
Name: "password-file",
Usage: `The path to the <file> containing the password to encrypt or decrypt the private key.`,
}

// NoPassword is a cli.Flag used to avoid using a password to encrypt private
// keys.
var NoPassword = cli.BoolFlag{
Name: "no-password",
Usage: `Do not ask for a password to encrypt a private key. Sensitive key material will
be written to disk unencrypted. This is not recommended. Requires **--insecure** flag.`,
}

// ParseTimeOrDuration is a helper that returns the time or the current time
// with an extra duration. It's used in flags like --not-before, --not-after.
func ParseTimeOrDuration(s string) (time.Time, bool) {
Expand Down
10 changes: 10 additions & 0 deletions utils/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ func ReadPasswordFromFile(filename string) ([]byte, error) {
return password, nil
}

// ReadStringPasswordFromFile reads and returns the password from the given filename.
// The contents of the file will be trimmed at the right.
func ReadStringPasswordFromFile(filename string) (string, error) {
b, err := ReadPasswordFromFile(filename)
if err != nil {
return "", err
}
return string(b), nil
}

// ReadInput from stdin if something is detected or ask the user for an input
// using the given prompt.
func ReadInput(prompt string) ([]byte, error) {
Expand Down
10 changes: 10 additions & 0 deletions utils/read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ func TestReadPasswordFromFile(t *testing.T) {
require.True(t, bytes.Equal([]byte("my-password-on-file"), b), "expected %s to equal %s", b, content)
}

func TestStringReadPasswordFromFile(t *testing.T) {
content := []byte("my-password-on-file\n")
f, cleanup := newFile(t, content)
defer cleanup()

s, err := ReadStringPasswordFromFile(f.Name())
require.NoError(t, err)
require.Equal(t, "my-password-on-file", s, "expected %s to equal %s", s, content)
}

// Returns a temp file and a cleanup function to delete it.
func newFile(t *testing.T, data []byte) (file *os.File, cleanup func()) {
f, err := ioutil.TempFile("" /* dir */, "utils-read-test")
Expand Down

0 comments on commit 3f37257

Please sign in to comment.