diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 00000000..01ad40d1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,11 @@ +--- +name: CLI Enhancement +about: Suggest an enhancement to step cli +labels: area/cert-management enhancement +--- + +### What would you like to be added + + +### Why this is needed + diff --git a/.gitignore b/.gitignore index 515d07fd..50d71a24 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ coverage.txt output vendor step + +# Ignore modules until switch from gopkg +go.mod +go.sum diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..fa27f581 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,67 @@ +linters-settings: + govet: + check-shadowing: true + settings: + printf: + funcs: + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + golint: + min-confidence: 0 + gocyclo: + min-complexity: 10 + maligned: + suggest-new: true + dupl: + threshold: 100 + goconst: + min-len: 2 + min-occurrences: 2 + depguard: + list-type: blacklist + packages: + # logging is allowed only by logutils.Log, logrus + # is allowed to use only in logutils package + - github.com/sirupsen/logrus + misspell: + locale: US + lll: + line-length: 140 + goimports: + local-prefixes: github.com/golangci/golangci-lint + gocritic: + enabled-tags: + - performance + - style + - experimental + disabled-checks: + - wrapperFunc + - dupImport # https://github.com/go-critic/go-critic/issues/845 + +linters: + disable-all: true + enable: + - gofmt + - golint + - vet + - misspell + - ineffassign + - deadcode + +run: + skip-dirs: + - pkg + +issues: + exclude: + - can't lint + - declaration of "err" shadows declaration at line + - should have a package comment, unless it's in another file for this package +# golangci.com configuration +# https://github.com/golangci/golangci/wiki/Configuration +service: + golangci-lint-version: 1.17.x # use the fixed version to not introduce new linters unexpectedly + prepare: + - echo "here I can run custom commands, but no preparation needed for this repo" diff --git a/Gopkg.lock b/Gopkg.lock index 386e384f..0e265dc7 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -9,14 +9,6 @@ pruneopts = "UT" revision = "e2d15f34fcf99d5dbb871c820ec73f710fca9815" -[[projects]] - branch = "master" - digest = "1:c10265d5a71326618d37e97169eddb3582f78e8ac7dcf87403b4cf619efd519a" - name = "github.com/DHowett/go-plist" - packages = ["."] - pruneopts = "UT" - revision = "591f970eefbbeb04d7b37f334a0c4c3256e32876" - [[projects]] branch = "master" digest = "1:655f3b07160fbe90713062296ef215c096ad4308bdc0081620cacd9b9d46dce5" @@ -25,21 +17,6 @@ pruneopts = "UT" revision = "5482f03509440585d13d8f648989e05903001842" -[[projects]] - digest = "1:304cb78c285eaf02ab529ad02a257cad9b4845022915e6c82f87860ac53222d8" - name = "github.com/alecthomas/gometalinter" - packages = ["."] - pruneopts = "UT" - revision = "bae2f1293d092fd8167939d5108d1b025eaef9de" - -[[projects]] - branch = "master" - digest = "1:c198fdc381e898e8fb62b8eb62758195091c313ad18e52a3067366e1dda2fb3c" - name = "github.com/alecthomas/units" - packages = ["."] - pruneopts = "UT" - revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" - [[projects]] digest = "1:320e7ead93de9fd2b0e59b50fd92a4d50c1f8ab455d96bc2eb083267453a9709" name = "github.com/asaskevich/govalidator" @@ -68,17 +45,6 @@ pruneopts = "UT" revision = "2972be24d48e78746da79ba8e24e8b488c9880de" -[[projects]] - digest = "1:848ef40f818e59905140552cc49ff3dc1a15f955e4b56d1c5c2cc4b54dbadf0c" - name = "github.com/client9/misspell" - packages = [ - ".", - "cmd/misspell", - ] - pruneopts = "UT" - revision = "b90dc15cfd220ecf8bbc9043ecb928cef381f011" - version = "v0.3.4" - [[projects]] branch = "master" digest = "1:cc439e1d9d8cff3d575642f5401033b00f2b8d0cd9f859db45604701c990879a" @@ -134,17 +100,6 @@ revision = "72cd26f257d44c1114970e19afddcd812016007e" version = "v1.4.1" -[[projects]] - branch = "travis-1.9" - digest = "1:e8f5d9c09a7209c740e769713376abda388c41b777ba8e9ed52767e21acf379f" - name = "github.com/golang/lint" - packages = [ - ".", - "golint", - ] - pruneopts = "UT" - revision = "883fe33ffc4344bad1ecd881f61afd5ec5d80e0a" - [[projects]] digest = "1:318f1c959a8a740366fce4b1e1eb2fd914036b4af58fbd0a003349b305f118ad" name = "github.com/golang/protobuf" @@ -169,22 +124,6 @@ revision = "3629d6846518309d22c16fee15d1007262a459d2" version = "v1.0.21" -[[projects]] - branch = "master" - digest = "1:750e747d0aad97b79f4a4e00034bae415c2ea793fd9e61438d966ee9c79579bf" - name = "github.com/google/shlex" - packages = ["."] - pruneopts = "UT" - revision = "6f45313302b9c56850fc17f99e40caebce98c716" - -[[projects]] - branch = "master" - digest = "1:824d147914b40e56e9e1eebd602bc6bb9761989d52fd8e4a498428467980eb17" - name = "github.com/gordonklaus/ineffassign" - packages = ["."] - pruneopts = "UT" - revision = "1003c8bd00dc2869cb5ca5282e6ce33834fed514" - [[projects]] branch = "master" digest = "1:22725c01ecd8ed0c0f0078944305a57053340d92878b02db925c660cc4accf64" @@ -302,27 +241,6 @@ revision = "f5bce3387232559bcbe6a5f8227c4bf508dac1ba" version = "v1.11.0" -[[projects]] - digest = "1:07140002dbf37da92090f731b46fa47be4820b82fe5c14a035203b0e813d0ec2" - name = "github.com/nicksnyder/go-i18n" - packages = [ - "i18n", - "i18n/bundle", - "i18n/language", - "i18n/translation", - ] - pruneopts = "UT" - revision = "0dc1626d56435e9d605a29875701721c54bc9bbd" - version = "v1.10.0" - -[[projects]] - digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e" - name = "github.com/pelletier/go-toml" - packages = ["."] - pruneopts = "UT" - revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194" - version = "v1.2.0" - [[projects]] digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b" name = "github.com/pkg/errors" @@ -410,11 +328,11 @@ [[projects]] branch = "master" - digest = "1:c3207093bfee46dc9f408a55408d6fa6ed59431bdeb54df2ab89ffa1d8e1bfaf" + digest = "1:f4d37f61cbbd5adb7066017d7e5f303b722a39c3408b41d46a5ea04f81adba8c" name = "github.com/smallstep/certinfo" packages = ["."] pruneopts = "UT" - revision = "fef09aeb6b3b6451151ae248670cf020454c0d5b" + revision = "203093530c86c19d79cfe5ce9ad0b8897e3cce9b" [[projects]] branch = "master" @@ -432,15 +350,15 @@ [[projects]] branch = "master" - digest = "1:4bde64565730a308d3cebca9b93f19c8e1137f9ba5b57174669ad0f732dec044" + digest = "1:f41de3b55032e81c12f4d109e6c5222e1cff573197a3652b800ca8ac2aaada35" name = "github.com/smallstep/truststore" packages = ["."] pruneopts = "UT" - revision = "b8300b931ab584b7aa01fe43b3c92d5a61cf2ce3" + revision = "8418f8a7d0b74e79026254b4ad23c67dd77fe5f0" [[projects]] branch = "master" - digest = "1:167fb96bb586d7abc0714a63d5bd30f24563d369012b295268db4d64008c4f7d" + digest = "1:822ad7c8c41fe68fb9c9c95ad7e77a1172e216d9e3e527451819927448b6dee6" name = "github.com/smallstep/zcrypto" packages = [ "json", @@ -449,7 +367,7 @@ "x509/pkix", ] pruneopts = "UT" - revision = "0eaa490bf930eb2c8f1fd0dec8750619588aadae" + revision = "6bab21fcaafc3d150cf793b6d5f25fe32f49c80e" [[projects]] branch = "master" @@ -474,14 +392,6 @@ revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686" version = "v1.2.2" -[[projects]] - branch = "master" - digest = "1:ba52e5a5fb800ce55108b7a5f181bb809aab71c16736051312b0aa969f82ad39" - name = "github.com/tsenart/deadcode" - packages = ["."] - pruneopts = "UT" - revision = "210d2dc333e90c7e3eedf4f2242507a8e83ed4ab" - [[projects]] branch = "master" digest = "1:6743b69de0d73e91004e4e201cf4965b59a0fa5caf6f0ffbe0cb9ee8807738a7" @@ -617,13 +527,6 @@ revision = "54a98f90d1c46b7731eb8fb305d2a321c30ef610" version = "v1.5.0" -[[projects]] - digest = "1:39efb07a0d773dc09785b237ada4e10b5f28646eb6505d97bc18f8d2ff439362" - name = "gopkg.in/alecthomas/kingpin.v3-unstable" - packages = ["."] - pruneopts = "UT" - revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306" - [[projects]] digest = "1:9593bab40e981b1f90b7e07faeab0d09b75fe338880d08880f986a9d3283c53f" name = "gopkg.in/square/go-jose.v2" @@ -638,23 +541,19 @@ version = "v2.3.1" [[projects]] - digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" - name = "gopkg.in/yaml.v2" + branch = "master" + digest = "1:c10265d5a71326618d37e97169eddb3582f78e8ac7dcf87403b4cf619efd519a" + name = "howett.net/plist" packages = ["."] pruneopts = "UT" - revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" - version = "v2.2.1" + revision = "591f970eefbbeb04d7b37f334a0c4c3256e32876" [solve-meta] analyzer-name = "dep" analyzer-version = 1 input-imports = [ "github.com/ThomasRooney/gexpect", - "github.com/alecthomas/gometalinter", "github.com/chzyer/readline", - "github.com/client9/misspell/cmd/misspell", - "github.com/golang/lint/golint", - "github.com/gordonklaus/ineffassign", "github.com/icrowley/fake", "github.com/manifoldco/promptui", "github.com/pkg/errors", @@ -674,7 +573,6 @@ "github.com/smallstep/zlint", "github.com/stretchr/testify/assert", "github.com/stretchr/testify/require", - "github.com/tsenart/deadcode", "github.com/urfave/cli", "golang.org/x/crypto/argon2", "golang.org/x/crypto/bcrypt", diff --git a/Gopkg.toml b/Gopkg.toml index eb2499fa..9927d62f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -23,18 +23,6 @@ # non-go = false # go-tests = true # unused-packages = true -required = [ - "github.com/alecthomas/gometalinter", - "github.com/golang/lint/golint", - "github.com/client9/misspell/cmd/misspell", - "github.com/gordonklaus/ineffassign", - "github.com/tsenart/deadcode", -] - -[[constraint]] - name = "github.com/alecthomas/gometalinter" - revision = "bae2f1293d092fd8167939d5108d1b025eaef9de" - [[override]] name = "gopkg.in/alecthomas/kingpin.v3-unstable" revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306" diff --git a/README.md b/README.md index a381a7f1..87c5cf2d 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,6 @@ # Step CLI -`step` is a zero trust swiss army knife. It's an easy-to-use and hard-to-misuse -utility for building, operating, and automating systems that use zero trust -technologies like authenticated encryption (X.509, TLS), single sign-on (OAuth -OIDC, SAML), multi-factor authentication (OATH OTP, FIDO U2F), -encryption mechanisms (JSON Web Encryption, NaCl), and verifiable -claims (JWT, SAML assertions). +`step` is a zero trust swiss army knife that integrates with [`step-ca`](https://github.com/smallstep/certificates) for automated certificate management. It's an easy-to-use and hard-to-misuse utility for building, operating, and automating systems that use zero trust technologies like authenticated encryption (X.509, TLS), single sign-on (OAuth OIDC, SAML), multi-factor authentication (OATH OTP, FIDO U2F), encryption mechanisms (JSON Web Encryption, NaCl), and verifiable claims (JWT, SAML assertions). [Website](https://smallstep.com) | [Documentation](https://smallstep.com/docs/cli) | @@ -26,6 +21,52 @@ claims (JWT, SAML assertions). ![Animated terminal showing step in practice](https://smallstep.com/images/blog/2018-08-07-unfurl.gif) +## Features + +`step` is a powerful security tool that's been carefully designed to be safe and easy to use, even if you don't have a favorite elliptic curve or if you're inclined to forget to check the `aud` when you verify a JWT. + +- Safe and sane defaults everywhere encourage best practices by making the right thing easy +- Insecure or subtle operations are gated with flags to prevent accidental misuse +- In-depth help with examples is available via `step help` + +### Work with [JWTs](https://jwt.io) ([RFC7519](https://tools.ietf.org/html/rfc7519)) and [other JOSE constructs](https://datatracker.ietf.org/wg/jose/documents/) + +- [Sign](https://smallstep.com/docs/cli/crypto/jwt/sign), [verify](https://smallstep.com/docs/cli/crypto/jwt/verify), and [inspect](https://smallstep.com/docs/cli/crypto/jwt/inspect) JSON Web Tokens (JWTs) +- [Sign](https://smallstep.com/docs/cli/crypto/jws/sign), [verify](https://smallstep.com/docs/cli/crypto/jws/verify), and [inspect](https://smallstep.com/docs/cli/crypto/jws/inspect/) arbitrary data using JSON Web Signature (JWS) +- [Encrypt](https://smallstep.com/docs/cli/crypto/jwe/encrypt/) and [decrypt](https://smallstep.com/docs/cli/crypto/jwe/decrypt/) data and wrap private keys using JSON Web Encryption (JWE) +- [Create JWKs](https://smallstep.com/docs/cli/crypto/jwk/create/) and [manage key sets](https://smallstep.com/docs/cli/crypto/jwk/keyset) for use with JWT, JWE, and JWS + +### Work with X.509 (TLS/HTTPS) certificates + +- Create key pairs (RSA, ECDSA, EdDSA) and certificate signing requests (CSRs) +- Create [RFC5280](https://tools.ietf.org/html/rfc5280) and [CA/Browser Forum](https://cabforum.org/baseline-requirements-documents/) compliant X.509 certificates that work **for TLS and HTTPS** +- [Create](https://smallstep.com/docs/cli/certificate/create/) root and intermediate signing certificates (CA certificates) +- Create self-signed & CA-signed certificates, and [sign CSRs](https://smallstep.com/docs/cli/certificate/sign/) +- [Inspect](https://smallstep.com/docs/cli/certificate/inspect/) and [lint](https://smallstep.com/docs/cli/certificate/lint/) certificates on disk or in use by a remote server +- [Install root certificates](https://smallstep.com/docs/cli/certificate/install/) so your CA is trusted by default (issue development certificates **that [work in browsers](https://smallstep.com/blog/step-v0-8-6-valid-HTTPS-certificates-for-dev-pre-prod.html)**) +- Get certificates from any ACME compliant CA (*coming soon*) + +### Connect to [`step-ca`](https://github.com/smallstep/certificates) and get certificates from your own private certificate authority + +- [Authenticate and obtain a certificate](https://smallstep.com/docs/cli/ca/certificate/) using any enrollment mechanism supported by `step-ca` +- Securely [distribute root certificates](https://smallstep.com/docs/cli/ca/root/) and [bootstrap](https://smallstep.com/docs/cli/ca/bootstrap/) PKI relying parties +- [Renew](https://smallstep.com/docs/cli/ca/renew/) and [revoke](https://smallstep.com/docs/cli/ca/revoke/) certificates issued by `step-ca` +- [Submit CSRs](https://smallstep.com/docs/cli/ca/sign/) to be signed by `step-ca` + +### Command line OAuth and MFA + +- [Get OAuth access tokens](https://smallstep.com/docs/cli/oauth/) and OIDC identity tokens at the command line from any provider +- Supports OAuth authorization code, implicit, OOB, jwt-bearer, and refresh token flows +- Automatically launch browser to complete OAuth flow (or use console flow) +- Verify OIDC identity tokens (using `step crypt jwt verify`) +- [Generate and verify](https://smallstep.com/docs/cli/crypto/otp/) TOTP tokens + +### NaCl and other crypto utilities + +- [Work with NaCl](https://smallstep.com/docs/cli/crypto/nacl/) box, secretbox, and sign constructs +- [Apply key derivation functions](https://smallstep.com/docs/cli/crypto/kdf/) (KDFs) and [verify passwords](https://smallstep.com/docs/cli/crypto/kdf/compare/) using `scrypt`, `bcrypt`, and `argo2` +- Generate and check [file hashes](https://smallstep.com/docs/cli/crypto/hash/) + ## Installation Guide These instructions will install an OS specific version of the `step` binary on @@ -36,17 +77,13 @@ development](docs/local-development.md) below. Install `step` via [Homebrew](https://brew.sh/): -

