diff --git a/Gopkg.lock b/Gopkg.lock index d2d4193c..51c6342a 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -83,7 +83,7 @@ branch = "master" name = "github.com/gordonklaus/ineffassign" packages = ["."] - revision = "7bae11eba15a3285c75e388f77eb6357a2d73ee2" + revision = "3fd9b69f2fb179405773f03d33c68a00f3a1ca4a" [[projects]] branch = "master" @@ -219,7 +219,7 @@ "scrypt", "ssh/terminal" ] - revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602" + revision = "a2144134853fc9a27a7b1e3eb4f19f1a76df13c9" [[projects]] branch = "master" @@ -231,7 +231,7 @@ branch = "master" name = "golang.org/x/net" packages = ["idna"] - revision = "d0887baf81f4598189d4e12a37c6da86f0bba4d0" + revision = "8887df42c721e930089d31b28391090a10a497d7" [[projects]] branch = "master" @@ -272,7 +272,7 @@ "go/internal/gcimporter", "go/types/typeutil" ] - revision = "57f659e14dda699044a713b0f8d1d3ff033e0455" + revision = "32950ab3be12acf6d472893021373669979907ab" [[projects]] name = "gopkg.in/alecthomas/kingpin.v3-unstable" diff --git a/cmd/step/main.go b/cmd/step/main.go index cf59fbb8..73de20eb 100644 --- a/cmd/step/main.go +++ b/cmd/step/main.go @@ -19,7 +19,7 @@ import ( "github.com/smallstep/cli/usage" // Enabled commands - _ "github.com/smallstep/cli/command/certificates" + _ "github.com/smallstep/cli/command/certificate" _ "github.com/smallstep/cli/command/crypto" _ "github.com/smallstep/cli/command/oauth" diff --git a/command/certificates/bundle.go b/command/certificate/bundle.go similarity index 63% rename from command/certificates/bundle.go rename to command/certificate/bundle.go index 188186fb..a5c62907 100644 --- a/command/certificates/bundle.go +++ b/command/certificate/bundle.go @@ -1,4 +1,4 @@ -package certificates +package certificate import ( "encoding/pem" @@ -14,19 +14,33 @@ func bundleCommand() cli.Command { Name: "bundle", Action: cli.ActionFunc(bundleAction), Usage: `bundle a certificate with intermediate certificate(s) needed for certificate path validation.`, - UsageText: `step certificates bundle CRT_FILE BUNDLE_FILE`, - Description: `The 'step certificates bundle' command bundles a certificate with any -intermediates necessary to validate the certificate. + UsageText: `**step certificate bundle** `, + Description: `**step certificate bundle** command bundles a certificate + with any intermediates necessary to validate the certificate. - POSITIONAL ARGUMENTS - CRT_FILE - The path to a leaf certificate to bundle with issuing certificate(s). +## POSITIONAL ARGUMENTS - CA_FILE - The path to the Certificate Authoriy issusing certificate for the leaf. + +: The path to a leaf certificate to bundle with issuing certificate(s). - BUNDLE_FILE - The path to write the bundle.`, + +: The path to the Certificate Authoriy issusing certificate. + + +: The path to write the bundle. + +## EXIT CODES + +This command returns 0 on success and \>0 if any error occurs. + +## EXAMPLES + +Bundle a certificate with the intermediate certificate authority (issuer): + +''' +$ step certificate bundle foo.crt intermediate-ca.crt foo-bundle.crt +''' +`, } } diff --git a/command/certificates/certificates.go b/command/certificate/certificates.go similarity index 81% rename from command/certificates/certificates.go rename to command/certificate/certificates.go index da99bad0..11c7c480 100644 --- a/command/certificates/certificates.go +++ b/command/certificate/certificates.go @@ -1,4 +1,4 @@ -package certificates +package certificate import ( "github.com/smallstep/cli/command" @@ -8,10 +8,10 @@ import ( // Command returns the cli.Command for jwt and related subcommands. func init() { cmd := cli.Command{ - Name: "certificates", + Name: "certificate", Usage: "create, revoke, validate, bundle, and otherwise manage certificates.", UsageText: "step certificates [arguments] [global-flags] [subcommand-flags]", - Description: `The 'step certificates' command group provides facilities for creating + Description: `**step certificates** command group provides facilities for creating certificate signing requests (CSRs), creating self-signed certificates (e.g., for use as a root certificate authority), generating leaf or intermediate CA certificate by signing a CSR, validating certificates, @@ -19,7 +19,7 @@ func init() { of private keys. More information about certificates in general (as opposed to the - certificates commands) can be found at 'step help topics certificates' + **step certificate** sub-commands) can be found at **step help topics certificate** or online at [URL].`, Subcommands: cli.Commands{ diff --git a/command/certificates/create.go b/command/certificate/create.go similarity index 68% rename from command/certificates/create.go rename to command/certificate/create.go index 075939bf..35adbebf 100644 --- a/command/certificates/create.go +++ b/command/certificate/create.go @@ -1,4 +1,4 @@ -package certificates +package certificate import ( "crypto/rand" @@ -23,59 +23,63 @@ func createCommand() cli.Command { Name: "create", Action: cli.ActionFunc(createAction), Usage: "create a certificate or certificate signing request", - UsageText: `step certificates create SUBJECT CRT_FILE KEY_FILE [--type=CERTIFICATE_TYPE] - [--profile=PROFILE] [--csr] [--token=TOKEN]`, - Description: `The 'step certificates create' command generates a certificate or a - certificate signing requests (CSR) that can be signed later using 'step - certificates sign' (or some other tool) to produce a certificate. + UsageText: `**step certificate create** + [**ca**=] [**ca-key**=] [**--csr**] + [**no-password**] [**--profile**=]`, + Description: `**step certificate create** generates a certificate or a +certificate signing requests (CSR) that can be signed later using 'step +certificates sign' (or some other tool) to produce a certificate. - This command can create x.509 certificates for use with TLS as well as SSH - certificates. +This command creates x.509 certificates for use with TLS. -POSITIONAL ARGUMENTS - SUBJECT - The subject of the certificate. Typically this is a hostname for services or an email address for people. +## POSITIONAL ARGUMENTS - CRT_FILE - File to write CRT or CSR to (PEM format) + +: The subject of the certificate. Typically this is a hostname for services or an email address for people. - KEY_FILE - File to write private key to (PEM format)`, + +: File to write CRT or CSR to (PEM format) + + +: File to write private key to (PEM format) + +## EXIT CODES + +This command returns 0 on success and \>0 if any error occurs. + +## EXAMPLES + +Create a CSR and key: + +''' +$ step certificate create foo foo.csr foo.key --csr +''' + +Create a CSR and key - do not encrypt the key when writing to disk: + +''' +$ step certificate create foo foo.csr foo.key --csr --no-password --insecure +''' + +Create a leaf certificate and key: + +''' +$ step certificate create foo foo.crt foo.key intermediate-ca --ca ./intermediate-ca.crt --ca-key ./intermediate-ca.key +''' + +Create a root certificate and key: + +''' +$ step certificate create foo foo.crt foo.key --profile root-ca +''' + +Create an intermediate certificate and key: + +''' +$ step certificate create foo foo.crt foo.key --profile intermediate-ca --ca ./root-ca.crt --ca-key ./root-ca.key +''' +`, Flags: []cli.Flag{ - cli.StringFlag{ - Name: "type", - Value: "x509", - Usage: `The type of certificate to generate. If not specified default is x509. - - CERTIFICATE_TYPE must be one of: - x509 - Generate an x.509 certificate suitable for use with TLS. - ssh - Generate an SSH certificate.`, - }, - cli.StringFlag{ - Name: "profile", - Value: "leaf", - Usage: `The certificate profile sets various certificate details such as - certificate use and expiration. The default profile is 'leaf' which is suitable - for a client or server using TLS. - - PROFILE must be one of: - leaf - Generate a leaf x.509 certificate suitable for use with TLs. - intermediate-ca - Generate a certificate that can be used to sign additional leaf or intermediate certificates. - root-ca - Generate a new self-signed root certificate suitable for use as a root CA.`, - }, - cli.StringFlag{ - Name: "token", - Usage: `A provisioning token or bootstrap token for authenticating to a remote CA.`, - }, - cli.BoolFlag{ - Name: "csr", - Usage: `Generate a certificate signing request (CSR) instead of a certificate.`, - }, cli.StringFlag{ Name: "ca", Usage: `The certificate authority used to issue the new certificate (PEM file).`, @@ -85,17 +89,37 @@ POSITIONAL ARGUMENTS Usage: `The certificate authority private key used to sign the new certificate (PEM file).`, }, cli.BoolFlag{ - Name: "no-password", - Usage: "TODO, requires --insecure", - }, - cli.BoolFlag{ - Name: "subtle", - Hidden: true, + Name: "csr", + Usage: `Generate a certificate signing request (CSR) instead of a certificate.`, }, 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.`, + }, + cli.StringFlag{ + Name: "profile", + Value: "leaf", + Usage: `The certificate profile sets various certificate details such as + certificate use and expiration. The default profile is 'leaf' which is suitable + for a client or server using TLS. + +: is a case-sensitive string and must be one of: + + **leaf** + : Generate a leaf x.509 certificate suitable for use with TLs. + + **intermediate-ca** + : Generate a certificate that can be used to sign additional leaf or intermediate certificates. + + **root-ca** + : Generate a new self-signed root certificate suitable for use as a root CA.`, + }, }, } } @@ -122,12 +146,22 @@ func createAction(ctx *cli.Context) error { return errs.EqualArguments(ctx, "CRT_FILE", "KEY_FILE") } - typ := ctx.String("type") prof := ctx.String("profile") caPath := ctx.String("ca") caKeyPath := ctx.String("ca-key") + if prof != "root-ca" { + if caPath == "" { + return errs.RequiredWithFlagValue(ctx, "profile", prof, "ca") + } + if caKeyPath == "" { + return errs.RequiredWithFlagValue(ctx, "profile", prof, "ca-key") + } + } + var typ string if ctx.Bool("csr") { typ = "x509-csr" + } else { + typ = "x509" } var ( @@ -204,10 +238,8 @@ func createAction(ctx *cli.Context) error { Headers: map[string]string{}, } priv = profile.SubjectPrivateKey() - case "ssh": - return errors.Errorf("implementation incomplete! Come back later ...") default: - return errs.InvalidFlagValue(ctx, "type", typ, "x509, ssh") + return errs.NewError("unexpected type: %s", typ) } if err := ioutil.WriteFile(crtFile, pem.EncodeToMemory(pubPEM), diff --git a/command/certificates/inspect.go b/command/certificate/inspect.go similarity index 62% rename from command/certificates/inspect.go rename to command/certificate/inspect.go index fa808c1c..0502e9a4 100644 --- a/command/certificates/inspect.go +++ b/command/certificate/inspect.go @@ -1,4 +1,4 @@ -package certificates +package certificate import ( "crypto/tls" @@ -22,42 +22,92 @@ func inspectCommand() cli.Command { return cli.Command{ Name: "inspect", Action: cli.ActionFunc(inspectAction), - Usage: `print certificate or CSR details in human readable format.`, - UsageText: `step certificates inspect CRT_FILE [--format=FORMAT]`, - Description: `The 'step certificates inspect' command prints the details of a certificate + Usage: `print certificate or CSR details in human readable format`, + UsageText: `**step certificate inspect** [**--format**=]`, + Description: `**step certificate inspect** command prints the details of a certificate or CSR in a human readable format. Output from the inspect command is printed to STDERR instead of STDOUT unless. This is an intentional barrier to accidental misuse: scripts should never rely on the contents of an unvalidated certificate. -For scripting purposes, use 'step certificates verify'. +For scripting purposes, use **step certificate verify**. + + +## POSITIONAL ARGUMENTS + + +: Path to a certificate or certificate signing request (CSR) to inspect. + +## EXIT CODES + +This command returns 0 on success and \>0 if any error occurs. + +## EXAMPLES + +Inspect a local certificate (default to text format): + +''' +$ step certificate inspect ./certificate.crt +''' + +Inspect a local certificate in json format: + +''' +$ step certificate inspect ./certificate.crt --format json +''' + +Inspect a remote certificate (using the default root certificate bundle to verify the server): + +''' +$ step certificate inspect https://smallstep.com +''' + +Inspect a remote certificate using a custom root certificate to verify the server: + +''' +$ step certificate inspect https://smallstep.com --roots ./certificate.crt +''' + +Inspect a remote certificate using a custom list of root certificates to verify the server: + +''' +$ step certificate inspect https://smallstep.com --roots "./certificate.crt,./certificate2.crt,/certificate3.crt" +''' + +Inspect a remote certificate using a custom directory of root certificates to verify the server: + +''' +$ step certificate inspect https://smallstep.com --roots "./path/to/certificates/" +''' +`, - POSITIONAL ARGUMENTS - CRT_FILE - The path to a certificate or certificate signing request (CSR) to inspect.`, Flags: []cli.Flag{ cli.StringFlag{ Name: "format", Value: "text", Usage: `The output format for printing the introspection details. - FORMAT must be one of: - text - Print output in unstructured text suitable for a human to read - json - Print output in JSON format`, +: is a string and must be one of: + + **text** + : Print output in unstructured text suitable for a human to read. + + **json** + : Print output in JSON format.`, }, cli.StringFlag{ Name: "roots", - Usage: `Root certificate(s) to use in request to obtain remote server certificate. + Usage: `Root certificate(s) that will be used to verify the +authenticity of the remote server. - ROOTS is a string containing a (FILE | LIST of FILES | DIRECTORY) defined in one of the following ways: - FILE - Relative or full path to a file. All certificates in the file will be used for path validation. - LIST of Files - Comma-separated list of relative or full file paths. Every PEM encoded certificate - from each file will be used for path validation. - DIRECTORY - Relative or full path to a directory. Every PEM encoded certificate from each file - in the directory will be used for path validation.`, +: is a case-sensitive string and may be one of: + + **file** + : Relative or full path to a file. All certificates in the file will be used for path validation. + + **list of files** + : Comma-separated list of relative or full file paths. Every PEM encoded certificate from each file will be used for path validation. + + **directory** + : Relative or full path to a directory. Every PEM encoded certificate from each file in the directory will be used for path validation.`, }, }, } diff --git a/command/certificates/lint.go b/command/certificate/lint.go similarity index 55% rename from command/certificates/lint.go rename to command/certificate/lint.go index f30665cb..6c98df49 100644 --- a/command/certificates/lint.go +++ b/command/certificate/lint.go @@ -1,4 +1,4 @@ -package certificates +package certificate import ( "crypto/tls" @@ -22,26 +22,64 @@ func lintCommand() cli.Command { Name: "lint", Action: cli.ActionFunc(lintAction), Usage: `lint certificate details.`, - UsageText: `step certificates lint CRT_FILE`, + UsageText: `**step certificate lint** [**--roots**=]`, Description: `UPDATE ME - POSITIONAL ARGUMENTS - CRT_FILE - The path to a certificate or certificate signing request (CSR) to inspect.`, +## POSITIONAL ARGUMENTS + + +: Path to a certificate or certificate signing request (CSR) to lint. + +## EXIT CODES + +This command returns 0 on success and \>0 if any error occurs. + +## EXAMPLES + +''' +$ step certificate lint ./certificate.crt +''' + +Lint a remote certificate (using the default root certificate bundle to verify the server): + +''' +$ step certificate lint https://smallstep.com +''' + +Lint a remote certificate using a custom root certificate to verify the server: + +''' +$ step certificate lint https://smallstep.com --roots ./certificate.crt +''' + +Lint a remote certificate using a custom list of root certificates to verify the server: + +''' +$ step certificate lint https://smallstep.com --roots "./certificate.crt,./certificate2.crt,/certificate3.crt" +''' + +Lint a remote certificate using a custom directory of root certificates to verify the server: + +''' +$ step certificate lint https://smallstep.com --roots "./path/to/certificates/" +''' +`, Flags: []cli.Flag{ cli.StringFlag{ Name: "roots", - Usage: `Root certificate(s) to use in request to obtain remote server certificate. + Usage: `Root certificate(s) that will be used to verify the +authenticity of the remote server. - ROOTS is a string containing a (FILE | LIST of FILES | DIRECTORY) defined in one of the following ways: - FILE - Relative or full path to a file. All certificates in the file will be used for path validation. - LIST of Files - Comma-separated list of relative or full file paths. Every PEM encoded certificate - from each file will be used for path validation. - DIRECTORY - Relative or full path to a directory. Every PEM encoded certificate from each file - in the directory will be used for path validation.`, +: is a case-sensitive string and may be one of: + + **file** + : Relative or full path to a file. All certificates in the file will be used for path validation. + + **list of files** + : Comma-separated list of relative or full file paths. Every PEM encoded certificate from each file will be used for path validation. + + **directory** + : Relative or full path to a directory. Every PEM encoded certificate from each file in the directory will be used for path validation.`, }, }, } diff --git a/command/certificates/sign.go b/command/certificate/sign.go similarity index 74% rename from command/certificates/sign.go rename to command/certificate/sign.go index d020342e..5f747efd 100644 --- a/command/certificates/sign.go +++ b/command/certificate/sign.go @@ -1,4 +1,4 @@ -package certificates +package certificate import ( "encoding/pem" @@ -18,24 +18,33 @@ func signCommand() cli.Command { Name: "sign", Action: cli.ActionFunc(signAction), Usage: "sign a certificate signing request (CSR).", - UsageText: `step certificates sign CSR_FILE CRT_FILE KEY_FILE [--token=TOKEN]`, - Description: `The 'step certificates sign' generates a signed certificate from a - certificate signing requests (CSR). + UsageText: `**step certificate sign** `, + Description: `**step certificate sign** generates a signed +certificate from a certificate signing request (CSR). - POSITIONAL ARGUMENTS - CSR_FILE - The path to a certificate signing request (CSR) to be signed. - CRT_FILE - The path to an issuing certificate. - KEY_FILE - The path to a private key for signing the CSR.`, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "token", - Usage: `A provisioning token or bootstrap token for secure introduction and - mutual authentication with an unknown CA.`, - }, - }, +## POSITIONAL ARGUMENTS + + +: The path to a certificate signing request (CSR) to be signed. + + +: The path to an issuing certificate. + + +: The path to a private key for signing the CSR. + +## EXIT CODES + +This command returns 0 on success and \>0 if any error occurs. + +## EXAMPLES + +Sign a certificate signing request: + +''' +$ step certificate sign ./certificate-signing-request.csr ./issuer-certificate.crt ./issuer-private-key.priv +''' +`, } } @@ -58,6 +67,7 @@ func signAction(ctx *cli.Context) error { return errors.WithStack(err) } // Load the Issuer Certificate. + issuerCrt, _, err := stepx509.LoadCertificate(crtFile) if err != nil { return errors.WithStack(err) diff --git a/command/certificates/verify.go b/command/certificate/verify.go similarity index 50% rename from command/certificates/verify.go rename to command/certificate/verify.go index 279269a0..c274de42 100644 --- a/command/certificates/verify.go +++ b/command/certificate/verify.go @@ -1,4 +1,4 @@ -package certificates +package certificate import ( "crypto/x509" @@ -13,18 +13,51 @@ import ( func verifyCommand() cli.Command { return cli.Command{ - Name: "verify", - Action: cli.ActionFunc(verifyAction), - Usage: `verify a certificate.`, - UsageText: `step certificates verify CRT_FILE [--host=HOST]`, - Description: `The 'step certificates verify' command executes the certificate path validation -algorithm for x.509 certificates defined in RFC 5280. If the certificate is valid -this command will return '0'. If validation fails, or if an error occurs, this -command will produce a non-zero return value. + Name: "verify", + Action: cli.ActionFunc(verifyAction), + Usage: `verify a certificate.`, + UsageText: `**step certificates verify** [**--host**=] + [**--roots**=]`, + Description: `**step certificates verify** executes the certificate path +validation algorithm for x.509 certificates defined in RFC 5280. If the +certificate is valid this command will return '0'. If validation fails, or if +an error occurs, this command will produce a non-zero return value. - POSITIONAL ARGUMENTS - CRT_FILE - The path to a certificate to validate.`, +## POSITIONAL ARGUMENTS + + +: The path to a certificate to validate. + +## EXIT CODES + +This command returns 0 on success and \>0 if any error occurs. + +## EXAMPLES + +Verify a certificate using your operating system's default root certificate bundle: + +''' +$ step certificate verify ./certificate.crt +''' + +Verify a certificate using a custom root certificate for path validation: + +''' +$ step certificate verify ./certificate.crt --roots ./root-certificate.crt +''' + +Verify a certificate using a custom list of root certificates for path validation: + +''' +$ step certificate verify ./certificate.crt --roots "./root-certificate.crt,./root-certificate2.crt,/root-certificate3.crt" +''' + +Verify a certificate using a custom directory of root certificates for path validation: + +''' +$ step certificate verify ./certificate.crt --roots "./path/to/root-certificates/" +''' +`, Flags: []cli.Flag{ cli.StringFlag{ Name: "host", @@ -32,17 +65,19 @@ command will produce a non-zero return value. }, cli.StringFlag{ Name: "roots", - Usage: `Root certificates to use in the path validation algorithm. + Usage: `Root certificate(s) that will be used to verify the +authenticity of the remote server. - ROOTS is a string containing a (FILE | LIST of FILES | DIRECTORY) defined in one of the following ways: - FILE - Relative or full path to a file. All certificates in the file will be used for path validation. - LIST of Files - Comma-separated list of relative or full file paths. Every PEM encoded certificate - from each file will be used for path validation. - DIRECTORY - Relative or full path to a directory. Every PEM encoded certificate from each file - in the directory will be used for path validation.`, +: is a case-sensitive string and may be one of: + + **file** + : Relative or full path to a file. All certificates in the file will be used for path validation. + + **list of files** + : Comma-separated list of relative or full file paths. Every PEM encoded certificate from each file will be used for path validation. + + **directory** + : Relative or full path to a directory. Every PEM encoded certificate from each file in the directory will be used for path validation.`, }, }, } diff --git a/command/crypto/jwk/create.go b/command/crypto/jwk/create.go index e17f1532..e6bbc6b2 100644 --- a/command/crypto/jwk/create.go +++ b/command/crypto/jwk/create.go @@ -136,7 +136,7 @@ $ step crypto jwk create ed.pub.json ed.json \ Create a key from an existing PEM file: ''' -$ step crypto jwk create jwk.pub.json jwk.json +$ step crypto jwk create jwk.pub.json jwk.json --from-pem key.pem ''' diff --git a/command/crypto/keypair.go b/command/crypto/keypair.go index b7a1c6b0..c94d105e 100644 --- a/command/crypto/keypair.go +++ b/command/crypto/keypair.go @@ -13,66 +13,118 @@ import ( func createKeyPairCommand() cli.Command { return cli.Command{ - Name: "keypair", - Action: cli.ActionFunc(createAction), - Usage: "generate a public /private keypair in PEM format.", - UsageText: `step crypto keypair PUB_FILE PRIV_FILE [--type=TYPE] [--size=SIZE] [--curve=CURVE]`, - Description: `The 'step crypto keypair' command 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 in a CSR and - signed by a CA to produce a certificate. + Name: "keypair", + Action: cli.ActionFunc(createAction), + Usage: "generate a public /private keypair in PEM format.", + UsageText: `**step crypto keypair** +[**--curve**=] [**--no-password**] [**--size**=] +[**--type**=]`, + 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 +in a CSR and signed by a CA to produce a certificate. - Private keys are encrypted using a password. You'll be prompted for this password - automatically when the key is used. +Private keys are encrypted using a password. You'll be prompted for this +password automatically when the key is used. -POSITIONAL ARGUMENTS: - PUB_FILE - The path to write the public key. +## POSITIONAL ARGUMENTS: - PRIV_FILE - The path to write the private key.`, + +: The path to write the public key. + + +: The path to write the private key. + +## EXIT CODES + +This command returns 0 on success and \>0 if any error occurs. + +## EXAMPLES + +Create an RSA public / private key pair with 4096 bits: + +''' +$ step crypto keypair foo.pub foo.key --type RSA --size 4096 +''' + +Create an RSA public / private key with fewer than the recommended number of bits (recommended >= 2048 bits): + +''' +$ step crypto keypair foo.pub foo.key --type RSA --size 1024 --insecure +''' + +Create an EC public / private key pair with curve P-521: + +''' +$ step crypto keypair foo.pub foo.key --type EC --curve "P-521" +''' + +Create an EC public / private key pair but do not encrypt the private key file: + +''' +$ step crypto keypair foo.pub foo.key --type EC --curve "P-256" --no-password --insecure +''' + +Create an Octet Key Pair with curve Ed25519: + +''' +$ step crypto keypair foo.pub foo.key --type OKP --curve Ed25519 +''' +`, Flags: []cli.Flag{ cli.StringFlag{ - Name: "type", + Name: "kty, type", Value: "EC", - Usage: `The type of key to generate. + Usage: `The of key to create. Corresponds to the **"kty"** JWK parameter. +If unset, default is EC. - TYPE is a case-sensitive string and must be one of: - EC - Generate an asymmetric Elliptic Curve Key Pair. - RSA - Generate an asymmetric RSA (Rivest–Shamir–Adleman) Key Pair. - OKP - Generate an asymmetric Octet Key Pair.`, +: is a case-sensitive string and must be one of: + + **EC** + : Create an **elliptic curve** keypair + + **OKP** + : Create an octet key pair (for **"Ed25519"** curve) + + **RSA** + : Create an **RSA** keypair +`, }, cli.IntFlag{ Name: "size", - Usage: `The size (in bits) of the key for RSA and oct key types. RSA keys require a - minimum key size of 2048 bits.`, - Value: 2048, + Usage: `The (in bits) of the key for RSA and oct key types. RSA keys require a +minimum key size of 2048 bits. If unset, default is 2048 bits for RSA keys and 128 bits for oct keys.`, }, cli.StringFlag{ - Name: "crv, curve", - Value: "P-256", - Usage: `The elliptic curve to use for this keypair for EC and OKP key types. + Name: "crv, curve", + Usage: `The elliptic to use for EC and OKP key types. Corresponds +to the **"crv"** JWK parameter. Valid curves are defined in JWA [RFC7518]. If +unset, default is P-256 for EC keys and Ed25519 for OKP keys. - CURVE is a case-sensitive string and must be one of: - P-256 - NIST P-256 Curve; compatible with 'EC' key type only - P-384 - NIST P-384 Curve; compatible with 'EC' key type only - P-521 - NIST P-521 Curve; compatible with 'EC' key type only - Ed25519 - EdDSA Curve 25519; compatible with 'OKP' key type only`, +: is a case-sensitive string and must be one of: + + **P-256** + : NIST P-256 Curve + + **P-384** + : NIST P-384 Curve + + **P-521** + : NIST P-521 Curve + + **Ed25519** + : Ed25519 Curve +`, }, cli.BoolFlag{ Name: "insecure", Hidden: true, }, cli.BoolFlag{ - Name: "no-password", - Usage: `The directive to leave the private key unencrypted. This is not recommended.`, + 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.`, }, }, } @@ -89,37 +141,53 @@ func createAction(ctx *cli.Context) error { return errs.EqualArguments(ctx, "PUB_FILE", "PRIV_FILE") } - typ := ctx.String("type") - crv := ctx.String("crv") - if ctx.IsSet("crv") { - switch typ { - case "EC", "OKP": - default: - return errors.Errorf("key type '%s' is not compatible with flag '--crv'", typ) - } - } else { - switch typ { - // If crv not set and the key type is OKP then set cruve Ed25519. - // The cli assumes a default curve for EC key type. - case "OKP": - crv = "Ed25519" - } - } - if ctx.IsSet("size") && typ != "RSA" { - return errors.Errorf("key type '%s' is not compatible with flag '--size'", typ) - } - size := ctx.Int("size") insecure := ctx.Bool("insecure") noPass := ctx.Bool("no-password") - if noPass && !insecure { return errs.RequiredWithFlag(ctx, "insecure", "no-password") } - if size < 2048 && !insecure { - return errs.MinSizeInsecureFlag(ctx, "size", "2048") - } - if size <= 0 { - return errs.MinSizeFlag(ctx, "size", "0") + + var ( + crv = ctx.String("curve") + size = ctx.Int("size") + typ = ctx.String("type") + ) + switch typ { + case "RSA": + if size < 2048 && !insecure { + return errs.MinSizeInsecureFlag(ctx, "size", "2048") + } + if size <= 0 { + return errs.MinSizeFlag(ctx, "size", "0") + } + if ctx.IsSet("curve") { + return errs.IncompatibleFlagValue(ctx, "curve", "type", typ) + } + case "EC": + if ctx.IsSet("size") { + return errs.IncompatibleFlagValue(ctx, "size", "type", typ) + } + if !ctx.IsSet("curve") { + return errs.RequiredWithFlagValue(ctx, "type", typ, "curve") + } + switch crv { + case "P-256", "P-384", "P-521": //ok + default: + return errs.IncompatibleFlagValues(ctx, "curve", crv, "type", typ) + } + case "OKP": + if ctx.IsSet("size") { + return errs.IncompatibleFlagValue(ctx, "size", "type", typ) + } + if !ctx.IsSet("curve") { + return errs.RequiredWithFlagValue(ctx, "type", typ, "curve") + } + switch crv { + case "Ed25519": //ok + default: + return errs.IncompatibleFlagValues(ctx, "curve", crv, "type", typ) + } + default: } pub, priv, err := keys.GenerateKeyPair(typ, crv, size) diff --git a/command/oauth/cmd.go b/command/oauth/cmd.go index 9452c128..8caea273 100644 --- a/command/oauth/cmd.go +++ b/command/oauth/cmd.go @@ -20,7 +20,6 @@ import ( "github.com/smallstep/cli/command" "github.com/smallstep/cli/crypto/randutil" - "github.com/smallstep/cli/errs" "github.com/smallstep/cli/exec" jose "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" @@ -142,7 +141,7 @@ func oauthCmd(c *cli.Context) error { Console: c.Bool("console"), } if err := opts.Validate(); err != nil { - return errs.UsageExitError(c, err) + return err } if (opts.Provider != "google" || c.IsSet("authorization-endpoint")) && !c.IsSet("client-id") { return errors.New("flag '--client-id' required with '--provider'") @@ -206,7 +205,7 @@ func oauthCmd(c *cli.Context) error { o, err := newOauth(opts.Provider, clientID, clientSecret, authzEp, tokenEp, scope, opts.Email) if err != nil { - return errs.ToError(err) + return err } var tok *token @@ -223,7 +222,7 @@ func oauthCmd(c *cli.Context) error { } if err != nil { - return errs.ToError(err) + return err } if c.Bool("header") { diff --git a/crypto/certificates/x509/crt.go b/crypto/certificates/x509/crt.go index ef73ba75..f6011e75 100644 --- a/crypto/certificates/x509/crt.go +++ b/crypto/certificates/x509/crt.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/pkg/errors" + "github.com/smallstep/cli/errs" ) // WriteCertificate encodes a x509 Certificate to a file on disk in PEM format. @@ -19,8 +20,7 @@ func WriteCertificate(crt []byte, out string) error { certOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(0644)) if err != nil { - return errors.Wrapf(err, - "failed to open '%s' for writing", out) + return errs.FileError(err, out) } err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: crt}) if err != nil { @@ -35,7 +35,7 @@ func WriteCertificate(crt []byte, out string) error { func LoadCertificate(crtPath string) (*x509.Certificate, *pem.Block, error) { publicBytes, err := ioutil.ReadFile(crtPath) if err != nil { - return nil, nil, errors.Wrapf(err, "error opening certificate file %s", crtPath) + return nil, nil, errs.FileError(err, crtPath) } publicPEM, _ := pem.Decode(publicBytes) if publicPEM == nil { @@ -63,7 +63,7 @@ func ReadCertPool(path string) (*x509.CertPool, error) { if info.IsDir() { finfos, err := ioutil.ReadDir(path) if err != nil { - return nil, errors.WithStack(err) + return nil, errs.FileError(err, path) } for _, finfo := range finfos { files = append(files, filepath.Join(path, finfo.Name())) @@ -76,7 +76,7 @@ func ReadCertPool(path string) (*x509.CertPool, error) { for _, f := range files { bytes, err := ioutil.ReadFile(f) if err != nil { - return nil, errors.WithStack(err) + return nil, errs.FileError(err, f) } for len(bytes) > 0 { var block *pem.Block diff --git a/crypto/certificates/x509/crt_test.go b/crypto/certificates/x509/crt_test.go index fdc5e69e..462c5ab2 100644 --- a/crypto/certificates/x509/crt_test.go +++ b/crypto/certificates/x509/crt_test.go @@ -5,10 +5,8 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" - "fmt" "io/ioutil" "net" - "strings" "testing" "time" @@ -35,7 +33,7 @@ func Test_WriteCertificate(t *testing.T) { return []byte{}, nil }, crtOut: "./fakeDir/test.crt", - err: errors.New("failed to open './fakeDir/test.crt' for writing: open ./fakeDir/test.crt: no such file or directory"), + err: errors.New("open ./fakeDir/test.crt failed: no such file or directory"), }, "success": { crt: func() ([]byte, error) { @@ -130,28 +128,38 @@ func Test_LoadCertificate(t *testing.T) { testCert = "./test_files/ca.crt" ) - e1 := fmt.Sprintf("error decoding certificate file %s", testBadPEMCert) - e2 := fmt.Sprintf("error parsing x509 certificate file %s", testBadCert) - - tests := []struct { - crtPath string - expectedError string + tests := map[string]struct { + crtPath string + err error }{ - {"", "error opening certificate file "}, - {"", "error opening certificate file "}, - {testBadPEMCert, e1}, - {testBadCert, e2}, + "certificate file does not exist": { + crtPath: "", + err: errors.New("open failed: no such file or directory"), + }, + "certificate poorly formatted - PEM decode failure": { + crtPath: testBadPEMCert, + err: errors.New("error decoding certificate file"), + }, + "certificate parse failure": { + crtPath: testBadCert, + err: errors.New("error parsing x509 certificate file"), + }, + "success": { + crtPath: testCert, + }, } - for i, tc := range tests { - _, _, err := LoadCertificate(tc.crtPath) - if assert.Error(t, err) { - assert.True(t, strings.HasPrefix(err.Error(), tc.expectedError), err.Error(), i) + for name, test := range tests { + t.Logf("Running test case: %s", name) + + crt, block, err := LoadCertificate(test.crtPath) + if err != nil { + if assert.NotNil(t, test.err) { + assert.HasPrefix(t, err.Error(), test.err.Error()) + } + } else { + assert.Equals(t, crt.Subject.CommonName, "internal.smallstep.com") + assert.NotNil(t, block) } } - - crt, pemBlock, err := LoadCertificate(testCert) - assert.FatalError(t, err) - assert.Equals(t, crt.Subject.CommonName, "internal.smallstep.com") - assert.NotNil(t, pemBlock) } diff --git a/errs/errs.go b/errs/errs.go index 32c22b3c..52ed3127 100644 --- a/errs/errs.go +++ b/errs/errs.go @@ -9,30 +9,12 @@ import ( "github.com/urfave/cli" ) -// err errExitCode is the default exit code when an error occurs. -const errExitCode = 1 - -// ErrTooFewArgs occurs when too few arguments were provided by the user -var ErrTooFewArgs = NewError("Not enough arguments were provided") - -// ErrTooManyArgs occurs when too many arguments were provided by the user -var ErrTooManyArgs = NewError("Too many arguments were provided") - -// ErrMissingArgs occurs when one or more arguments are missing -var ErrMissingArgs = NewError("An incorrect number of arguments were provided") - -// ErrMissingToken occurs when a STEP_TOKEN or --token flag is not provided -var ErrMissingToken = NewError("A one-time token must be provided to bootstrap the identity via the `--token` flag or `$STEP_TOKEN` environment variable") - -// ErrMissingCAURL occurs when a STEP_CA_URL or --ca-url flag is not provided -var ErrMissingCAURL = NewError("The CA URL must be provided through the --ca-url flag or `$STEP_CA_URL` environment variable") - // NewError returns a new Error for the given format and arguments func NewError(format string, args ...interface{}) error { return errors.Errorf(format, args...) } -// NewExitError returns an error than the urfave/cli package will handle and +// NewExitError returns an error that the urfave/cli package will handle and // will show the given error and exit with the given code. func NewExitError(err error, exitCode int) error { return cli.NewExitError(err, exitCode) @@ -54,38 +36,6 @@ func Wrap(err error, format string, args ...interface{}) error { return errors.Wrapf(cause, format, args...) } -// UsageExitError prints out the usage error followed by the help documentation -// for the command -func UsageExitError(c *cli.Context, err error) error { - msg := fmt.Sprintf("Error: %s\n\n%s", err.Error(), usageString(c)) - return cli.NewExitError(msg, errExitCode) -} - -// UnexpectedExitError wraps the error denoting that it was unexpected -func UnexpectedExitError(err error) error { - msg := fmt.Sprintf("Error: An unexpected error was encountered: %s", err.Error()) - return cli.NewExitError(msg, errExitCode) -} - -// ToError transforms the given error into our frameworks error type -func ToError(err error) error { - switch err.(type) { - case nil: - return nil - default: - return cli.NewExitError(prependErrorMsg(err), errExitCode) - } -} - -func prependErrorMsg(err error) string { - m := err.Error() - if strings.HasPrefix(m, "Error:") { - return m - } - - return "Error: " + m -} - // InsecureCommand returns an error with a message saying that the current // command requires the insecure flag. func InsecureCommand(ctx *cli.Context) error { @@ -177,6 +127,22 @@ func IncompatibleFlag(ctx *cli.Context, flag string, value string) error { return errors.Errorf("flag '--%s' is incompatible with '%s'", flag, value) } +// IncompatibleFlagValue returns an error with the flag being incompatible with the +// given value. +func IncompatibleFlagValue(ctx *cli.Context, flag, incompatibleWith, + incompatibleWithValue string) error { + return errors.Errorf("flag '--%s' is incompatible with flag '--%s %s'", + flag, incompatibleWith, incompatibleWithValue) +} + +// IncompatibleFlagValues returns an error with the flag being incompatible with the +// given value. +func IncompatibleFlagValues(ctx *cli.Context, flag, value, incompatibleWith, + incompatibleWithValue string) error { + return errors.Errorf("flag '--%s %s' is incompatible with flag '--%s %s'", + flag, value, incompatibleWith, incompatibleWithValue) +} + // RequiredFlag returns an error with the required flag message. func RequiredFlag(ctx *cli.Context, flag string) error { return errors.Errorf("'%s %s' requires the '--%s' flag", ctx.App.HelpName, @@ -184,8 +150,13 @@ func RequiredFlag(ctx *cli.Context, flag string) error { } // RequiredWithFlag returns an error with the required flag message with another flag. -func RequiredWithFlag(ctx *cli.Context, required, with string) error { - return errors.Errorf("flag '--%s' requires the '--%s' flag", required, with) +func RequiredWithFlag(ctx *cli.Context, flag, required string) error { + return errors.Errorf("flag '--%s' requires the '--%s' flag", flag, required) +} + +// RequiredWithFlagValue returns an error with the required flag message. +func RequiredWithFlagValue(ctx *cli.Context, flag, value, required string) error { + return errors.Errorf("'--%s %s' requires the '--%s' flag", flag, value, required) } // RequiredInsecureFlag returns an error with the required flag message unless @@ -237,11 +208,6 @@ func usage(ctx *cli.Context) string { return strings.Replace(lines[0], "**", "", -1) } -// usageString returns the command usage prepended by the string "Usage: ". -func usageString(ctx *cli.Context) string { - return "Usage: " + usage(ctx) -} - // FileError is a wrapper for errors of the os package. func FileError(err error, filename string) error { switch e := errors.Cause(err).(type) {