diff --git a/command/ca/init.go b/command/ca/init.go index e3e535ea4..596520dad 100644 --- a/command/ca/init.go +++ b/command/ca/init.go @@ -60,6 +60,10 @@ func initCommand() cli.Command { Name: "password-file", Usage: `The path to the containing the password to encrypt the keys.`, }, + cli.StringFlag{ + Name: "provisioner-password-file", + Usage: `The path to the containing the password to encrypt the provisioner key.`, + }, cli.StringFlag{ Name: "with-ca-url", Usage: ` of the Step Certificate Authority to write in defaults.json`, @@ -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{} @@ -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()) @@ -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 + } } } diff --git a/command/ca/provisioner/add.go b/command/ca/provisioner/add.go index 2b5a2daca..95a6890f1 100644 --- a/command/ca/provisioner/add.go +++ b/command/ca/provisioner/add.go @@ -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" ) @@ -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** [ ...] - [**--ca-config**=] [**--create**]`, + [**--ca-config**=] [**--create**] [**--password-file**=]`, Flags: []cli.Flag{ cli.StringFlag{ Name: "ca-config", @@ -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. @@ -58,7 +61,7 @@ $ step ca provisioner add max@smallstep.com ./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) } @@ -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") @@ -89,7 +100,7 @@ func addAction(ctx *cli.Context) error { if ctx.NArg() > 1 { return errs.IncompatibleFlag(ctx, "create", " 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 } diff --git a/command/crypto/jwk/create.go b/command/crypto/jwk/create.go index ea61e5519..b90d3a19d 100644 --- a/command/crypto/jwk/create.go +++ b/command/crypto/jwk/create.go @@ -33,7 +33,7 @@ func createCommand() cli.Command { UsageText: `**step crypto jwk create** [**--kty**=] [**--alg**=] [**--use**=] [**--size**=] [**--crv**=] [**--kid**=] - [**--from-pem**=]`, + [**--from-pem**=] [**--password-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. @@ -393,12 +393,8 @@ related.`, Usage: `Create a JWK representing the key encoded in an existing 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, @@ -406,7 +402,7 @@ recommended. Requires **--insecure** flag.`, } } -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 @@ -414,7 +410,11 @@ func createAction(ctx *cli.Context) error { // 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 { @@ -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") @@ -476,7 +485,6 @@ func createAction(ctx *cli.Context) error { } // Generate or read secrets - var err error var jwk *jose.JSONWebKey switch { case pemFile != "": @@ -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") } diff --git a/command/crypto/keypair.go b/command/crypto/keypair.go index b7754d15e..5493c8ff0 100644 --- a/command/crypto/keypair.go +++ b/command/crypto/keypair.go @@ -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** -[**--curve**=] [**--no-password**] [**--size**=] -[**--kty**=]`, +[**--kty**=] [**--curve**=] [**--size**=] +[**--password-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 @@ -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 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 } @@ -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 { @@ -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") } diff --git a/flags/flags.go b/flags/flags.go index bf4f87186..2802a1035 100644 --- a/flags/flags.go +++ b/flags/flags.go @@ -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 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) { diff --git a/utils/read.go b/utils/read.go index 8d620027d..3a1cbb449 100644 --- a/utils/read.go +++ b/utils/read.go @@ -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) { diff --git a/utils/read_test.go b/utils/read_test.go index 1877aed23..2541c6a32 100644 --- a/utils/read_test.go +++ b/utils/read_test.go @@ -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")