-$ brew install step
-
+
$ brew install step
> Note: If you have installed `step` previously through the `smallstep/smallstep` > tap you will need to run the following commands before installing: - -

-$ brew untap smallstep/smallstep
-$ brew uninstall step
-
+> +>
$ brew untap smallstep/smallstep
+> $ brew uninstall step
### Linux @@ -54,12 +91,10 @@ Install `step` via [Homebrew](https://brew.sh/): Download and install the latest Debian package from [releases](https://github.com/smallstep/cli/releases): -

-$ wget https://github.com/smallstep/cli/releases/download/X.Y.Z/step_X.Y.Z_amd64.deb
+
$ wget https://github.com/smallstep/cli/releases/download/X.Y.Z/step_X.Y.Z_amd64.deb
 
 # Install the Debian package:
-$ sudo dpkg -i step_X.Y.Z_amd64.deb
-
+$ sudo dpkg -i step_X.Y.Z_amd64.deb
#### Arch Linux @@ -73,8 +108,7 @@ a sibling repository) can be found [here](https://aur.archlinux.org/packages/ste You can use [pacman](https://www.archlinux.org/pacman/) to install the packages. ### Test -

-$ step certificate inspect https://smallstep.com
+
$ step certificate inspect https://smallstep.com
 Certificate:
     Data:
         Version: 3 (0x2)
@@ -85,18 +119,86 @@ Certificate:
             Not Before: Feb 8 13:07:44 2019 UTC
             Not After : May 9 13:07:44 2019 UTC
         Subject: CN=smallstep.com
-[...]
-
+[...]
## Examples +### X.509 Certificates from `step-ca` + +This example assumes you already have [`step-ca`](https://github.com/smallstep/certificates) running at `https://ca.local`. + +Get your root certificate fingerprint from the machine running `step-ca`: + +
ca$ step certificate fingerprint $(step path)/certs/root_ca.crt
+0eea955785796f0a103637df88f29d8dfc8c1f4260f35c8e744be155f06fd82d
+ +Bootstrap a new machine to trust and connect to `step-ca`: + +
$ step ca bootstrap --ca-url https://ca.local \
+                    --fingerprint 0eea955785796f0a103637df88f29d8dfc8c1f4260f35c8e744be155f06fd82d
+ +Create a key pair, generate a CSR, and get a certificate from `step-ca`: + +
$ step ca certificate foo.local foo.crt foo.key
+Use the arrow keys to navigate: ↓ ↑ → ←
+What provisioner key do you want to use?
+  ▸ bob@smallstep.com (JWK) [kid: XXX]
+    Google (OIDC) [client: XXX.apps.googleusercontent.com]
+    Auth0 (OIDC) [client: XXX]
+    AWS IID Provisioner (AWS)
+✔ CA: https://ca.local
+✔ Certificate: foo.crt
+✔ Private Key: foo.key
+ + +Use `step certificate inspect` to check our work: + +
$ step certificate inspect --short foo.crt
+X.509v3 TLS Certificate (ECDSA P-256) [Serial: 2982...2760]
+  Subject:     foo.local
+  Issuer:      Intermediate CA
+  Provisioner: bob@smallstep.com [ID: EVct...2B-I]
+  Valid from:  2019-08-31T00:14:50Z
+          to:  2019-09-01T00:14:50Z
+ +Renew certificate: + +
$ step ca renew foo.crt foo.key --force
+Your certificate has been saved in foo.crt.
+ +Revoke certificate: + +
$ step ca revoke --cert foo.crt --key foo.key
+✔ CA: https://ca.local
+Certificate with Serial Number 202784089649824696691681223134769107758 has been revoked.
+
+$ step ca renew foo.crt foo.key --force
+error renewing certificate: Unauthorized
+ +You can install your root certificate locally: + +
$ step certificate install $(step path)/certs/root_ca.crt
+ +And issued certificates will work in your browser and with tools like `curl`. See [our blog post](https://smallstep.com/blog/step-v0-8-6-valid-HTTPS-certificates-for-dev-pre-prod.html) for more info. + +![Browser demo of HTTPS working without warnings](https://smallstep.com/images/blog/2019-02-25-localhost-tls.png) + +Alternatively, for internal service-to-service communication, you can [configure your code and infrastructure to trust your root certificate](https://github.com/smallstep/autocert/tree/master/examples/hello-mtls). + ### X.509 Certificates +The `step certificate` command group can also be used to create an offline CA and self-signed certificates. + +Create a self-signed certificate: + +
$ step certificate create foo.local foo.crt foo.key --profile self-signed --subtle
+Your certificate has been saved in foo.crt.
+Your private key has been saved in foo.key.
+ Create a root CA, an intermediate, and a leaf X.509 certificate. Bundle the leaf with the intermediate for use with TLS: -

-$ step certificate create --profile root-ca \
+
$ step certificate create --profile root-ca \
      "Example Root CA" root-ca.crt root-ca.key
 Please enter the password to encrypt the private key:
 Your certificate has been saved in root-ca.crt.
@@ -120,41 +222,23 @@ Your private key has been saved in example.com.key.
 
 $ step certificate bundle \
      example.com.crt intermediate-ca.crt example.com-bundle.crt
-Your certificate has been saved in example.com-bundle.crt.
-
+Your certificate has been saved in example.com-bundle.crt.
Extract the expiration date from a certificate (requires [`jq`](https://stedolan.github.io/jq/)): -

-$ step certificate inspect example.com.crt --format json | jq -r .validity.end
+
$ step certificate inspect example.com.crt --format json | jq -r .validity.end
 2019-02-28T17:46:16Z
 
 $ step certificate inspect https://smallstep.com --format json | jq -r .validity.end
-2019-05-09T13:07:44Z
-
- -You can install your root certificate locally: - -``` -$ step certificate install root-ca.crt -``` - -And issued certificates will work in your browser and with tools like `curl`. See [our blog post](https://smallstep.com/blog/step-v0-8-6-valid-HTTPS-certificates-for-dev-pre-prod.html) for more info. - -![Browser demo of HTTPS working without warnings](https://smallstep.com/images/blog/2019-02-25-localhost-tls.png) - -Alternatively, for internal service-to-service communication, you can [configure your code and infrastructure to trust your root certificate](https://github.com/smallstep/certificates/tree/master/autocert/examples/hello-mtls). - -If you need certificates for your microservices, containers, or other internal services see [step certificates](https://github.com/smallstep/certificates), a sub-project that adds an online certificate authority and automated certificate management tools to `step`. +2019-05-09T13:07:44Z
### JSON Object Signing & Encryption (JOSE) Create a [JSON Web Key](https://tools.ietf.org/html/rfc7517) (JWK), add the public key to a keyset, and sign a [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT): -

-$ step crypto jwk create pub.json key.json
+
$ step crypto jwk create pub.json key.json
 Please enter the password to encrypt the private JWK:
 Your public key has been saved in pub.json.
 Your private key has been saved in key.json.
@@ -187,16 +271,14 @@ Please enter the password to decrypt key.json:
     "sub": "subject@example.com"
   },
   "signature": "JU7fPGqBJcIfauJHA7KP9Wp292g_G9s4bLMVLyRgEQDpL5faaG-3teJ81_igPz1zP7IjHmz8D6Gigt7kbnlasw"
-}
-
+}
### Single Sign-On Login with Google, get an access token, and use it to make a request to Google's APIs: -

-$ curl -H"$(step oauth --header)" https://www.googleapis.com/oauth2/v3/userinfo
+
$ curl -H"$(step oauth --header)" https://www.googleapis.com/oauth2/v3/userinfo
 Your default web browser has been opened to visit:
 
 https://accounts.google.com/o/oauth2/v2/auth?client_id=1087160488420-AAAAAAAAAAAAAAA.apps.googleusercontent.com&code_challenge=XXXXX
@@ -207,13 +289,11 @@ https://accounts.google.com/o/oauth2/v2/auth?client_id=1087160488420-AAAAAAAAAAA
   "email": "bob@smallstep.com",
   "email_verified": true,
   "hd": "smallstep.com"
-}
-
+}
Login with Google and obtain an OAuth OIDC identity token for single sign-on: -

-$ step oauth \
+
$ step oauth \
     --provider https://accounts.google.com \
     --client-id 1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com \
     --client-secret udTrOT3gzrO7W9fDPgZQLfYJ \
@@ -222,13 +302,11 @@ Your default web browser has been opened to visit:
 
 https://accounts.google.com/o/oauth2/v2/auth?client_id=[...]
 
-xxx-google-xxx.yyy-oauth-yyy.zzz-token-zzz
-
+xxx-google-xxx.yyy-oauth-yyy.zzz-token-zzz
Obtain and verify a Google-issued OAuth OIDC identity token: -

-$ step oauth \
+
$ step oauth \
      --provider https://accounts.google.com \
      --client-id 1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com \
      --client-secret udTrOT3gzrO7W9fDPgZQLfYJ \
@@ -260,26 +338,21 @@ https://accounts.google.com/o/oauth2/v2/auth?client_id=[...]
     "exp": 1551296734
   },
   "signature": "[...]"
-}
-
+}
### Multi-factor Authentication Generate a [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm) token and a QR code: -

-$ step crypto otp generate \
+
$ step crypto otp generate \
     --issuer smallstep.com --account name@smallstep.com \
-    --qr smallstep.png > smallstep.totp
-
+ --qr smallstep.png > smallstep.totp
Scan the QR Code (`smallstep.png`) using Google Authenticator, Authy or similar software and use it to verify the TOTP token: -

-$ step crypto otp verify --secret smallstep.totp
-
+
$ step crypto otp verify --secret smallstep.totp
## Documentation diff --git a/command/ca/bootstrap.go b/command/ca/bootstrap.go index 7069522b..4a61c359 100644 --- a/command/ca/bootstrap.go +++ b/command/ca/bootstrap.go @@ -32,7 +32,7 @@ Bootstrap will store the root certificate in <$STEPPATH/certs/root_ca.crt> and create a configuration file in <$STEPPATH/configs/defaults.json> with the CA url, the root certificate location and its fingerprint. -After the bootstrap, ca commands do not need to specify the flags +After the bootstrap, ca commands do not need to specify the flags --ca-url, --root or --fingerprint if we want to use the same environment.`, Flags: []cli.Flag{ caURLFlag, @@ -76,11 +76,11 @@ func bootstrapAction(ctx *cli.Context) error { return errors.Wrap(err, "error downloading root certificate") } - if err := os.MkdirAll(filepath.Dir(rootFile), 0700); err != nil { + if err = os.MkdirAll(filepath.Dir(rootFile), 0700); err != nil { return errs.FileError(err, rootFile) } - if err := os.MkdirAll(filepath.Dir(configFile), 0700); err != nil { + if err = os.MkdirAll(filepath.Dir(configFile), 0700); err != nil { return errs.FileError(err, configFile) } diff --git a/command/ca/certificate.go b/command/ca/certificate.go index 7635fd31..d1af38c7 100644 --- a/command/ca/certificate.go +++ b/command/ca/certificate.go @@ -20,9 +20,9 @@ func certificateCommand() cli.Command { Action: command.ActionFunc(certificateAction), Usage: "generate a new private key and certificate signed by the root certificate", UsageText: `**step ca certificate** - [**--token**=] [**--issuer**=] [**--ca-url**=] [**--root**=] - [**--not-before**=] [**--not-after**=] - [**--san**=]`, +[**--token**=] [**--issuer**=] [**--ca-url**=] [**--root**=] +[**--not-before**=] [**--not-after**=] [**--san**=] +[**--kty**=] [**--curve**=] [**--size**=] [**--console**]`, Description: `**step ca certificate** command generates a new certificate pair ## POSITIONAL ARGUMENTS @@ -72,6 +72,16 @@ $ step ca certificate --offline internal.example.com internal.crt internal.key Request a new certificate using an OIDC provisioner: ''' $ step ca certificate --token $(step oauth --oidc --bare) joe@example.com joe.crt joe.key +''' + +Request a new certificate using an OIDC provisioner while remaining in the console: +''' +$ step ca certificate joe@example.com joe.crt joe.key --issuer Google --console +''' + +Request a new certificate with an RSA public key (default is ECDSA256): +''' +$ step ca certificate foo.internal foo.crt foo.key --kty RSA --size 4096 '''`, Flags: []cli.Flag{ tokenFlag, @@ -82,15 +92,22 @@ $ step ca certificate --token $(step oauth --oidc --bare) joe@example.com joe.cr notAfterCertFlag, cli.StringSliceFlag{ Name: "san", - Usage: `Add DNS or IP Address Subjective Alternative Names (SANs) that the token is -authorized to request. A certificate signing request using this token must match -the complete set of subjective alternative names in the token 1:1. Use the '--san' -flag multiple times to configure multiple SANs. The '--san' flag and the '--token' -flag are mutually exlusive.`, + Usage: `Add DNS Name, IP Address, or Email Address Subjective Alternative Names (SANs) +that the token is authorized to request. A certificate signing request using +this token must match the complete set of subjective alternative names in the +token 1:1. Use the '--san' flag multiple times to configure multiple SANs. The +'--san' flag and the '--token' flag are mutually exlusive.`, }, offlineFlag, caConfigFlag, + cli.BoolFlag{ + Name: "console", + Usage: "Complete the flow while remaining inside the terminal", + }, flags.Force, + flags.KTY, + flags.Size, + flags.Curve, }, } } @@ -125,7 +142,7 @@ func certificateAction(ctx *cli.Context) error { } } - req, pk, err := flow.CreateSignRequest(tok, subject, sans) + req, pk, err := flow.CreateSignRequest(ctx, tok, subject, sans) if err != nil { return err } @@ -157,7 +174,7 @@ func certificateAction(ctx *cli.Context) error { return errors.New("token is not supported") } - if err := flow.Sign(ctx, tok, req.CsrPEM, crtFile); err != nil { + if err = flow.Sign(ctx, tok, req.CsrPEM, crtFile); err != nil { return err } diff --git a/command/ca/init.go b/command/ca/init.go index 04c40a72..a9e2478d 100644 --- a/command/ca/init.go +++ b/command/ca/init.go @@ -22,7 +22,7 @@ func initCommand() cli.Command { Action: cli.ActionFunc(initAction), Usage: "initialize the CA PKI", UsageText: `**step ca init** - [**--root**=] [**--key**=] [**--pki**] [**--ssh**] [**--name**=] +[**--root**=] [**--key**=] [**--pki**] [**--ssh**] [**--name**=] [**dns**=] [**address**=
] [**provisioner**=] [**provisioner-password-file**=] [**password-file**=] [**with-ca-url**=] [**no-db**]`, @@ -84,7 +84,7 @@ func initCommand() cli.Command { } func initAction(ctx *cli.Context) (err error) { - if err := assertCryptoRand(); err != nil { + if err = assertCryptoRand(); err != nil { return err } @@ -100,7 +100,6 @@ func initAction(ctx *cli.Context) (err error) { case len(root) == 0 && len(key) > 0: return errs.RequiredWithFlag(ctx, "key", "root") case len(root) > 0 && len(key) > 0: - var err error if rootCrt, err = pemutil.ReadCertificate(root); err != nil { return err } @@ -145,7 +144,8 @@ func initAction(ctx *cli.Context) (err error) { } if configure { - names, err := ui.Prompt("What DNS names or IP addresses would you like to add to your new CA? (e.g. ca.smallstep.com[,1.1.1.1,etc.])", + var names string + names, err = ui.Prompt("What DNS names or IP addresses would you like to add to your new CA? (e.g. ca.smallstep.com[,1.1.1.1,etc.])", ui.WithValidateFunc(ui.DNS()), ui.WithValue(ctx.String("dns"))) if err != nil { return err @@ -160,13 +160,15 @@ func initAction(ctx *cli.Context) (err error) { dnsNames = append(dnsNames, strings.TrimSpace(name)) } - address, err := ui.Prompt("What address will your new CA listen at? (e.g. :443)", + var address string + address, err = ui.Prompt("What address will your new CA listen at? (e.g. :443)", ui.WithValidateFunc(ui.Address()), ui.WithValue(ctx.String("address"))) if err != nil { return err } - provisioner, err := ui.Prompt("What would you like to name the first provisioner for your new CA? (e.g. you@smallstep.com)", + var provisioner string + provisioner, err = ui.Prompt("What would you like to name the first provisioner for your new CA? (e.g. you@smallstep.com)", ui.WithValidateNotEmpty(), ui.WithValue(ctx.String("provisioner"))) if err != nil { return err @@ -187,11 +189,11 @@ func initAction(ctx *cli.Context) (err error) { if configure { // Generate provisioner key pairs. if len(provisionerPassword) > 0 { - if err := p.GenerateKeyPairs(provisionerPassword); err != nil { + if err = p.GenerateKeyPairs(provisionerPassword); err != nil { return err } } else { - if err := p.GenerateKeyPairs(pass); err != nil { + if err = p.GenerateKeyPairs(pass); err != nil { return err } } @@ -211,7 +213,7 @@ func initAction(ctx *cli.Context) (err error) { } else { fmt.Println() fmt.Print("Copying root certificate... \n") - if err := p.WriteRootCertificate(rootCrt, rootKey, pass); err != nil { + if err = p.WriteRootCertificate(rootCrt, rootKey, pass); err != nil { return err } fmt.Println("all done!") @@ -246,7 +248,7 @@ func initAction(ctx *cli.Context) (err error) { return p.Save(opts...) } -// assertCrytoRand asserts that a cryptographically secure random number +// assertCryptoRand asserts that a cryptographically secure random number // generator is available, it will return an error otherwise. func assertCryptoRand() error { buf := make([]byte, 64) diff --git a/command/ca/revoke.go b/command/ca/revoke.go index bdad60b7..28ea8e14 100644 --- a/command/ca/revoke.go +++ b/command/ca/revoke.go @@ -2,6 +2,7 @@ package ca import ( "crypto/tls" + "crypto/x509" "net/http" "os" "strconv" @@ -233,14 +234,15 @@ func revokeCertificateAction(ctx *cli.Context) error { if len(serial) > 0 { errs.IncompatibleFlagWithFlag(ctx, "cert", "serial") } - cert, err := pemutil.ReadCertificateBundle(certFile) + var cert []*x509.Certificate + cert, err = pemutil.ReadCertificateBundle(certFile) if err != nil { return err } serial = cert[0].SerialNumber.String() } else { // Must be using serial number so verify that only 1 command line args was given. - if err := errs.NumberOfArguments(ctx, 1); err != nil { + if err = errs.NumberOfArguments(ctx, 1); err != nil { return err } if len(token) == 0 { @@ -397,7 +399,8 @@ func (f *revokeFlow) Revoke(ctx *cli.Context, serial, token string) error { certFile, keyFile := ctx.String("cert"), ctx.String("key") // If there is no token then we must be doing a Revoke over mTLS. - cert, err := tls.LoadX509KeyPair(certFile, keyFile) + var cert tls.Certificate + cert, err = tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return errors.Wrap(err, "error loading certificates") } @@ -407,11 +410,12 @@ func (f *revokeFlow) Revoke(ctx *cli.Context, serial, token string) error { root := ctx.String("root") if len(root) == 0 { root = pki.GetRootCAPath() - if _, err := os.Stat(root); err != nil { + if _, err = os.Stat(root); err != nil { return errs.RequiredUnlessFlag(ctx, "root", "token") } } - rootCAs, err := x509util.ReadCertPool(root) + var rootCAs *x509.CertPool + rootCAs, err = x509util.ReadCertPool(root) if err != nil { return err } diff --git a/command/ca/sign.go b/command/ca/sign.go index a76078f6..8906ed00 100644 --- a/command/ca/sign.go +++ b/command/ca/sign.go @@ -22,8 +22,9 @@ func signCertificateCommand() cli.Command { Action: command.ActionFunc(signCertificateAction), Usage: "generate a new certificate signing a certificate request", UsageText: `**step ca sign** - [**--token**=] [**--issuer**=] [**--ca-url**=] [**--root**=] - [**--not-before**=] [**--not-after**=]`, +[**--token**=] [**--issuer**=] [**--ca-url**=] [**--root**=] +[**--not-before**=] [**--not-after**=] +[**--console**]`, Description: `**step ca sign** command signs the given csr and generates a new certificate. ## POSITIONAL ARGUMENTS @@ -63,6 +64,10 @@ $ step ca sign --offline internal internal.csr internal.crt offlineFlag, caConfigFlag, flags.Force, + cli.BoolFlag{ + Name: "console", + Usage: "Complete the flow while remaining inside the terminal", + }, }, } } @@ -156,5 +161,11 @@ func mergeSans(ctx *cli.Context, csr *x509.CertificateRequest) []string { m[s] = true } } + for _, s := range csr.EmailAddresses { + if _, ok := m[s]; !ok { + uniq = append(uniq, s) + m[s] = true + } + } return uniq } diff --git a/command/certificate/create.go b/command/certificate/create.go index f396fddd..6ef42ab7 100644 --- a/command/certificate/create.go +++ b/command/certificate/create.go @@ -25,8 +25,8 @@ func createCommand() cli.Command { Usage: "create a certificate or certificate signing request", UsageText: `**step certificate create** [**ca**=] [**ca-key**=] [**--csr**] -[**--curve**=] [**no-password**] [**--profile**=] -[**--size**=] [**--type**=] [**--san**=]`, +[**no-password**] [**--profile**=] [**--san**=] [**--bundle**] +[**--kty**=] [**--curve**=] [**--size**=]`, 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. @@ -113,6 +113,12 @@ $ step certificate create foo foo.crt foo.key --profile leaf \ --not-before 24h --not-after 2160h ''' +Create a self-signed leaf certificate and key: + +''' +$ step certificate create self-signed-leaf.local leaf.crt leaf.key --profile self-signed --subtle +''' + Create a root certificate and key with underlying OKP Ed25519: ''' @@ -179,51 +185,12 @@ recommended. Requires **--insecure** flag.`, : 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: "kty", - Value: "EC", - Usage: `The to build the certificate upon. -If unset, default is EC. + : Generate a new self-signed root certificate suitable for use as a root CA. -: 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 (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", - 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. - -: 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 -`, + **self-signed** + : Generate a new self-signed leaf certificate suitable for use with TLS. + This profile requires the **--subtle** flag because the use of self-signed leaf + certificates is discouraged unless absolutely necessary.`, }, cli.StringFlag{ Name: "not-before", @@ -246,7 +213,16 @@ unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", Usage: `Add DNS or IP Address Subjective Alternative Names (SANs). Use the '--san' flag multiple times to configure multiple SANs.`, }, + cli.BoolFlag{ + Name: "bundle", + Usage: `Bundle the new leaf certificate with the signing certificate. This flag requires +the **--ca** flag.`, + }, + flags.KTY, + flags.Size, + flags.Curve, flags.Force, + flags.Subtle, }, } } @@ -297,15 +273,19 @@ func createAction(ctx *cli.Context) error { if len(sans) == 0 { sans = []string{subject} } - dnsNames, ips := x509util.SplitSANs(sans) + dnsNames, ips, emails := x509util.SplitSANs(sans) var ( priv interface{} - pubPEM *pem.Block + pubPEMs []*pem.Block outputType string + bundle = ctx.Bool("bundle") ) switch typ { case "x509-csr": + if bundle { + return errs.IncompatibleFlagWithFlag(ctx, "bundle", "csr") + } if ctx.IsSet("profile") { return errs.IncompatibleFlagWithFlag(ctx, "profile", "csr") } @@ -318,28 +298,32 @@ func createAction(ctx *cli.Context) error { Subject: pkix.Name{ CommonName: subject, }, - DNSNames: dnsNames, - IPAddresses: ips, + DNSNames: dnsNames, + IPAddresses: ips, + EmailAddresses: emails, } - csrBytes, err := stepx509.CreateCertificateRequest(rand.Reader, _csr, priv) + var csrBytes []byte + csrBytes, err = stepx509.CreateCertificateRequest(rand.Reader, _csr, priv) if err != nil { return errors.WithStack(err) } - pubPEM = &pem.Block{ + pubPEMs = []*pem.Block{{ Type: "CERTIFICATE REQUEST", Bytes: csrBytes, Headers: map[string]string{}, - } + }} outputType = "certificate signing request" case "x509": var ( - err error prof = ctx.String("profile") caPath = ctx.String("ca") caKeyPath = ctx.String("ca-key") profile x509util.Profile ) + if bundle && prof != "leaf" { + return errs.IncompatibleFlagValue(ctx, "bundle", "profile", prof) + } switch prof { case "leaf", "intermediate-ca": if caPath == "" { @@ -350,7 +334,8 @@ func createAction(ctx *cli.Context) error { } switch prof { case "leaf": - issIdentity, err := loadIssuerIdentity(ctx, prof, caPath, caKeyPath) + var issIdentity *x509util.Identity + issIdentity, err = loadIssuerIdentity(ctx, prof, caPath, caKeyPath) if err != nil { return errors.WithStack(err) } @@ -358,15 +343,14 @@ func createAction(ctx *cli.Context) error { issIdentity.Key, x509util.GenerateKeyPair(kty, crv, size), x509util.WithNotBeforeAfterDuration(notBefore, notAfter, 0), x509util.WithDNSNames(dnsNames), - x509util.WithIPAddresses(ips)) + x509util.WithIPAddresses(ips), + x509util.WithEmailAddresses(emails)) if err != nil { return errors.WithStack(err) } case "intermediate-ca": - issIdentity, err := loadIssuerIdentity(ctx, prof, caPath, caKeyPath) - if err != nil { - return errors.WithStack(err) - } + var issIdentity *x509util.Identity + issIdentity, err = loadIssuerIdentity(ctx, prof, caPath, caKeyPath) if err != nil { return errors.WithStack(err) } @@ -375,7 +359,8 @@ func createAction(ctx *cli.Context) error { x509util.GenerateKeyPair(kty, crv, size), x509util.WithNotBeforeAfterDuration(notBefore, notAfter, 0), x509util.WithDNSNames(dnsNames), - x509util.WithIPAddresses(ips)) + x509util.WithIPAddresses(ips), + x509util.WithEmailAddresses(emails)) if err != nil { return errors.WithStack(err) } @@ -385,21 +370,41 @@ func createAction(ctx *cli.Context) error { x509util.GenerateKeyPair(kty, crv, size), x509util.WithNotBeforeAfterDuration(notBefore, notAfter, 0), x509util.WithDNSNames(dnsNames), - x509util.WithIPAddresses(ips)) + x509util.WithIPAddresses(ips), + x509util.WithEmailAddresses(emails)) + if err != nil { + return errors.WithStack(err) + } + case "self-signed": + if !ctx.Bool("subtle") { + return errs.RequiredWithFlagValue(ctx, "profile", "self-signed", "subtle") + } + profile, err = x509util.NewSelfSignedLeafProfile(subject, + x509util.GenerateKeyPair(kty, crv, size), + x509util.WithNotBeforeAfterDuration(notBefore, notAfter, 0), + x509util.WithDNSNames(dnsNames), + x509util.WithIPAddresses(ips), + x509util.WithEmailAddresses(emails)) if err != nil { return errors.WithStack(err) } default: - return errs.InvalidFlagValue(ctx, "profile", prof, "leaf, intermediate-ca, root-ca") + return errs.InvalidFlagValue(ctx, "profile", prof, "leaf, intermediate-ca, root-ca, self-signed") } - crtBytes, err := profile.CreateCertificate() + var crtBytes []byte + crtBytes, err = profile.CreateCertificate() if err != nil { return errors.WithStack(err) } - pubPEM = &pem.Block{ - Type: "CERTIFICATE", - Bytes: crtBytes, - Headers: map[string]string{}, + pubPEMs = []*pem.Block{{ + Type: "CERTIFICATE", + Bytes: crtBytes, + }} + if bundle { + pubPEMs = append(pubPEMs, &pem.Block{ + Type: "CERTIFICATE", + Bytes: profile.Issuer().Raw, + }) } priv = profile.SubjectPrivateKey() outputType = "certificate" @@ -407,7 +412,11 @@ func createAction(ctx *cli.Context) error { return errs.NewError("unexpected type: %s", typ) } - if err := utils.WriteFile(crtFile, pem.EncodeToMemory(pubPEM), 0600); err != nil { + pubBytes := []byte{} + for _, pp := range pubPEMs { + pubBytes = append(pubBytes, pem.EncodeToMemory(pp)...) + } + if err = utils.WriteFile(crtFile, pubBytes, 0600); err != nil { return errs.FileError(err, crtFile) } @@ -417,7 +426,8 @@ func createAction(ctx *cli.Context) error { return errors.WithStack(err) } } else { - pass, err := ui.PromptPassword("Please enter the password to encrypt the private key") + var pass []byte + pass, err = ui.PromptPassword("Please enter the password to encrypt the private key") if err != nil { return errors.Wrap(err, "error reading password") } diff --git a/command/certificate/sign.go b/command/certificate/sign.go index e5aa7f70..7fbe39a2 100644 --- a/command/certificate/sign.go +++ b/command/certificate/sign.go @@ -13,10 +13,11 @@ import ( func signCommand() cli.Command { return cli.Command{ - Name: "sign", - Action: cli.ActionFunc(signAction), - Usage: "sign a certificate signing request (CSR)", - UsageText: `**step certificate sign** `, + Name: "sign", + Action: cli.ActionFunc(signAction), + Usage: "sign a certificate signing request (CSR)", + UsageText: `**step certificate sign** +[**--bundle**]`, Description: `**step certificate sign** generates a signed certificate from a certificate signing request (CSR). @@ -43,7 +44,20 @@ Sign a certificate signing request: $ step certificate sign ./certificate-signing-request.csr \ ./issuer-certificate.crt ./issuer-private-key.priv ''' + +Sign a certificate signing request and bundle the new certificate with the issuer: + +''' +$ step certificate sign ./certificate-signing-request.csr \ +./issuer-certificate.crt ./issuer-private-key.priv --bundle +''' `, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "bundle", + Usage: `Bundle the new leaf certificate with the signing certificate.`, + }, + }, } } @@ -64,7 +78,7 @@ func signAction(ctx *cli.Context) error { if err != nil { return errors.WithStack(err) } - if err := x509util.CheckCertificateRequestSignature(csr); err != nil { + if err = x509util.CheckCertificateRequestSignature(csr); err != nil { return errors.Wrapf(err, "Certificate Request has invalid signature") } @@ -83,11 +97,21 @@ func signAction(ctx *cli.Context) error { if err != nil { return errors.Wrapf(err, "failure creating new leaf certificate from input csr") } - block := &pem.Block{ + pubPEMs := []*pem.Block{{ Type: "CERTIFICATE", Bytes: crtBytes, + }} + if ctx.Bool("bundle") { + pubPEMs = append(pubPEMs, &pem.Block{ + Type: "CERTIFICATE", + Bytes: issuerIdentity.Crt.Raw, + }) } - fmt.Printf("%s", string(pem.EncodeToMemory(block))) + pubBytes := []byte{} + for _, pp := range pubPEMs { + pubBytes = append(pubBytes, pem.EncodeToMemory(pp)...) + } + fmt.Printf("%s", string(pubBytes)) return nil } diff --git a/command/certificate/verify.go b/command/certificate/verify.go index b477cc17..3eb59b7e 100644 --- a/command/certificate/verify.go +++ b/command/certificate/verify.go @@ -96,7 +96,6 @@ func verifyAction(ctx *cli.Context) error { } var ( - err error crtFile = ctx.Args().Get(0) host = ctx.String("host") roots = ctx.String("roots") @@ -153,6 +152,7 @@ func verifyAction(ctx *cli.Context) error { } if roots != "" { + var err error rootPool, err = x509util.ReadCertPool(roots) if err != nil { errors.Wrapf(err, "failure to load root certificate pool from input path '%s'", roots) diff --git a/command/crypto/change-pass.go b/command/crypto/change-pass.go index 4ff09ea4..a4006bf1 100644 --- a/command/crypto/change-pass.go +++ b/command/crypto/change-pass.go @@ -20,13 +20,14 @@ import ( func changePassCommand() cli.Command { return cli.Command{ - Name: "change-pass", - Action: command.ActionFunc(changePassAction), - Usage: "change password of an encrypted private key (PEM or JWK format)", - UsageText: `**step crypto change-pass** [**--out**=]`, - Description: `**step crypto change-pass** extracts the private key from -a file and encrypts disk using a new password by either overwriting the original -encrypted key or writing a new file to disk. + Name: "change-pass", + Action: command.ActionFunc(changePassAction), + Usage: "change password of an encrypted private key (PEM or JWK format)", + UsageText: `**step crypto change-pass** +[**--out**=] [**--insecure**] [**--no-password**]`, + Description: `**step crypto change-pass** extracts and decrypts +the private key from a file and encrypts and serializes the key to disk using a +new password. ## POSITIONAL ARGUMENTS @@ -40,6 +41,11 @@ Change password for PEM formatted key: $ step crypto change-pass key.pem ''' +Remove password for PEM formatted key: +''' +$ step crypto change-pass key.pem --no-password --insecure +''' + Change password for PEM formatted key and write encrypted key to different file: ''' $ step crypto change-pass key.pem --out new-key.pem @@ -50,6 +56,11 @@ Change password for JWK formatted key: $ step crypto change-pass key.jwk ''' +Removed password for JWK formatted key: +''' +$ step crypto change-pass key.jwk --no-password --insecure +''' + Change password for JWK formatted key: ''' $ step crypto change-pass key.jwk --out new-key.jwk @@ -60,6 +71,13 @@ $ step crypto change-pass key.jwk --out new-key.jwk Usage: "The new encrypted key path. Default to overwriting the positional argument", }, flags.Force, + flags.Insecure, + 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.`, + }, }, } } @@ -72,8 +90,14 @@ func changePassAction(ctx *cli.Context) error { if err := errs.NumberOfArguments(ctx, 1); err != nil { return err } - keyPath := ctx.Args().Get(0) + insecure := ctx.Bool("insecure") + noPass := ctx.Bool("no-password") + if noPass && !insecure { + return errs.RequiredWithFlag(ctx, "insecure", "no-password") + } + + keyPath := ctx.Args().Get(0) newKeyPath := ctx.String("out") if len(newKeyPath) == 0 { newKeyPath = keyPath @@ -89,11 +113,16 @@ func changePassAction(ctx *cli.Context) error { if err != nil { return err } - pass, err := ui.PromptPassword(fmt.Sprintf("Please enter the password to encrypt %s", newKeyPath)) - if err != nil { - return errors.Wrap(err, "error reading password") + var opts []pemutil.Options + if !noPass { + pass, err := ui.PromptPassword(fmt.Sprintf("Please enter the password to encrypt %s", newKeyPath)) + if err != nil { + return errors.Wrap(err, "error reading password") + } + opts = append(opts, pemutil.WithPassword(pass)) } - if _, err := pemutil.Serialize(key, pemutil.WithPassword(pass), pemutil.ToFile(newKeyPath, 0644)); err != nil { + opts = append(opts, pemutil.ToFile(newKeyPath, 0644)) + if _, err := pemutil.Serialize(key, opts...); err != nil { return err } } else { @@ -101,12 +130,21 @@ func changePassAction(ctx *cli.Context) error { if err != nil { return err } - jwe, err := jose.EncryptJWK(jwk) - if err != nil { - return err + var b []byte + if noPass { + b, err = jwk.MarshalJSON() + if err != nil { + return err + } + } else { + jwe, err := jose.EncryptJWK(jwk) + if err != nil { + return err + } + b = []byte(jwe.FullSerialize()) } var out bytes.Buffer - if err := json.Indent(&out, []byte(jwe.FullSerialize()), "", " "); err != nil { + if err := json.Indent(&out, b, "", " "); err != nil { return errors.Wrap(err, "error formatting JSON") } if err := utils.WriteFile(newKeyPath, out.Bytes(), 0600); err != nil { diff --git a/command/crypto/hash/hash.go b/command/crypto/hash/hash.go index b9e8fc70..0a258429 100644 --- a/command/crypto/hash/hash.go +++ b/command/crypto/hash/hash.go @@ -203,7 +203,8 @@ func digestAction(ctx *cli.Context) error { } for _, filename := range ctx.Args() { - st, err := os.Stat(filename) + var st os.FileInfo + st, err = os.Stat(filename) if err != nil { return errs.FileError(err, filename) } diff --git a/command/crypto/jwe/decrypt.go b/command/crypto/jwe/decrypt.go index f63060c0..f4a78db0 100644 --- a/command/crypto/jwe/decrypt.go +++ b/command/crypto/jwe/decrypt.go @@ -129,7 +129,7 @@ func decryptAction(ctx *cli.Context) error { } // Validate jwk - if err := jose.ValidateJWK(jwk); err != nil { + if err = jose.ValidateJWK(jwk); err != nil { return err } diff --git a/command/crypto/jwe/encrypt.go b/command/crypto/jwe/encrypt.go index 1dbcf8f9..68375e88 100644 --- a/command/crypto/jwe/encrypt.go +++ b/command/crypto/jwe/encrypt.go @@ -249,7 +249,7 @@ func encryptAction(ctx *cli.Context) error { } // Validate jwk - if err := jose.ValidateJWK(jwk); err != nil { + if err = jose.ValidateJWK(jwk); err != nil { return err } diff --git a/command/crypto/jwk/create.go b/command/crypto/jwk/create.go index e6ebbc46..b8d024ed 100644 --- a/command/crypto/jwk/create.go +++ b/command/crypto/jwk/create.go @@ -177,32 +177,8 @@ If unset, default is EC. : Create an **RSA** keypair `, }, - cli.IntFlag{ - Name: "size", - 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", - 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. - -: 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 -`, - }, + flags.Size, + flags.Curve, cli.StringFlag{ Name: "alg, algorithm", Usage: `The intended for use with this key. Corresponds to the @@ -404,7 +380,7 @@ existing instead of creating a new key.`, func createAction(ctx *cli.Context) (err error) { // require public and private files - if err := errs.NumberOfArguments(ctx, 2); err != nil { + if err = errs.NumberOfArguments(ctx, 2); err != nil { return err } @@ -502,7 +478,8 @@ func createAction(ctx *cli.Context) (err error) { } else { // A hash of a symmetric key can leak information, so we only thumbprint asymmetric keys. if kty != "oct" { - hash, err := jwk.Thumbprint(crypto.SHA256) + var hash []byte + hash, err = jwk.Thumbprint(crypto.SHA256) if err != nil { return errors.Wrap(err, "error generating JWK thumbprint") } @@ -515,7 +492,7 @@ func createAction(ctx *cli.Context) (err error) { jwk.Algorithm = alg } - if err := jose.ValidateJWK(jwk); err != nil { + if err = jose.ValidateJWK(jwk); err != nil { return err } @@ -531,7 +508,7 @@ func createAction(ctx *cli.Context) (err error) { if err != nil { return errors.Wrap(err, "error marshaling JWK") } - if err := utils.WriteFile(pubFile, b, 0600); err != nil { + if err = utils.WriteFile(pubFile, b, 0600); err != nil { return errs.FileError(err, pubFile) } @@ -547,12 +524,14 @@ func createAction(ctx *cli.Context) (err 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", ui.WithValue(password)) + var key []byte + 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") } - salt, err := randutil.Salt(pbkdf2SaltSize) + var salt []byte + salt, err = randutil.Salt(pbkdf2SaltSize) if err != nil { return err } @@ -564,7 +543,8 @@ func createAction(ctx *cli.Context) (err error) { PBES2Salt: salt, } } else { - key, err := randutil.Alphanumeric(32) + var key string + key, err = randutil.Alphanumeric(32) if err != nil { return errors.Wrap(err, "error generating password") } @@ -579,18 +559,20 @@ func createAction(ctx *cli.Context) (err error) { opts := new(jose.EncrypterOptions) opts.WithContentType(jose.ContentType("jwk+json")) - encrypter, err := jose.NewEncrypter(jose.DefaultEncAlgorithm, rcpt, opts) + var encrypter jose.Encrypter + encrypter, err = jose.NewEncrypter(jose.DefaultEncAlgorithm, rcpt, opts) if err != nil { return errors.Wrap(err, "error creating cipher") } - obj, err := encrypter.Encrypt(b) + var obj *jose.JSONWebEncryption + obj, err = encrypter.Encrypt(b) if err != nil { return errors.Wrap(err, "error encrypting JWK") } var out bytes.Buffer - if err := json.Indent(&out, []byte(obj.FullSerialize()), "", " "); err != nil { + if err = json.Indent(&out, []byte(obj.FullSerialize()), "", " "); err != nil { return errors.Wrap(err, "error formatting JSON") } b = out.Bytes() @@ -600,7 +582,7 @@ func createAction(ctx *cli.Context) (err error) { return errors.Wrap(err, "error marshaling JWK") } } - if err := utils.WriteFile(privFile, b, 0600); err != nil { + if err = utils.WriteFile(privFile, b, 0600); err != nil { return errs.FileError(err, privFile) } diff --git a/command/crypto/jwk/keyset.go b/command/crypto/jwk/keyset.go index ed3d5faa..62b0a9f7 100644 --- a/command/crypto/jwk/keyset.go +++ b/command/crypto/jwk/keyset.go @@ -135,7 +135,7 @@ func keysetAddAction(ctx *cli.Context) error { // Unmarshal the plain (or decrypted JWK) var jwk jose.JSONWebKey - if err := json.Unmarshal(b, &jwk); err != nil { + if err = json.Unmarshal(b, &jwk); err != nil { return errors.New("error reading JWK: unsupported format") } diff --git a/command/crypto/jwk/public.go b/command/crypto/jwk/public.go index 00a98201..8417f913 100644 --- a/command/crypto/jwk/public.go +++ b/command/crypto/jwk/public.go @@ -37,7 +37,7 @@ func publicAction(ctx *cli.Context) error { } // Unmarshal the plain (or decrypted JWK) - if err := json.Unmarshal(b, jwk); err != nil { + if err = json.Unmarshal(b, jwk); err != nil { return errors.New("error reading JWK: unsupported format") } diff --git a/command/crypto/jwk/thumbprint.go b/command/crypto/jwk/thumbprint.go index d9f5859a..e7d6d931 100644 --- a/command/crypto/jwk/thumbprint.go +++ b/command/crypto/jwk/thumbprint.go @@ -40,7 +40,7 @@ func thumbprintAction(ctx *cli.Context) error { } // Unmarshal the plain (or decrypted JWK) - if err := json.Unmarshal(b, jwk); err != nil { + if err = json.Unmarshal(b, jwk); err != nil { return errors.New("error reading JWK: unsupported format") } diff --git a/command/crypto/jws/sign.go b/command/crypto/jws/sign.go index e28c6115..16a0583a 100644 --- a/command/crypto/jws/sign.go +++ b/command/crypto/jws/sign.go @@ -238,7 +238,7 @@ func signAction(ctx *cli.Context) error { if jwk.Algorithm == "" { return errors.New("flag '--alg' is required with the given key") } - if err := jose.ValidateJWK(jwk); err != nil { + if err = jose.ValidateJWK(jwk); err != nil { return err } diff --git a/command/crypto/jws/verify.go b/command/crypto/jws/verify.go index e14d1c5c..ed505a58 100644 --- a/command/crypto/jws/verify.go +++ b/command/crypto/jws/verify.go @@ -158,7 +158,7 @@ func verifyAction(ctx *cli.Context) error { if jwk.Algorithm == "" { return errors.New("flag '--alg' is required with the given key") } - if err := jose.ValidateJWK(jwk); err != nil { + if err = jose.ValidateJWK(jwk); err != nil { return err } diff --git a/command/crypto/jwt/sign.go b/command/crypto/jwt/sign.go index 74b75438..8b31e48f 100644 --- a/command/crypto/jwt/sign.go +++ b/command/crypto/jwt/sign.go @@ -285,7 +285,7 @@ func signAction(ctx *cli.Context) error { if jwk.Algorithm == "" { return errors.New("flag '--alg' is required with the given key") } - if err := jose.ValidateJWK(jwk); err != nil { + if err = jose.ValidateJWK(jwk); err != nil { return err } diff --git a/command/crypto/keypair.go b/command/crypto/keypair.go index 5493c8ff..9fd7821d 100644 --- a/command/crypto/keypair.go +++ b/command/crypto/keypair.go @@ -76,50 +76,9 @@ $ step crypto keypair foo.pub foo.key --kty OKP --curve Ed25519 ''' `, Flags: []cli.Flag{ - cli.StringFlag{ - Name: "kty", - Value: "EC", - Usage: `The (key type) to create. -If unset, default is EC. - -: 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 (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", - 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. - -: 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 -`, - }, + flags.KTY, + flags.Size, + flags.Curve, cli.StringFlag{ Name: "from-jwk", Usage: `Create a PEM representing the key encoded in an @@ -134,7 +93,7 @@ existing instead of creating a new key.`, } func createAction(ctx *cli.Context) (err error) { - if err := errs.NumberOfArguments(ctx, 2); err != nil { + if err = errs.NumberOfArguments(ctx, 2); err != nil { return err } @@ -175,7 +134,8 @@ func createAction(ctx *cli.Context) (err error) { return errs.IncompatibleFlagWithFlag(ctx, "from-jwk", "size") } - jwk, err := jose.ParseKey(fromJWK) + var jwk *jose.JSONWebKey + jwk, err = jose.ParseKey(fromJWK) if err != nil { return err } @@ -187,7 +147,11 @@ func createAction(ctx *cli.Context) (err error) { priv = jwk.Key } } else { - kty, crv, size, err := utils.GetKeyDetailsFromCLI(ctx, insecure, "kty", + var ( + kty, crv string + size int + ) + kty, crv, size, err = utils.GetKeyDetailsFromCLI(ctx, insecure, "kty", "curve", "size") if err != nil { return err @@ -217,7 +181,8 @@ func createAction(ctx *cli.Context) (err error) { return err } } else { - pass, err := ui.PromptPassword("Please enter the password to encrypt the private key", ui.WithValue(password)) + var pass []byte + 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/command/crypto/nacl/box.go b/command/crypto/nacl/box.go index d023f2be..8500f728 100644 --- a/command/crypto/nacl/box.go +++ b/command/crypto/nacl/box.go @@ -29,7 +29,7 @@ NaCl crypto_box function is designed to meet the standard notions of privacy and third-party unforgeability for a public-key authenticated-encryption scheme using nonces. For formal definitions see, e.g., Jee Hea An, "Authenticated encryption in the public-key setting: security notions and -analyses," https://eprint.iacr.org/2001/079. Distinct messages between the same +analyzes," https://eprint.iacr.org/2001/079. Distinct messages between the same {sender, receiver} set are required to have distinct nonces. For example, the lexicographically smaller public key can use nonce 1 for its first message to the other key, nonce 3 for its second message, nonce 5 for its third message, diff --git a/command/oauth/cmd.go b/command/oauth/cmd.go index 87d6e789..26604ae4 100644 --- a/command/oauth/cmd.go +++ b/command/oauth/cmd.go @@ -9,6 +9,7 @@ import ( "encoding/pem" "fmt" "io/ioutil" + "net" "net/http" "net/http/httptest" "net/url" @@ -129,6 +130,10 @@ func init() { Name: "jwt", Usage: "Generate a JWT Auth token instead of an OAuth Token (only works with service accounts)", }, + cli.StringFlag{ + Name: "listen", + Usage: "Callback listener URL", + }, cli.BoolFlag{ Name: "implicit", Usage: "Uses the implicit flow to authenticate the user. Requires **--insecure** and **--client-id** flags.", @@ -148,10 +153,11 @@ func init() { func oauthCmd(c *cli.Context) error { opts := &options{ - Provider: c.String("provider"), - Email: c.String("email"), - Console: c.Bool("console"), - Implicit: c.Bool("implicit"), + Provider: c.String("provider"), + Email: c.String("email"), + Console: c.Bool("console"), + Implicit: c.Bool("implicit"), + CallbackListener: c.String("listen"), } if err := opts.Validate(); err != nil { return err @@ -199,7 +205,7 @@ func oauthCmd(c *cli.Context) error { return errors.Wrapf(err, "error reading account from %s", filename) } account := make(map[string]interface{}) - if err := json.Unmarshal(b, &account); err != nil { + if err = json.Unmarshal(b, &account); err != nil { return errors.Wrapf(err, "error reading %s: unsupported format", filename) } @@ -274,16 +280,17 @@ func oauthCmd(c *cli.Context) error { } type options struct { - Provider string - Email string - Console bool - Implicit bool + Provider string + Email string + Console bool + Implicit bool + CallbackListener string } // Validate validates the options. func (o *options) Validate() error { if o.Provider != "google" && !strings.HasPrefix(o.Provider, "https://") { - return errors.New("Use a valid provider: google") + return errors.New("use a valid provider: google") } return nil } @@ -302,6 +309,7 @@ type oauth struct { codeChallenge string nonce string implicit bool + CallbackListener string errCh chan error tokCh chan *token } @@ -337,6 +345,7 @@ func newOauth(provider, clientID, clientSecret, authzEp, tokenEp, scope string, codeChallenge: challenge, nonce: nonce, implicit: opts.Implicit, + CallbackListener: opts.CallbackListener, errCh: make(chan error), tokCh: make(chan *token), }, nil @@ -371,6 +380,7 @@ func newOauth(provider, clientID, clientSecret, authzEp, tokenEp, scope string, codeChallenge: challenge, nonce: nonce, implicit: opts.Implicit, + CallbackListener: opts.CallbackListener, errCh: make(chan error), tokCh: make(chan *token), }, nil @@ -385,7 +395,7 @@ func disco(provider string) (map[string]interface{}, error) { // TODO: OIDC and OAuth specify two different ways of constructing this // URL. This is the OIDC way. Probably want to try both. See // https://tools.ietf.org/html/rfc8414#section-5 - if strings.Index(url.Path, "/.well-known/openid-configuration") == -1 { + if !strings.Contains(url.Path, "/.well-known/openid-configuration") { url.Path = path.Join(url.Path, "/.well-known/openid-configuration") } resp, err := http.Get(url.String()) @@ -398,17 +408,37 @@ func disco(provider string) (map[string]interface{}, error) { return nil, errors.Wrapf(err, "error retrieving %s", url.String()) } details := make(map[string]interface{}) - if err := json.Unmarshal(b, &details); err != nil { + if err = json.Unmarshal(b, &details); err != nil { return nil, errors.Wrapf(err, "error reading %s: unsupported format", url.String()) } return details, err } +// NewServer creates http server +func (o *oauth) NewServer() (*httptest.Server, error) { + if o.CallbackListener == "" { + return httptest.NewServer(o), nil + } + l, err := net.Listen("tcp", o.CallbackListener) + if err != nil { + return nil, errors.Wrapf(err, "error listening on %s", o.CallbackListener) + } + srv := &httptest.Server{ + Listener: l, + Config: &http.Server{Handler: o}, + } + srv.Start() + return srv, nil +} + // DoLoopbackAuthorization performs the log in into the identity provider // opening a browser and using a redirect_uri in a loopback IP address // (http://127.0.0.1:port or http://[::1]:port). func (o *oauth) DoLoopbackAuthorization() (*token, error) { - srv := httptest.NewServer(o) + srv, err := o.NewServer() + if err != nil { + return nil, err + } o.redirectURI = srv.URL defer srv.Close() @@ -686,7 +716,6 @@ func (o *oauth) implicitHandler(w http.ResponseWriter, req *http.Request) { w.Write([]byte(`Success
`)) w.Write([]byte(`Click here if your browser does not automatically redirect you`)) w.Write([]byte(`

`)) - return } // Auth returns the OAuth 2.0 authentication url. diff --git a/crypto/pemutil/pem.go b/crypto/pemutil/pem.go index cbb2c069..368b88ae 100644 --- a/crypto/pemutil/pem.go +++ b/crypto/pemutil/pem.go @@ -152,7 +152,8 @@ func ReadCertificate(filename string, opts ...Options) (*x509.Certificate, error // PEM format if bytes.HasPrefix(b, []byte("-----BEGIN ")) { - crt, err := Read(filename, opts...) + var crt interface{} + crt, err = Read(filename, opts...) if err != nil { return nil, err } @@ -190,7 +191,8 @@ func ReadCertificateBundle(filename string) ([]*x509.Certificate, error) { if block.Type != "CERTIFICATE" { return nil, errors.Errorf("error decoding PEM: file '%s' is not a certificate bundle", filename) } - crt, err := x509.ParseCertificate(block.Bytes) + var crt *x509.Certificate + crt, err = x509.ParseCertificate(block.Bytes) if err != nil { return nil, errors.Wrapf(err, "error parsing %s", filename) } @@ -220,7 +222,8 @@ func ReadStepCertificate(filename string) (*stepx509.Certificate, error) { // PEM format if bytes.HasPrefix(b, []byte("-----BEGIN ")) { - crt, err := Read(filename, []Options{WithStepCrypto()}...) + var crt interface{} + crt, err = Read(filename, []Options{WithStepCrypto()}...) if err != nil { return nil, err } @@ -335,12 +338,13 @@ func Read(filename string, opts ...Options) (interface{}, error) { // Serialize will serialize the input to a PEM formatted block and apply // modifiers. -func Serialize(in interface{}, opts ...Options) (p *pem.Block, err error) { +func Serialize(in interface{}, opts ...Options) (*pem.Block, error) { ctx := new(context) if err := ctx.apply(opts); err != nil { return nil, err } + var p *pem.Block switch k := in.(type) { case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: b, err := MarshalPKIXPublicKey(k) @@ -424,11 +428,13 @@ func Serialize(in interface{}, opts ...Options) (p *pem.Block, err error) { // Apply options on the PEM blocks. if ctx.password != nil { if _, ok := in.(crypto.PrivateKey); ok && ctx.pkcs8 { + var err error p, err = EncryptPKCS8PrivateKey(rand.Reader, p.Bytes, ctx.password, DefaultEncCipher) if err != nil { return nil, err } } else { + var err error p, err = x509.EncryptPEMBlock(rand.Reader, p.Type, p.Bytes, ctx.password, DefaultEncCipher) if err != nil { return nil, errors.Wrap(err, "failed to serialize to PEM") diff --git a/crypto/pemutil/pem_test.go b/crypto/pemutil/pem_test.go index bdb9555d..7f9770f1 100644 --- a/crypto/pemutil/pem_test.go +++ b/crypto/pemutil/pem_test.go @@ -450,7 +450,8 @@ func TestSerialize(t *testing.T) { assert.Equals(t, p.Type, "RSA PRIVATE KEY") assert.Equals(t, p.Headers["Proc-Type"], "4,ENCRYPTED") - der, err := x509.DecryptPEMBlock(p, []byte(test.pass)) + var der []byte + der, err = x509.DecryptPEMBlock(p, []byte(test.pass)) assert.FatalError(t, err) assert.Equals(t, der, x509.MarshalPKCS1PrivateKey(k)) } @@ -458,7 +459,8 @@ func TestSerialize(t *testing.T) { assert.False(t, x509.IsEncryptedPEMBlock(p)) assert.Equals(t, p.Type, "PUBLIC KEY") - b, err := x509.MarshalPKIXPublicKey(k) + var b []byte + b, err = x509.MarshalPKIXPublicKey(k) assert.FatalError(t, err) assert.Equals(t, p.Bytes, b) case *ecdsa.PrivateKey: @@ -474,17 +476,20 @@ func TestSerialize(t *testing.T) { actualBytes, err = x509.DecryptPEMBlock(p, []byte(test.pass)) assert.FatalError(t, err) } - expectedBytes, err := x509.MarshalECPrivateKey(k) + var expectedBytes []byte + expectedBytes, err = x509.MarshalECPrivateKey(k) assert.FatalError(t, err) assert.Equals(t, actualBytes, expectedBytes) if test.file != "" { // Check key permissions - fileInfo, err := os.Stat(test.file) + var fileInfo os.FileInfo + fileInfo, err = os.Stat(test.file) assert.FatalError(t, err) assert.Equals(t, fileInfo.Mode(), os.FileMode(0600)) // Verify that key written to file is correct - keyFileBytes, err := ioutil.ReadFile(test.file) + var keyFileBytes []byte + keyFileBytes, err = ioutil.ReadFile(test.file) assert.FatalError(t, err) pemKey, _ := pem.Decode(keyFileBytes) assert.Equals(t, pemKey.Type, "EC PRIVATE KEY") diff --git a/crypto/pemutil/pkcs8.go b/crypto/pemutil/pkcs8.go index e5c8b4d6..69ac7672 100644 --- a/crypto/pemutil/pkcs8.go +++ b/crypto/pemutil/pkcs8.go @@ -238,7 +238,7 @@ func ParsePKIXPublicKey(derBytes []byte) (pub interface{}, err error) { } } -// MarshalPKIXPublicKey serialises a public key to DER-encoded PKIX format. The +// MarshalPKIXPublicKey serializes a public key to DER-encoded PKIX format. The // following key types are supported: *rsa.PublicKey, *ecdsa.PublicKey, // ed25519.Publickey. Unsupported key types result in an error. func MarshalPKIXPublicKey(pub interface{}) ([]byte, error) { @@ -265,7 +265,7 @@ func MarshalPKCS8PrivateKey(key interface{}) ([]byte, error) { switch k := key.(type) { case *rsa.PrivateKey, *ecdsa.PrivateKey: b, err := x509.MarshalPKCS8PrivateKey(key) - return b, errors.Wrap(err, "error marshalling PKCS#8") + return b, errors.Wrap(err, "error marshaling PKCS#8") case ed25519.PrivateKey: var priv pkcs8 priv.PrivateKey = append([]byte{4, 32}, k.Seed()...)[:34] @@ -273,9 +273,9 @@ func MarshalPKCS8PrivateKey(key interface{}) ([]byte, error) { Algorithm: asn1.ObjectIdentifier{1, 3, 101, 112}, } b, err := asn1.Marshal(priv) - return b, errors.Wrap(err, "error marshalling PKCS#8") + return b, errors.Wrap(err, "error marshaling PKCS#8") default: - return nil, errors.Errorf("x509: unknown key type while marshalling PKCS#8: %T", key) + return nil, errors.Errorf("x509: unknown key type while marshaling PKCS#8: %T", key) } } @@ -428,7 +428,7 @@ func EncryptPKCS8PrivateKey(rand io.Reader, data, password []byte, alg x509.PEMC b, err := asn1.Marshal(pki) if err != nil { - return nil, errors.Wrap(err, "error marshalling encrypted key") + return nil, errors.Wrap(err, "error marshaling encrypted key") } return &pem.Block{ Type: "ENCRYPTED PRIVATE KEY", diff --git a/crypto/pki/pki.go b/crypto/pki/pki.go index d687a942..c6c09a39 100644 --- a/crypto/pki/pki.go +++ b/crypto/pki/pki.go @@ -8,9 +8,11 @@ import ( "encoding/json" "encoding/pem" "fmt" + "html" "net" "os" "path/filepath" + "strconv" "strings" "golang.org/x/crypto/ssh" @@ -76,7 +78,7 @@ func GetRootCAPath() string { return filepath.Join(config.StepPath(), publicPath, "root_ca.crt") } -// GetOTTKeyPath returns the path where the ont-time token key is stored based +// 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") @@ -125,37 +127,34 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) { // PKI represents the Public Key Infrastructure used by a certificate authority. type PKI struct { - root, rootKey, rootFingerprint string - intermediate, intermediateKey string - sshHostCert, sshHostKey string - sshUserCert, sshUserKey string - country, locality, organization string - config, defaults string - ottPublicKey *jose.JSONWebKey - ottPrivateKey *jose.JSONWebEncryption - provisioner string - address string - dnsNames []string - caURL string - enableSSH bool + root, rootKey, rootFingerprint string + intermediate, intermediateKey string + sshHostCert, sshHostKey string + sshUserCert, 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) { - var err error - - if _, err = os.Stat(public); os.IsNotExist(err) { + 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.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.Stat(config); os.IsNotExist(err) { if err = os.MkdirAll(config, 0700); err != nil { return nil, errs.FileError(err, config) } @@ -168,6 +167,7 @@ func New(public, private, config string) (*PKI, error) { 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", @@ -320,10 +320,27 @@ func (p *PKI) GenerateSSHSigningKeys(password []byte) error { 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) @@ -372,7 +389,7 @@ func WithoutDB() Option { // 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() + p.tellPKI() key, err := p.ottPrivateKey.CompactSerialize() if err != nil { @@ -420,7 +437,7 @@ func (p *PKI) Save(opt ...Option) error { b, err := json.MarshalIndent(config, "", " ") if err != nil { - return errors.Wrapf(err, "error marshalling %s", p.config) + 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) @@ -429,7 +446,8 @@ func (p *PKI) Save(opt ...Option) error { // Generate the CA URL. if p.caURL == "" { p.caURL = p.dnsNames[0] - _, port, err := net.SplitHostPort(p.address) + var port string + _, port, err = net.SplitHostPort(p.address) if err != nil { return errors.Wrapf(err, "error parsing %s", p.address) } @@ -448,7 +466,7 @@ func (p *PKI) Save(opt ...Option) error { } b, err = json.MarshalIndent(defaults, "", " ") if err != nil { - return errors.Wrapf(err, "error marshalling %s", p.defaults) + 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) @@ -462,5 +480,7 @@ func (p *PKI) Save(opt ...Option) error { ui.Println() ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.") + p.askFeedback() + return nil } diff --git a/crypto/x509util/crt.go b/crypto/x509util/crt.go index ae415779..a8571738 100644 --- a/crypto/x509util/crt.go +++ b/crypto/x509util/crt.go @@ -24,14 +24,17 @@ func Fingerprint(cert *x509.Certificate) string { // SplitSANs splits a slice of Subject Alternative Names into slices of // IP Addresses and DNS Names. If an element is not an IP address, then it // is bucketed as a DNS Name. -func SplitSANs(sans []string) (dnsNames []string, ips []net.IP) { +func SplitSANs(sans []string) (dnsNames []string, ips []net.IP, emails []string) { dnsNames = []string{} ips = []net.IP{} + emails = []string{} if sans == nil { return } for _, san := range sans { - if ip := net.ParseIP(san); ip != nil { + if strings.Contains(san, "@") { + emails = append(emails, san) + } else if ip := net.ParseIP(san); ip != nil { ips = append(ips, ip) } else { // If not IP then assume DNSName. diff --git a/crypto/x509util/crt_test.go b/crypto/x509util/crt_test.go index dc6a7b31..afef7f93 100644 --- a/crypto/x509util/crt_test.go +++ b/crypto/x509util/crt_test.go @@ -4,7 +4,10 @@ import ( "crypto/x509" "encoding/pem" "io/ioutil" + "net" "testing" + + "github.com/smallstep/assert" ) func TestFingerprint(t *testing.T) { @@ -40,3 +43,49 @@ func mustParseCertificate(t *testing.T, filename string) *x509.Certificate { } return cert } + +func TestSplitSANs(t *testing.T) { + tests := []struct { + name string + sans, dns, emails []string + ips []net.IP + }{ + {name: "empty", sans: []string{}, dns: []string{}, ips: []net.IP{}, emails: []string{}}, + { + name: "all-dns", + sans: []string{"foo.internal", "bar.internal"}, + dns: []string{"foo.internal", "bar.internal"}, + ips: []net.IP{}, + emails: []string{}, + }, + { + name: "all-ip", + sans: []string{"0.0.0.0", "127.0.0.1"}, + dns: []string{}, + ips: []net.IP{net.ParseIP("0.0.0.0"), net.ParseIP("127.0.0.1")}, + emails: []string{}, + }, + { + name: "all-email", + sans: []string{"max@smallstep.com", "mariano@smallstep.com"}, + dns: []string{}, + ips: []net.IP{}, + emails: []string{"max@smallstep.com", "mariano@smallstep.com"}, + }, + { + name: "mix", + sans: []string{"foo.internal", "max@smallstep.com", "mariano@smallstep.com", "1.1.1.1", "bar.internal"}, + dns: []string{"foo.internal", "bar.internal"}, + ips: []net.IP{net.ParseIP("1.1.1.1")}, + emails: []string{"max@smallstep.com", "mariano@smallstep.com"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dns, ips, emails := SplitSANs(tt.sans) + assert.Equals(t, dns, tt.dns) + assert.Equals(t, ips, tt.ips) + assert.Equals(t, emails, tt.emails) + }) + } +} diff --git a/crypto/x509util/leafProfile.go b/crypto/x509util/leafProfile.go index 603d57ce..3710f566 100644 --- a/crypto/x509util/leafProfile.go +++ b/crypto/x509util/leafProfile.go @@ -31,6 +31,20 @@ func NewLeafProfile(cn string, iss *x509.Certificate, issPriv crypto.PrivateKey, return newProfile(&Leaf{}, sub, iss, issPriv, withOps...) } +// NewSelfSignedLeafProfile returns a new leaf x509 Certificate profile. +// A new public/private key pair will be generated for the Profile if +// not set in the `withOps` profile modifiers. +func NewSelfSignedLeafProfile(cn string, withOps ...WithOption) (Profile, error) { + sub := defaultLeafTemplate(pkix.Name{CommonName: cn}, pkix.Name{CommonName: cn}) + p, err := newProfile(&Leaf{}, sub, sub, nil, withOps...) + if err != nil { + return nil, err + } + // self-signed certificate + p.SetIssuerPrivateKey(p.SubjectPrivateKey()) + return p, nil +} + // NewLeafProfileWithCSR returns a new leaf x509 Certificate Profile with // Subject Certificate fields populated directly from the CSR. // A public/private keypair **WILL NOT** be generated for this profile because diff --git a/crypto/x509util/profile.go b/crypto/x509util/profile.go index 1a2c3e02..f9733333 100644 --- a/crypto/x509util/profile.go +++ b/crypto/x509util/profile.go @@ -190,6 +190,16 @@ func WithIPAddresses(ips []net.IP) WithOption { } } +// WithEmailAddresses returns a Profile modifier which sets the Email Addresses +// that will be bound to the subject alternative name extension of the Certificate. +func WithEmailAddresses(emails []string) WithOption { + return func(p Profile) error { + crt := p.Subject() + crt.EmailAddresses = emails + return nil + } +} + // WithHosts returns a Profile modifier which sets the DNS Names and IP Addresses // that will be bound to the subject Certificate. // @@ -351,7 +361,7 @@ func (b *base) CreateWriteCertificate(crtOut, keyOut, pass string) ([]byte, erro if err != nil { return nil, errors.WithStack(err) } - if err := utils.WriteFile(crtOut, pem.EncodeToMemory(&pem.Block{ + if err = utils.WriteFile(crtOut, pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: crtBytes, }), 0600); err != nil { diff --git a/exec/exec.go b/exec/exec.go index abced481..8f0f31d4 100644 --- a/exec/exec.go +++ b/exec/exec.go @@ -96,14 +96,20 @@ func OpenInBrowser(url string) error { // Step executes step with the given commands and returns the standard output. func Step(args ...string) ([]byte, error) { - var stderr bytes.Buffer + var stdout bytes.Buffer cmd := exec.Command(os.Args[0], args...) - cmd.Stderr = &stderr - out, err := cmd.Output() - if err != nil { - return nil, errors.Wrapf(err, "error running %s %s:\n%s", os.Args[0], strings.Join(args, " "), stderr.String()) + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Stdout = &stdout + + if err := cmd.Start(); nil != err { + return nil, errors.Wrapf(err, "error starting: %s %s", os.Args[0], strings.Join(args, " ")) } - return out, nil + if err := cmd.Wait(); nil != err { + return nil, errors.Wrapf(err, "error running: %s %s", os.Args[0], strings.Join(args, " ")) + } + + return stdout.Bytes(), nil } // Command executes the given command with it's arguments and returns the @@ -158,7 +164,12 @@ func errorAndExit(name string, err error) { // signalHandler forwards all the signals to the cmd. func signalHandler(cmd *exec.Cmd, exitCh chan int) { - signals := make(chan os.Signal) + // signal.Notify prefers a buffered channel. From documentation: "For a + // channel used for notification of just one signal value, a buffer of size + // 1 is sufficient." As we do not know how many signal values the cmd is + // expecting we select 1 as a sane default. In the future maybe we can make + // this value configurable. + signals := make(chan os.Signal, 1) signal.Notify(signals) defer signal.Stop(signals) for { diff --git a/flags/flags.go b/flags/flags.go index 68e6d1ce..01dae893 100644 --- a/flags/flags.go +++ b/flags/flags.go @@ -11,6 +11,54 @@ import ( ) var ( + // KTY is the flag to set the key type. + KTY = cli.StringFlag{ + Name: "kty", + Value: "EC", + Usage: `The to build the certificate upon. +If unset, default is EC. + +: 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`, + } + + // Size is the flag to set the key size. + Size = cli.IntFlag{ + Name: "size", + 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.`, + } + + // Curve is the flag to se the key curve. + Curve = cli.StringFlag{ + 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. + +: 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`, + } + // Subtle is the flag required for delicate operations. Subtle = cli.BoolFlag{ Name: "subtle", diff --git a/jose/parse.go b/jose/parse.go index 93c9ea40..644af6e9 100644 --- a/jose/parse.go +++ b/jose/parse.go @@ -87,7 +87,7 @@ func ParseKey(filename string, opts ...Option) (*JSONWebKey, error) { } // Unmarshal the plain (or decrypted JWK) - if err := json.Unmarshal(b, jwk); err != nil { + if err = json.Unmarshal(b, jwk); err != nil { return nil, errors.Errorf("error reading %s: unsupported format", filename) } case pemKeyType: diff --git a/make/common.mk b/make/common.mk index faa2c0ff..bb7876e5 100644 --- a/make/common.mk +++ b/make/common.mk @@ -20,25 +20,17 @@ SHELL := /bin/bash 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 vendor: Gopkg.lock $Q dep ensure -BOOTSTRAP=\ - github.com/golang/lint/golint \ - github.com/client9/misspell/cmd/misspell \ - github.com/gordonklaus/ineffassign \ - github.com/tsenart/deadcode \ - github.com/alecthomas/gometalinter - define VENDOR_BIN_TMPL vendor/bin/$(notdir $(1)): vendor $Q go build -o $$@ ./vendor/$(1) VENDOR_BINS += vendor/bin/$(notdir $(1)) endef -$(foreach pkg,$(BOOTSTRAP),$(eval $(call VENDOR_BIN_TMPL,$(pkg)))) - .PHONY: bootstra% vendor ################################################# @@ -121,26 +113,10 @@ integration: bin/$(BINNAME) # Linting ######################################### -LINTERS=\ - gofmt \ - golint \ - vet \ - misspell \ - ineffassign \ - deadcode +lint: + $Q LOG_LEVEL=error golangci-lint run -$(patsubst %,%-bin,$(filter-out gofmt vet,$(LINTERS))): %-bin: vendor/bin/% -gofmt-bin vet-bin: - -$(LINTERS): %: vendor/bin/gometalinter %-bin vendor - $Q PATH=`pwd`/vendor/bin:$$PATH gometalinter --tests --disable-all --vendor \ - --deadline=5m -s data -s pkg --enable $@ ./... -fmt: - $Q gofmt -l -w $(SRC) - -lint: $(LINTERS) - -.PHONY: $(LINTERS) lint fmt +.PHONY: lint ######################################### # Install diff --git a/usage/help.go b/usage/help.go index 4ded437f..c32a877c 100644 --- a/usage/help.go +++ b/usage/help.go @@ -41,6 +41,10 @@ func HelpCommand() cli.Command { Name: "report", Usage: "Writes a JSON report to the HTML docs directory.", }, + cli.BoolFlag{ + Name: "hugo", + Usage: "Writes hugo (vs jekyll) compatible markdown files", + }, }, } } diff --git a/usage/html.go b/usage/html.go index 07a11561..2d0827ff 100644 --- a/usage/html.go +++ b/usage/html.go @@ -30,52 +30,65 @@ func markdownHelpAction(ctx *cli.Context) error { return errs.FileError(err, dir) } + isHugo := ctx.Bool("hugo") + // app index index := path.Join(dir, "step.md") w, err := os.Create(index) if err != nil { return errs.FileError(err, index) } - markdownHelpPrinter(w, mdAppHelpTemplate, ctx.App) + markdownHelpPrinter(w, mdAppHelpTemplate, "", ctx.App) if err := w.Close(); err != nil { return errs.FileError(err, index) } // Subcommands for _, cmd := range ctx.App.Commands { - if err := markdownHelpCommand(ctx.App, cmd, path.Join(dir, cmd.Name)); err != nil { + if err := markdownHelpCommand(ctx.App, cmd, cmd, path.Join(dir, cmd.Name), isHugo); err != nil { return err } } return nil } -func markdownHelpCommand(app *cli.App, cmd cli.Command, base string) error { +func markdownHelpCommand(app *cli.App, cmd cli.Command, parent cli.Command, base string, isHugo bool) error { if err := os.MkdirAll(base, 0755); err != nil { return errs.FileError(err, base) } - index := path.Join(base, "index.md") + fileName := "index.md" + // preserve jekyll compatibility for transition period + if isHugo && len(cmd.Subcommands) > 0 { + fileName = "_index.md" + } + + index := path.Join(base, fileName) w, err := os.Create(index) if err != nil { return errs.FileError(err, index) } + parentName := parent.HelpName + if cmd.HelpName == parent.HelpName { + parentName = "step" + } + if len(cmd.Subcommands) == 0 { - markdownHelpPrinter(w, mdCommandHelpTemplate, cmd) + markdownHelpPrinter(w, mdCommandHelpTemplate, parentName, cmd) return errs.FileError(w.Close(), index) } ctx := cli.NewContext(app, nil, nil) ctx.App = createCliApp(ctx, cmd) - markdownHelpPrinter(w, mdSubcommandHelpTemplate, ctx.App) + markdownHelpPrinter(w, mdSubcommandHelpTemplate, parentName, ctx.App) if err := w.Close(); err != nil { return errs.FileError(err, index) } for _, sub := range cmd.Subcommands { sub.HelpName = fmt.Sprintf("%s %s", cmd.HelpName, sub.Name) - if err := markdownHelpCommand(app, sub, path.Join(base, sub.Name)); err != nil { + if err := markdownHelpCommand(app, sub, cmd, path.Join(base, sub.Name), isHugo); err != nil { return err } } diff --git a/usage/printer.go b/usage/printer.go index c68598c2..654a3e7f 100644 --- a/usage/printer.go +++ b/usage/printer.go @@ -16,6 +16,11 @@ var sectionRe = regexp.MustCompile(`(?m:^##)`) //var sectionRe = regexp.MustCompile(`^## [^\n]*$`) +type frontmatterData struct { + Data interface{} + Parent string +} + // HelpPrinter overwrites cli.HelpPrinter and prints the formatted help to the terminal. func HelpPrinter(w io.Writer, templ string, data interface{}) { b := helpPreprocessor(w, templ, data) @@ -34,11 +39,24 @@ func htmlHelpPrinter(w io.Writer, templ string, data interface{}) []byte { return html } -func markdownHelpPrinter(w io.Writer, templ string, data interface{}) { +func markdownHelpPrinter(w io.Writer, templ string, parent string, data interface{}) { b := helpPreprocessor(w, templ, data) + + frontmatter := frontmatterData{ + Data: data, + Parent: parent, + } + var frontMatterTemplate = `--- layout: auto-doc -title: {{.HelpName}} +title: {{.Data.HelpName}} +menu:{{if .Parent}} + docs: + parent: {{.Parent}}{{else}} + main: + name: "Reference" + parent: "Documentation" + weight: 300{{end}} --- ` @@ -46,7 +64,7 @@ title: {{.HelpName}} if err != nil { panic(err) } - err = t.Execute(w, data) + err = t.Execute(w, frontmatter) if err != nil { panic(err) } diff --git a/usage/usage.go b/usage/usage.go index d097161b..e352def9 100644 --- a/usage/usage.go +++ b/usage/usage.go @@ -3,6 +3,8 @@ package usage import ( "bytes" "fmt" + "html" + "strconv" "strings" "text/template" ) @@ -116,6 +118,15 @@ This documentation is available online at https://smallstep.com/docs/cli ## COPYRIGHT {{.Copyright}} + +## FEEDBACK ` + + html.UnescapeString("&#"+strconv.Itoa(128525)+";") + " " + + html.UnescapeString("&#"+strconv.Itoa(127867)+";") + + ` + +The **step** utility is not instrumented for usage statistics. It does not phone home. +But your feedback is extremely valuable. Any information you can provide regarding how you’re using **step** helps. +Please send us a sentence or two, good or bad: **feedback@smallstep.com** or join https://gitter.im/smallstep/community. {{end}} ` diff --git a/utils/cautils/certificate_flow.go b/utils/cautils/certificate_flow.go index e45a2263..e117ff77 100644 --- a/utils/cautils/certificate_flow.go +++ b/utils/cautils/certificate_flow.go @@ -213,23 +213,22 @@ func (f *CertificateFlow) Sign(ctx *cli.Context, token string, csr api.Certifica // CreateSignRequest is a helper function that given an x509 OTT returns a // simple but secure sign request as well as the private key used. -func (f *CertificateFlow) CreateSignRequest(tok, subject string, sans []string) (*api.SignRequest, crypto.PrivateKey, error) { +func (f *CertificateFlow) CreateSignRequest(ctx *cli.Context, tok, subject string, sans []string) (*api.SignRequest, crypto.PrivateKey, error) { jwt, err := token.ParseInsecure(tok) if err != nil { return nil, nil, err } - pk, err := keys.GenerateDefaultKey() + kty, crv, size, err := utils.GetKeyDetailsFromCLI(ctx, false, "kty", "curve", "size") + if err != nil { + return nil, nil, err + } + pk, err := keys.GenerateKey(kty, crv, size) if err != nil { return nil, nil, err } - var emails []string - dnsNames, ips := splitSANs(sans, jwt.Payload.SANs) - if jwt.Payload.Email != "" { - emails = append(emails, jwt.Payload.Email) - } - + dnsNames, ips, emails := splitSANs(sans, jwt.Payload.SANs) switch jwt.Payload.Type() { case token.AWS: doc := jwt.Payload.Amazon.InstanceIdentityDocument @@ -241,7 +240,7 @@ func (f *CertificateFlow) CreateSignRequest(tok, subject string, sans []string) if !sharedContext.DisableCustomSANs { defaultSANs = append(defaultSANs, subject) } - dnsNames, ips = splitSANs(defaultSANs) + dnsNames, ips, emails = splitSANs(defaultSANs) } case token.GCP: ce := jwt.Payload.Google.ComputeEngine @@ -253,7 +252,7 @@ func (f *CertificateFlow) CreateSignRequest(tok, subject string, sans []string) if !sharedContext.DisableCustomSANs { defaultSANs = append(defaultSANs, subject) } - dnsNames, ips = splitSANs(defaultSANs) + dnsNames, ips, emails = splitSANs(defaultSANs) } case token.Azure: if len(ips) == 0 && len(dnsNames) == 0 { @@ -263,8 +262,13 @@ func (f *CertificateFlow) CreateSignRequest(tok, subject string, sans []string) if !sharedContext.DisableCustomSANs { defaultSANs = append(defaultSANs, subject) } - dnsNames, ips = splitSANs(defaultSANs) + dnsNames, ips, emails = splitSANs(defaultSANs) } + case token.OIDC: + if jwt.Payload.Email != "" { + emails = append(emails, jwt.Payload.Email) + } + subject = jwt.Payload.Subject default: // Use common name in the token subject = jwt.Payload.Subject } @@ -273,10 +277,9 @@ func (f *CertificateFlow) CreateSignRequest(tok, subject string, sans []string) Subject: pkix.Name{ CommonName: subject, }, - SignatureAlgorithm: keys.DefaultSignatureAlgorithm, - DNSNames: dnsNames, - IPAddresses: ips, - EmailAddresses: emails, + DNSNames: dnsNames, + IPAddresses: ips, + EmailAddresses: emails, } csr, err := x509.CreateCertificateRequest(rand.Reader, template, pk) @@ -297,8 +300,8 @@ func (f *CertificateFlow) CreateSignRequest(tok, subject string, sans []string) } // splitSANs unifies the SAN collections passed as arguments and returns a list -// of DNS names and a list of IP addresses. -func splitSANs(args ...[]string) (dnsNames []string, ipAddresses []net.IP) { +// of DNS names, a list of IP addresses, and a list of emails. +func splitSANs(args ...[]string) (dnsNames []string, ipAddresses []net.IP, email []string) { m := make(map[string]bool) var unique []string for _, sans := range args { diff --git a/utils/cautils/offline.go b/utils/cautils/offline.go index 1334b650..f2945bb6 100644 --- a/utils/cautils/offline.go +++ b/utils/cautils/offline.go @@ -50,7 +50,7 @@ func NewOfflineCA(configFile string) (*OfflineCA, error) { } var config authority.Config - if err := json.Unmarshal(b, &config); err != nil { + if err = json.Unmarshal(b, &config); err != nil { return nil, errors.Wrapf(err, "error reading %s", configFile) } @@ -91,7 +91,7 @@ func (c *OfflineCA) VerifyClientCert(certFile, keyFile string) error { return err } // Validate that the certificate and key match - if _, err := tls.X509KeyPair(pem.EncodeToMemory(certPem), pem.EncodeToMemory(keyPem)); err != nil { + if _, err = tls.X509KeyPair(pem.EncodeToMemory(certPem), pem.EncodeToMemory(keyPem)); err != nil { return errors.Wrap(err, "error loading x509 key pair") } @@ -109,7 +109,7 @@ func (c *OfflineCA) VerifyClientCert(certFile, keyFile string) error { Intermediates: intermediatePool, } - if _, err := cert.Verify(opts); err != nil { + if _, err = cert.Verify(opts); err != nil { return errors.Wrapf(err, "failed to verify certificate") } @@ -266,7 +266,8 @@ func (c *OfflineCA) GenerateToken(ctx *cli.Context, typ int, subject string, san switch p := p.(type) { case *provisioner.OIDC: // Run step oauth - out, err := exec.Step("oauth", "--oidc", "--bare", + var out []byte + out, err = exec.Step("oauth", "--oidc", "--bare", "--provider", p.ConfigurationEndpoint, "--client-id", p.ClientID, "--client-secret", p.ClientSecret) if err != nil { diff --git a/utils/cautils/token_flow.go b/utils/cautils/token_flow.go index a6bb34c7..31d45373 100644 --- a/utils/cautils/token_flow.go +++ b/utils/cautils/token_flow.go @@ -85,9 +85,13 @@ func NewTokenFlow(ctx *cli.Context, typ int, subject string, sans []string, caUR switch p := p.(type) { case *provisioner.OIDC: // Run step oauth - out, err := exec.Step("oauth", "--oidc", "--bare", + args := []string{"oauth", "--oidc", "--bare", "--provider", p.ConfigurationEndpoint, - "--client-id", p.ClientID, "--client-secret", p.ClientSecret) + "--client-id", p.ClientID, "--client-secret", p.ClientSecret} + if ctx.IsSet("console") { + args = append(args, "--console") + } + out, err := exec.Step(args...) if err != nil { return "", err }