commit c58db6b98b16ff4aae4039d203cfa98b6a860b71
Author: Mariano Cano
Date: Tue Jul 17 16:06:17 2018 -0700
Initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..b2511b1c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+# Binaries for programs and plugins
+/bin/step
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Others
+*.swp
+coverage.txt
+output
+vendor
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..2da2aa47
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+
+## [Unreleased] - DATE
+### Added
+- Initial version of `step`
diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md
new file mode 100644
index 00000000..5e831f99
--- /dev/null
+++ b/GETTING_STARTED.md
@@ -0,0 +1,44 @@
+# Getting Started with Development
+
+To get started with local development, you will need three things:
+
+- Golang installed locally (instructions available [here](https://golang.org/doc/install))
+- The repository checked out in the appropriate location of your `$GOPATH`
+- A version of `make` available for usage of the `Makefile`
+
+Ensure you've checked out the repository into the appropriate path inside your
+`$GOPATH`. For example, if your `$GOPATH` is set to `~/code`, then you'd check
+this repository out at `~/code/src/github.com/smallstep/cli`. You can
+learn more about `$GOPATH` in the [documentation](https://golang.org/doc/code.html#GOPATH).
+
+### Installing Dependencies and Bootstrapping
+
+Once you've cloned the repository to the appropriate location, you will now be
+able to install any other dependencies via the `make bootstrap` command.
+
+You should only ever need to run this command once, as it will ensure you have
+the right version of `dep` and `gometalinter` installed.
+
+### Building step
+
+To build step, simply run `make build` which will build the cli and place the
+binary in the `bin` folder.
+
+### Running Tests and Linting
+
+Now that you've installed any dependencies, you can run the tests and lint the
+code base simply by running `make`.
+
+If you wish to only test or lint you can run `make test` or `make lint`
+respectively.
+
+### Adding and Removing Dependencies
+
+To add any dependency to the repository, simply import it into your code and
+then run `dep ensure` which will update the `Gopkg.lock` file. A specific
+version of a dependency can be specified by adding it to the `Gopkg.toml` file
+and running `dep ensure`.
+
+To remove a dependency, simply remove it from the codebase and any mention of
+it in the `Gopkg.toml` file and run `dep ensure` which will remove it from the
+`vendor` folder while updating the `Gopkg.lock` file.
diff --git a/Gopkg.lock b/Gopkg.lock
new file mode 100644
index 00000000..4fa93a11
--- /dev/null
+++ b/Gopkg.lock
@@ -0,0 +1,311 @@
+# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
+
+
+[[projects]]
+ branch = "master"
+ name = "github.com/ThomasRooney/gexpect"
+ packages = ["."]
+ revision = "5482f03509440585d13d8f648989e05903001842"
+
+[[projects]]
+ name = "github.com/alecthomas/gometalinter"
+ packages = ["."]
+ revision = "bae2f1293d092fd8167939d5108d1b025eaef9de"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/alecthomas/units"
+ packages = ["."]
+ revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
+
+[[projects]]
+ name = "github.com/asaskevich/govalidator"
+ packages = ["."]
+ revision = "ccb8e960c48f04d6935e72476ae4a51028f9e22f"
+ version = "v9"
+
+[[projects]]
+ name = "github.com/boombuler/barcode"
+ packages = [
+ ".",
+ "qr",
+ "utils"
+ ]
+ revision = "3cfea5ab600ae37946be2b763b8ec2c1cf2d272d"
+ version = "v1.0.0"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/bouk/monkey"
+ packages = ["."]
+ revision = "5df1f207ff77e025801505ae4d903133a0b4353f"
+
+[[projects]]
+ name = "github.com/client9/misspell"
+ packages = [
+ ".",
+ "cmd/misspell"
+ ]
+ revision = "b90dc15cfd220ecf8bbc9043ecb928cef381f011"
+ version = "v0.3.4"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/corpix/uarand"
+ packages = ["."]
+ revision = "2b8494104d86337cdd41d0a49cbed8e4583c0ab4"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/golang/lint"
+ packages = ["golint"]
+ revision = "06c8688daad7faa9da5a0c2f163a3d14aac986ca"
+
+[[projects]]
+ name = "github.com/google/go-cmp"
+ packages = [
+ "cmp",
+ "cmp/cmpopts",
+ "cmp/internal/diff",
+ "cmp/internal/function",
+ "cmp/internal/value"
+ ]
+ revision = "3af367b6b30c263d47e8895973edcca9a49cf029"
+ version = "v0.2.0"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/google/shlex"
+ packages = ["."]
+ revision = "6f45313302b9c56850fc17f99e40caebce98c716"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/gordonklaus/ineffassign"
+ packages = ["."]
+ revision = "7bae11eba15a3285c75e388f77eb6357a2d73ee2"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/grantae/certinfo"
+ packages = ["."]
+ revision = "59d56a35515b3ab2326749924739cfe58facc991"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/icrowley/fake"
+ packages = ["."]
+ revision = "4178557ae428460c3780a381c824a1f3aceb6325"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/kballard/go-shellquote"
+ packages = ["."]
+ revision = "95032a82bc518f77982ea72343cc1ade730072f0"
+
+[[projects]]
+ name = "github.com/kr/pty"
+ packages = ["."]
+ revision = "fa756f09eeb418bf1cc6268c66ceaad9bb98f598"
+ version = "v1.1.2"
+
+[[projects]]
+ name = "github.com/nicksnyder/go-i18n"
+ packages = [
+ "i18n",
+ "i18n/bundle",
+ "i18n/language",
+ "i18n/translation"
+ ]
+ revision = "0dc1626d56435e9d605a29875701721c54bc9bbd"
+ version = "v1.10.0"
+
+[[projects]]
+ name = "github.com/pelletier/go-toml"
+ packages = ["."]
+ revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
+ version = "v1.2.0"
+
+[[projects]]
+ name = "github.com/pkg/errors"
+ packages = ["."]
+ revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
+ version = "v0.8.0"
+
+[[projects]]
+ name = "github.com/pquerna/otp"
+ packages = [
+ ".",
+ "hotp",
+ "totp"
+ ]
+ revision = "b7b89250c468c06871d3837bee02e2d5c155ae19"
+ version = "v1.0.0"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/samfoo/ansi"
+ packages = ["."]
+ revision = "b6bd2ded7189ce35bc02233b554eb56a5146af73"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/shurcooL/sanitized_anchor_name"
+ packages = ["."]
+ revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/smallstep/assert"
+ packages = ["."]
+ revision = "56bdbac904282a87cfca8f33ea25a6486419e64b"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/smallstep/go-makefile"
+ packages = ["."]
+ revision = "c6025f797567554133ce98a3fcc224b3691a9f05"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/tsenart/deadcode"
+ packages = ["."]
+ revision = "210d2dc333e90c7e3eedf4f2242507a8e83ed4ab"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/urfave/cli"
+ packages = ["."]
+ revision = "8e01ec4cd3e2d84ab2fe90d8210528ffbb06d8ff"
+
+[[projects]]
+ name = "github.com/weppos/publicsuffix-go"
+ packages = ["publicsuffix"]
+ revision = "386050f8211b04c965721c3591e7d96650a1ea86"
+ version = "v0.4.0"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/zmap/zcrypto"
+ packages = [
+ "json",
+ "x509",
+ "x509/ct",
+ "x509/pkix"
+ ]
+ revision = "8a129f796df4f732131f4617e81a0ea3c9dc63cf"
+
+[[projects]]
+ branch = "master"
+ name = "github.com/zmap/zlint"
+ packages = [
+ ".",
+ "lints",
+ "util"
+ ]
+ revision = "12b8dc0338e6261fb4ad6a623c0a4c1bc99b3dfe"
+
+[[projects]]
+ branch = "master"
+ name = "golang.org/x/crypto"
+ packages = [
+ "bcrypt",
+ "blowfish",
+ "curve25519",
+ "ed25519",
+ "ed25519/internal/edwards25519",
+ "internal/subtle",
+ "nacl/auth",
+ "nacl/box",
+ "nacl/secretbox",
+ "nacl/sign",
+ "pbkdf2",
+ "poly1305",
+ "salsa20/salsa",
+ "scrypt",
+ "ssh/terminal"
+ ]
+ revision = "a49355c7e3f8fe157a85be2f77e6e269a0f89602"
+
+[[projects]]
+ branch = "master"
+ name = "golang.org/x/lint"
+ packages = ["."]
+ revision = "06c8688daad7faa9da5a0c2f163a3d14aac986ca"
+
+[[projects]]
+ branch = "master"
+ name = "golang.org/x/net"
+ packages = ["idna"]
+ revision = "d0887baf81f4598189d4e12a37c6da86f0bba4d0"
+
+[[projects]]
+ branch = "master"
+ name = "golang.org/x/sys"
+ packages = [
+ "unix",
+ "windows"
+ ]
+ revision = "ac767d655b305d4e9612f5f6e33120b9176c4ad4"
+
+[[projects]]
+ name = "golang.org/x/text"
+ packages = [
+ "collate",
+ "collate/build",
+ "internal/colltab",
+ "internal/gen",
+ "internal/tag",
+ "internal/triegen",
+ "internal/ucd",
+ "language",
+ "secure/bidirule",
+ "transform",
+ "unicode/bidi",
+ "unicode/cldr",
+ "unicode/norm",
+ "unicode/rangetable"
+ ]
+ revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
+ version = "v0.3.0"
+
+[[projects]]
+ branch = "master"
+ name = "golang.org/x/tools"
+ packages = [
+ "go/ast/astutil",
+ "go/gcexportdata",
+ "go/internal/gcimporter",
+ "go/types/typeutil"
+ ]
+ revision = "57f659e14dda699044a713b0f8d1d3ff033e0455"
+
+[[projects]]
+ name = "gopkg.in/alecthomas/kingpin.v3-unstable"
+ packages = ["."]
+ revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306"
+
+[[projects]]
+ branch = "v2"
+ name = "gopkg.in/square/go-jose.v2"
+ packages = [
+ ".",
+ "cipher",
+ "json",
+ "jwt"
+ ]
+ revision = "dac1c778d6ac5c3ef8150b3e331ed944179f56e9"
+ source = "github.com/maraino/go-jose"
+
+[[projects]]
+ name = "gopkg.in/yaml.v2"
+ packages = ["."]
+ revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
+ version = "v2.2.1"
+
+[solve-meta]
+ analyzer-name = "dep"
+ analyzer-version = 1
+ inputs-digest = "173e6eb13528165fee3e441bf795759815662b55ce4f0a05d6ce91844ce81507"
+ solver-name = "gps-cdcl"
+ solver-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
new file mode 100644
index 00000000..e9db25b9
--- /dev/null
+++ b/Gopkg.toml
@@ -0,0 +1,84 @@
+# Gopkg.toml example
+#
+# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
+# for detailed Gopkg.toml documentation.
+#
+# required = ["github.com/user/thing/cmd/thing"]
+# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
+#
+# [[constraint]]
+# name = "github.com/user/project"
+# version = "1.0.0"
+#
+# [[constraint]]
+# name = "github.com/user/project2"
+# branch = "dev"
+# source = "github.com/myfork/project2"
+#
+# [[override]]
+# name = "github.com/x/y"
+# version = "2.4.0"
+#
+# [prune]
+# 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",
+ "github.com/smallstep/go-makefile"
+]
+
+[[constraint]]
+ name = "github.com/alecthomas/gometalinter"
+ revision = "bae2f1293d092fd8167939d5108d1b025eaef9de"
+
+[[override]]
+ name = "gopkg.in/alecthomas/kingpin.v3-unstable"
+ revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306"
+
+[[constraint]]
+ name = "github.com/google/go-cmp"
+ version = "0.2.0"
+
+[[constraint]]
+ name = "github.com/pkg/errors"
+ version = "0.8.0"
+
+[[constraint]]
+ branch = "master"
+ name = "golang.org/x/crypto"
+
+[prune]
+ go-tests = true
+ unused-packages = true
+
+[[constraint]]
+ name = "gopkg.in/square/go-jose.v2"
+ # version = "2.1.6"
+ # Using special branch with pbkdf2 support
+ source = "github.com/maraino/go-jose"
+ branch = "v2"
+
+[[constraint]]
+ branch = "master"
+ name = "github.com/urfave/cli"
+
+[[constraint]]
+ branch = "master"
+ name = "github.com/grantae/certinfo"
+
+[[constraint]]
+ branch = "master"
+ name = "github.com/zmap/zcrypto"
+
+[[constraint]]
+ branch = "master"
+ name = "github.com/zmap/zlint"
+
+[[constraint]]
+ branch = "master"
+ name = "github.com/smallstep/assert"
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..7702f5f2
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,131 @@
+PKG=github.com/smallstep/cli/cmd/step
+BINNAME=step
+
+# Set V to 1 for verbose output from the Makefile
+Q=$(if $V,,@)
+SRC = $(shell find . -type f -name '*.go' -not -path "./vendor/*")
+PREFIX?=
+GOOS_OVERRIDE?=
+
+# Set shell to bash for `echo -e`
+SHELL:=/bin/bash
+
+all: build lint test
+
+.PHONY: all
+
+#########################################
+# Bootstrapping
+#########################################
+
+bootstrap:
+ $Q which dep || go get github.com/golang/dep/cmd/dep
+ $Q dep ensure
+
+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: bootstrap vendor
+
+#########################################
+# Build
+#########################################
+
+# Version flags to embed in the binaries
+# VERSION := $(shell [ -d .git ] && git describe --tags --always --dirty="-dev")
+DATE := $(shell date -u '+%Y-%m-%d %H:%M UTC')
+LDFLAGS := -ldflags='-w -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
+GOFLAGS := CGO_ENABLED=0
+
+build: $(PREFIX)bin/$(BINNAME)
+ @echo "Build Complete!"
+
+$(PREFIX)bin/$(BINNAME): vendor $(call rwildcard,*.go)
+ $Q mkdir -p $(@D)
+ $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -i -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG)
+
+.PHONY: build
+
+#########################################
+# Go generate
+#########################################
+
+generate:
+ $Q go generate ./...
+
+.PHONY: generate
+
+#########################################
+# Test
+#########################################
+test:
+ $Q $(GOFLAGS) go test -short -cover ./...
+
+vtest:
+ $(Q)for d in $$(go list ./... | grep -v vendor); do \
+ echo -e "TESTS FOR: for \033[0;35m$$d\033[0m"; \
+ $(GOFLAGS) go test -v -bench=. -run=. -short -coverprofile=profile.coverage.out -covermode=atomic $$d; \
+ out=$$?; \
+ if [[ $$out -ne 0 ]]; then ret=$$out; fi;\
+ rm -f profile.coverage.out; \
+ done; exit $$ret;
+
+.PHONY: test vtest
+
+integration: $(PREFIX)bin/$(BINNAME)
+ $Q $(GOFLAGS) go test -tags=integration ./integration/...
+
+.PHONY: integration
+
+#########################################
+# Linting
+#########################################
+
+LINTERS=\
+ gofmt \
+ golint \
+ vet \
+ misspell \
+ ineffassign \
+ deadcode
+
+$(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
+
+#########################################
+# Clean
+#########################################
+
+clean:
+ @echo "You will need to run 'make bootstrap' or 'dep ensure' directly to re-download any dependencies."
+ $Q rm -rf vendor
+ifneq ($(BINNAME),"")
+ $Q rm -f bin/$(BINNAME)
+endif
+
+.PHOMY: clean
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..67b7a3e4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# Step CLI
+
+This repository contains the code for the `step` command line tool.
+
+TODO: add description
+
+Please ensure to read the [CLI Style Guide](https://github.com/urfave/cli)
+before implementing any features or modifying behavior as it contains
+expectations surrounding how the CLI should behave.
+
+All changes to behavior *must* be documented in the [CHANGELOG.md](./CHANGELOG.md).
+
+### Table of Contents
+
+- [CHANGELOG](./CHANGELOG.md)
+- [Getting Started with Development](./GETTING_STARTED.md)
+- [How to add a new Command](./command/README.md)
diff --git a/cmd/step/main.go b/cmd/step/main.go
new file mode 100644
index 00000000..cf59fbb8
--- /dev/null
+++ b/cmd/step/main.go
@@ -0,0 +1,180 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "reflect"
+ "regexp"
+ "strings"
+
+ "github.com/urfave/cli"
+
+ "github.com/smallstep/cli/command"
+ "github.com/smallstep/cli/command/version"
+ "github.com/smallstep/cli/config"
+ "github.com/smallstep/cli/usage"
+
+ // Enabled commands
+ _ "github.com/smallstep/cli/command/certificates"
+ _ "github.com/smallstep/cli/command/crypto"
+ _ "github.com/smallstep/cli/command/oauth"
+
+ // Profiling and debugging
+ _ "net/http/pprof"
+)
+
+// Version is set by an LDFLAG at build time representing the git tag or commit
+// for the current release
+var Version = "N/A"
+
+// BuildTime is set by an LDFLAG at build time representing the timestamp at
+// the time of build
+var BuildTime = "N/A"
+
+func init() {
+ config.Set(Version, BuildTime)
+}
+
+func main() {
+ // Override global framework components
+ cli.VersionPrinter = func(c *cli.Context) {
+ version.Command(c)
+ }
+ cli.AppHelpTemplate = usage.AppHelpTemplate
+ cli.SubcommandHelpTemplate = usage.SubcommandHelpTemplate
+ cli.CommandHelpTemplate = usage.CommandHelpTemplate
+ cli.HelpPrinter = helpPrinter
+ cli.FlagNamePrefixer = usage.FlagNamePrefixer
+ cli.FlagStringer = stringifyFlag
+
+ // Configure cli app
+ app := cli.NewApp()
+ app.Name = "step"
+ app.HelpName = "step"
+ app.Usage = "plumbing for distributed systems"
+ app.Version = config.Version()
+ app.Commands = command.Retrieve()
+ app.Flags = append(app.Flags, cli.HelpFlag)
+ app.EnableBashCompletion = true
+ app.Copyright = "(c) 2018 Smallstep Inc."
+
+ // All non-successful output should be written to stderr
+ app.Writer = os.Stderr
+ app.ErrWriter = os.Stderr
+
+ // Start the golang debug logger if environment variable is set.
+ // See https://golang.org/pkg/net/http/pprof/
+ debugProfAddr := os.Getenv("STEP_PROF_ADDR")
+ if debugProfAddr != "" {
+ go func() {
+ log.Println(http.ListenAndServe(debugProfAddr, nil))
+ }()
+ }
+
+ if err := app.Run(os.Args); err != nil {
+ if os.Getenv("STEPDEBUG") == "1" {
+ fmt.Fprintf(os.Stderr, "%+v\n", err)
+ } else {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ os.Exit(1)
+ }
+}
+
+var sectionRe = regexp.MustCompile(`(?m:^##)`)
+
+//var sectionRe = regexp.MustCompile(`^## [^\n]*$`)
+
+func findSectionEnd(h, s string) int {
+ start := strings.Index(s, fmt.Sprintf("## %s", h))
+ if start == -1 {
+ return start
+ }
+ nextSection := sectionRe.FindStringIndex(s[start+2:])
+ if nextSection == nil {
+ return len(s)
+ }
+ return start + 2 + nextSection[0]
+}
+
+// Convert some stuff that we can't easily write in help files because
+// backticks and raw strings don't mix:
+// - "" to "`foo`"
+// - "'''" to "```"
+func markdownify(b []byte) []byte {
+ for i := 0; i < len(b); i++ {
+ switch b[i] {
+ case '<':
+ if b[i-1] != '\\' {
+ b[i] = '`'
+ }
+ case '>':
+ if b[i-1] != '\\' {
+ b[i] = '`'
+ }
+ case '\'':
+ if len(b) >= i+3 && string(b[i:i+3]) == "'''" {
+ b[i] = '`'
+ b[i+1] = '`'
+ b[i+2] = '`'
+ i += 2
+ }
+ }
+ }
+ return b
+}
+
+func helpPrinter(w io.Writer, templ string, data interface{}) {
+ buf := new(bytes.Buffer)
+ cli.HelpPrinterCustom(buf, templ, data, nil)
+ //w.Write(buf.Bytes())
+ s := string(markdownify(buf.Bytes()))
+ // Move the OPTIONS section to the right place. urfave puts them at the end
+ // of the file, we want them to be after POSITIONAL ARGUMENTS, DESCRIPTION,
+ // USAGE, or NAME (in that order, depending on which sections exist).
+ optLoc := strings.Index(s, "## OPTIONS")
+ if optLoc != -1 {
+ optEnd := findSectionEnd("OPTIONS", s)
+ if optEnd != -1 {
+ options := s[optLoc:optEnd]
+ s = s[:optLoc] + s[optEnd:]
+ if newLoc := findSectionEnd("POSITIONAL ARGUMENTS", s); newLoc != -1 {
+ s = s[:newLoc] + options + s[newLoc:]
+ } else if newLoc := findSectionEnd("DESCRIPTION", s); newLoc != -1 {
+ s = s[:newLoc] + options + s[newLoc:]
+ } else if newLoc := findSectionEnd("USAGE", s); newLoc != -1 {
+ s = s[:newLoc] + options + s[newLoc:]
+ } else if newLoc := findSectionEnd("NAME", s); newLoc != -1 {
+ s = s[:newLoc] + options + s[newLoc:]
+ } else {
+ // Keep it at the end I guess :/.
+ s = s + options
+ }
+ }
+ }
+ w.Write(usage.Render([]byte(s)))
+}
+
+func flagValue(f cli.Flag) reflect.Value {
+ fv := reflect.ValueOf(f)
+ for fv.Kind() == reflect.Ptr {
+ fv = reflect.Indirect(fv)
+ }
+ return fv
+}
+
+var placeholderString = regexp.MustCompile(`<.*?>`)
+
+func stringifyFlag(f cli.Flag) string {
+ fv := flagValue(f)
+ usage := fv.FieldByName("Usage").String()
+ placeholder := placeholderString.FindString(usage)
+ if placeholder == "" {
+ placeholder = ""
+ }
+ return cli.FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder) + "\t" + usage
+}
diff --git a/command/README.md b/command/README.md
new file mode 100644
index 00000000..7dcdd7d4
--- /dev/null
+++ b/command/README.md
@@ -0,0 +1,104 @@
+# How to add a new Command
+
+Before making any changes, please consult the [CLI style guide](https://github.com/urfave/cli)!
+
+### Package Layout
+
+The [`urfave/cli`](https://github.com/urfave/cli) package that forms the basis
+of Step CLI supports N-levels of command hierarchy. Each level of the hierarchy
+should exist within its own package if possible. For example, `version` and
+`help` exist inside their own packages inside the top-level `command` package.
+
+Any package used by a command but does not contain explicit business logic
+directly related to the command should exist in the top-level of this
+repository. For example, the `github.com/smallstep/cli/flags` and
+`github.com/smallstep/cli/errs` package are used by many different
+commands and contain functionality for defining flags and creating/manipulating
+errors.
+
+### Adding a Command
+
+Once you figured out where to add the command inside the package hierarchy you
+must register the command. This way the command *can* be made available if
+desired inside the `cmd/step/main.go`.
+
+An example of defining a command and registering it:
+
+```golang
+package validate
+
+import (
+ "github.com/urfave/cli"
+
+ "github.com/smallstep/cli/command"
+ "github.com/smallstep/cli/flags"
+)
+
+func init() {
+ cmd := cli.Command{
+ Name: "validate",
+ Usage: "Returns whether or not the provided token is valid",
+ Flags: []cli.Flag{
+ flags.Token("The one-time token value to validate"),
+ },
+ Action: validate,
+ }
+
+ command.Register(validate)
+}
+```
+
+Once this is done, you then must import the pkg inside `cmd/step/main.go` so
+the packages `init` method is run appropriately. This only needs to be done for
+top-level commands.
+
+```golang
+package main
+
+import (
+ "github.com/urfave/cli"
+
+ _ "github.com/smallstep/cli/validate"
+)
+```
+
+This will ensure that the `smallstep/cli/validate` package is initialized
+and thus registered with the `smallstep/cli/command`.
+
+### Usage, Flags, Errors, and Prompts
+
+There are three packages which contain functionality to make writing commands easier:
+
+- `github.com/smallstep/cli/usage`
+- `github.com/smallstep/cli/flags`
+- `github.com/smallstep/cli/prompts`
+- `github.com/smallstep/cli/errs`
+
+The usage package is used to extend the default documentation provided by
+`urfave/cli` by enabling us to document arguments, whether they are optional or
+required, and ensuring they're printed out as a part of the `step help` or
+`step -h` flow. If you need to add a different type of annotation to
+document an argument just add it to the `usage.Argument` struct!
+
+When you add a flag, look into the pre-existing ones inside the `flags`
+package. Could you use one of the pre-existing flags in order to reduce
+duplication? If not, make sure to add a flag so it could be used in future!
+
+The `errs` package contains functionality for defining and working with errors
+to ensure they are mutated properly into a `urfave/cli.ExitError` which ensures
+the process returns an appropriate exit code on termination. When you create an
+error, consider whether or not it's general and could be predefined inside the
+`errs` package. Errors that are specific to the command itself should exist
+only inside that commands respective package.
+
+The `prompts` package is a small wrapper around the various different types of
+prompts used by the commands. If you need a new prompt, consider adding a new
+function to this package to tailor the prompt for the step cli. This way other
+commands can adopt the step aesthetic as new functionality is introduced.
+
+### Hiding a Command
+
+Sometimes it's desirable to prevent a command from showing up in the help menu
+because it's been deprecated *or* it's not ready for users to leverage. This
+can be achieved by setting the `Hidden` property on the `cli.Command` struct to
+`true`.
diff --git a/command/certificates/bundle.go b/command/certificates/bundle.go
new file mode 100644
index 00000000..188186fb
--- /dev/null
+++ b/command/certificates/bundle.go
@@ -0,0 +1,65 @@
+package certificates
+
+import (
+ "encoding/pem"
+ "io/ioutil"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+func bundleCommand() cli.Command {
+ return cli.Command{
+ Name: "bundle",
+ Action: cli.ActionFunc(bundleAction),
+ Usage: `bundle a certificate with intermediate certificate(s) needed for certificate path validation.`,
+ UsageText: `step certificates bundle CRT_FILE BUNDLE_FILE`,
+ Description: `The 'step certificates bundle' command bundles a certificate with any
+intermediates necessary to validate the certificate.
+
+ POSITIONAL ARGUMENTS
+ CRT_FILE
+ The path to a leaf certificate to bundle with issuing certificate(s).
+
+ CA_FILE
+ The path to the Certificate Authoriy issusing certificate for the leaf.
+
+ BUNDLE_FILE
+ The path to write the bundle.`,
+ }
+}
+
+func bundleAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 3); err != nil {
+ return err
+ }
+
+ crtFile := ctx.Args().Get(0)
+ crtBytes, err := ioutil.ReadFile(crtFile)
+ if err != nil {
+ return errs.FileError(err, crtFile)
+ }
+ crtBlock, _ := pem.Decode(crtBytes)
+ if crtBlock == nil {
+ return errors.Errorf("could not parse certificate file '%s'", crtFile)
+ }
+
+ caFile := ctx.Args().Get(1)
+ caBytes, err := ioutil.ReadFile(caFile)
+ if err != nil {
+ return errs.FileError(err, caFile)
+ }
+ caBlock, _ := pem.Decode(caBytes)
+ if caBlock == nil {
+ return errors.Errorf("could not parse certificate file '%s'", caFile)
+ }
+
+ chainFile := ctx.Args().Get(2)
+ if err := ioutil.WriteFile(chainFile,
+ append(pem.EncodeToMemory(crtBlock), pem.EncodeToMemory(caBlock)...), 0600); err != nil {
+ return errs.FileError(err, chainFile)
+ }
+
+ return nil
+}
diff --git a/command/certificates/certificates.go b/command/certificates/certificates.go
new file mode 100644
index 00000000..da99bad0
--- /dev/null
+++ b/command/certificates/certificates.go
@@ -0,0 +1,37 @@
+package certificates
+
+import (
+ "github.com/smallstep/cli/command"
+ "github.com/urfave/cli"
+)
+
+// Command returns the cli.Command for jwt and related subcommands.
+func init() {
+ cmd := cli.Command{
+ Name: "certificates",
+ Usage: "create, revoke, validate, bundle, and otherwise manage certificates.",
+ UsageText: "step certificates [arguments] [global-flags] [subcommand-flags]",
+ Description: `The 'step certificates' command group provides facilities for creating
+ certificate signing requests (CSRs), creating self-signed certificates
+ (e.g., for use as a root certificate authority), generating leaf or
+ intermediate CA certificate by signing a CSR, validating certificates,
+ renewing certificates, generating certificate bundles, and key-wrapping
+ of private keys.
+
+ More information about certificates in general (as opposed to the
+ certificates commands) can be found at 'step help topics certificates'
+ or online at [URL].`,
+
+ Subcommands: cli.Commands{
+ bundleCommand(),
+ createCommand(),
+ inspectCommand(),
+ lintCommand(),
+ //renewCommand(),
+ signCommand(),
+ verifyCommand(),
+ },
+ }
+
+ command.Register(cmd)
+}
diff --git a/command/certificates/create.go b/command/certificates/create.go
new file mode 100644
index 00000000..075939bf
--- /dev/null
+++ b/command/certificates/create.go
@@ -0,0 +1,254 @@
+package certificates
+
+import (
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+
+ "github.com/pkg/errors"
+ stepx509 "github.com/smallstep/cli/crypto/certificates/x509"
+ "github.com/smallstep/cli/crypto/keys"
+ "github.com/smallstep/cli/errs"
+ "github.com/smallstep/cli/utils/reader"
+ "github.com/urfave/cli"
+)
+
+func createCommand() cli.Command {
+ return cli.Command{
+ Name: "create",
+ Action: cli.ActionFunc(createAction),
+ Usage: "create a certificate or certificate signing request",
+ UsageText: `step certificates create SUBJECT CRT_FILE KEY_FILE [--type=CERTIFICATE_TYPE]
+ [--profile=PROFILE] [--csr] [--token=TOKEN]`,
+ Description: `The 'step certificates create' command generates a certificate or a
+ certificate signing requests (CSR) that can be signed later using 'step
+ certificates sign' (or some other tool) to produce a certificate.
+
+ This command can create x.509 certificates for use with TLS as well as SSH
+ certificates.
+
+POSITIONAL ARGUMENTS
+ SUBJECT
+ The subject of the certificate. Typically this is a hostname for services or an email address for people.
+
+ CRT_FILE
+ File to write CRT or CSR to (PEM format)
+
+ KEY_FILE
+ File to write private key to (PEM format)`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "type",
+ Value: "x509",
+ Usage: `The type of certificate to generate. If not specified default is x509.
+
+ CERTIFICATE_TYPE must be one of:
+ x509
+ Generate an x.509 certificate suitable for use with TLS.
+ ssh
+ Generate an SSH certificate.`,
+ },
+ cli.StringFlag{
+ Name: "profile",
+ Value: "leaf",
+ Usage: `The certificate profile sets various certificate details such as
+ certificate use and expiration. The default profile is 'leaf' which is suitable
+ for a client or server using TLS.
+
+ PROFILE must be one of:
+ leaf
+ Generate a leaf x.509 certificate suitable for use with TLs.
+ intermediate-ca
+ Generate a certificate that can be used to sign additional leaf or intermediate certificates.
+ root-ca
+ Generate a new self-signed root certificate suitable for use as a root CA.`,
+ },
+ cli.StringFlag{
+ Name: "token",
+ Usage: `A provisioning token or bootstrap token for authenticating to a remote CA.`,
+ },
+ cli.BoolFlag{
+ Name: "csr",
+ Usage: `Generate a certificate signing request (CSR) instead of a certificate.`,
+ },
+ cli.StringFlag{
+ Name: "ca",
+ Usage: `The certificate authority used to issue the new certificate (PEM file).`,
+ },
+ cli.StringFlag{
+ Name: "ca-key",
+ Usage: `The certificate authority private key used to sign the new certificate (PEM file).`,
+ },
+ cli.BoolFlag{
+ Name: "no-password",
+ Usage: "TODO, requires --insecure",
+ },
+ cli.BoolFlag{
+ Name: "subtle",
+ Hidden: true,
+ },
+ cli.BoolFlag{
+ Name: "insecure",
+ Hidden: true,
+ },
+ },
+ }
+}
+
+func createAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 3); err != nil {
+ return err
+ }
+
+ // Use password to protect private JWK by default
+ usePassword := true
+ if ctx.Bool("no-password") {
+ if ctx.Bool("insecure") {
+ usePassword = false
+ } else {
+ return errs.RequiredWithFlag(ctx, "insecure", "no-password")
+ }
+ }
+
+ subject := ctx.Args().Get(0)
+ crtFile := ctx.Args().Get(1)
+ keyFile := ctx.Args().Get(2)
+ if crtFile == keyFile {
+ return errs.EqualArguments(ctx, "CRT_FILE", "KEY_FILE")
+ }
+
+ typ := ctx.String("type")
+ prof := ctx.String("profile")
+ caPath := ctx.String("ca")
+ caKeyPath := ctx.String("ca-key")
+ if ctx.Bool("csr") {
+ typ = "x509-csr"
+ }
+
+ var (
+ err error
+ priv interface{}
+ pubPEM *pem.Block
+ )
+ switch typ {
+ case "x509-csr":
+ priv, err = keys.GenerateDefaultKey()
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ _csr := &x509.CertificateRequest{
+ Subject: pkix.Name{
+ CommonName: subject,
+ },
+ }
+ csrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ pubPEM = &pem.Block{
+ Type: "CSR",
+ Bytes: csrBytes,
+ Headers: map[string]string{},
+ }
+ case "x509":
+ var (
+ err error
+ profile stepx509.Profile
+ )
+ switch prof {
+ case "leaf":
+ issIdentity, err := loadIssuerIdentity(prof, caPath, caKeyPath)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ profile, err = stepx509.NewLeafProfile(subject, issIdentity.Crt,
+ issIdentity.Key)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ case "intermediate-ca":
+ issIdentity, err := loadIssuerIdentity(prof, caPath, caKeyPath)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ profile, err = stepx509.NewIntermediateProfile(subject,
+ issIdentity.Crt, issIdentity.Key)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ case "root-ca":
+ profile, err = stepx509.NewRootProfile(subject)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ default:
+ return errs.InvalidFlagValue(ctx, "profile", prof, "leaf, intermediate-ca, root-ca")
+ }
+ crtBytes, err := profile.CreateCertificate()
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ pubPEM = &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: crtBytes,
+ Headers: map[string]string{},
+ }
+ priv = profile.SubjectPrivateKey()
+ case "ssh":
+ return errors.Errorf("implementation incomplete! Come back later ...")
+ default:
+ return errs.InvalidFlagValue(ctx, "type", typ, "x509, ssh")
+ }
+
+ if err := ioutil.WriteFile(crtFile, pem.EncodeToMemory(pubPEM),
+ os.FileMode(0600)); err != nil {
+ return errs.FileError(err, crtFile)
+ }
+
+ var pass string
+ if usePassword {
+ if err := reader.ReadPasswordSubtle(
+ fmt.Sprintf("Password with which to encrypt private key file `%s`: ", keyFile),
+ &pass, "Password", reader.RetryOnEmpty); err != nil {
+ return errors.WithStack(err)
+ }
+ }
+ if err := keys.WritePrivateKey(priv, pass, keyFile); err != nil {
+ return errors.WithStack(err)
+ }
+ return nil
+}
+
+func loadIssuerIdentity(profile, caPath, caKeyPath string) (*stepx509.Identity, error) {
+ if caPath == "" {
+ return nil, errors.Errorf("Missing value for flag '--ca'.\n\nFlags "+
+ "'--ca' and '--ca-key' are required when creating a %s x509 Certificate.",
+ strings.Title(profile))
+ }
+ if caKeyPath == "" {
+ return nil, errors.Errorf("Missing value for flag '--ca-key'.\n\nFlags "+
+ "'--ca' and '--ca-key' are required when creating a %s x509 Certificate.",
+ strings.Title(profile))
+ }
+ return stepx509.LoadIdentityFromDisk(caPath, caKeyPath,
+ func() (string, error) {
+ var pass string
+ if err := reader.ReadPasswordSubtle(
+ fmt.Sprintf("Password with which to decrypt CA private key file `%s`: ", caKeyPath),
+ &pass, "Password", reader.RetryOnEmpty); err != nil {
+ return "", errors.WithStack(err)
+ }
+ return pass, nil
+ })
+
+}
diff --git a/command/certificates/inspect.go b/command/certificates/inspect.go
new file mode 100644
index 00000000..fa808c1c
--- /dev/null
+++ b/command/certificates/inspect.go
@@ -0,0 +1,166 @@
+package certificates
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+
+ "github.com/grantae/certinfo"
+ "github.com/pkg/errors"
+ stepx509 "github.com/smallstep/cli/crypto/certificates/x509"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+ zx509 "github.com/zmap/zcrypto/x509"
+)
+
+func inspectCommand() cli.Command {
+ return cli.Command{
+ Name: "inspect",
+ Action: cli.ActionFunc(inspectAction),
+ Usage: `print certificate or CSR details in human readable format.`,
+ UsageText: `step certificates inspect CRT_FILE [--format=FORMAT]`,
+ Description: `The 'step certificates inspect' command prints the details of a certificate
+or CSR in a human readable format. Output from the inspect command is printed to
+STDERR instead of STDOUT unless. This is an intentional barrier to accidental
+misuse: scripts should never rely on the contents of an unvalidated certificate.
+For scripting purposes, use 'step certificates verify'.
+
+ POSITIONAL ARGUMENTS
+ CRT_FILE
+ The path to a certificate or certificate signing request (CSR) to inspect.`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "format",
+ Value: "text",
+ Usage: `The output format for printing the introspection details.
+
+ FORMAT must be one of:
+ text
+ Print output in unstructured text suitable for a human to read
+ json
+ Print output in JSON format`,
+ },
+ cli.StringFlag{
+ Name: "roots",
+ Usage: `Root certificate(s) to use in request to obtain remote server certificate.
+
+ ROOTS is a string containing a (FILE | LIST of FILES | DIRECTORY) defined in one of the following ways:
+ FILE
+ Relative or full path to a file. All certificates in the file will be used for path validation.
+ LIST of Files
+ Comma-separated list of relative or full file paths. Every PEM encoded certificate
+ from each file will be used for path validation.
+ DIRECTORY
+ Relative or full path to a directory. Every PEM encoded certificate from each file
+ in the directory will be used for path validation.`,
+ },
+ },
+ }
+}
+
+func inspectAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 1); err != nil {
+ return err
+ }
+
+ var (
+ crtFile = ctx.Args().Get(0)
+ block *pem.Block
+ )
+ if strings.HasPrefix(crtFile, "https://") {
+ var (
+ err error
+ rootCAs *x509.CertPool
+ )
+ if roots := ctx.String("roots"); roots != "" {
+ rootCAs, err = stepx509.ReadCertPool(roots)
+ if err != nil {
+ errors.Wrapf(err, "failure to load root certificate pool from input path '%s'", roots)
+ }
+ }
+ addr := strings.TrimPrefix(crtFile, "https://")
+ if !strings.Contains(addr, ":") {
+ addr += ":443"
+ }
+ conn, err := tls.Dial("tcp", addr, &tls.Config{RootCAs: rootCAs})
+ if err != nil {
+ return errors.Wrapf(err, "failed to connect")
+ }
+ conn.Close()
+ crt := conn.ConnectionState().PeerCertificates[0]
+ block = &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: crt.Raw,
+ }
+ } else {
+ crtBytes, err := ioutil.ReadFile(crtFile)
+ if err != nil {
+ return errs.FileError(err, crtFile)
+ }
+ block, _ = pem.Decode(crtBytes)
+ if block == nil {
+ return errors.Errorf("could not parse certificate file '%s'", crtFile)
+ }
+ }
+
+ format := ctx.String("format")
+ switch block.Type {
+ case "CERTIFICATE":
+ switch format {
+ case "text":
+ crt, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ result, err := certinfo.CertificateText(crt)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ fmt.Print(result)
+ case "json":
+ zcrt, err := zx509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ b, err := json.MarshalIndent(struct {
+ *zx509.Certificate
+ }{zcrt}, "", " ")
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ os.Stdout.Write(b)
+ default:
+ return errors.Errorf("invalid value for '--format'. '--format' must be "+
+ "one of 'text'(default) or 'json', but got '%s'", format)
+ }
+ case "CSR":
+ switch format {
+ case "text":
+ return errors.Errorf("Not implemented. Come back later :)")
+ case "json":
+ zcsr, err := zx509.ParseCertificateRequest(block.Bytes)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ b, err := json.MarshalIndent(struct {
+ *zx509.CertificateRequest
+ }{zcsr}, "", " ")
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ os.Stdout.Write(b)
+ default:
+ return errors.Errorf("invalid value for '--format'. '--format' must be "+
+ "one of 'text'(default) or 'json', but got '%s'", format)
+ }
+ default:
+ return errors.Errorf("Invalid PEM type in '%s'. Expected ['CERTIFICATE'|'CSR'] but got '%s')", crtFile, block.Type)
+ }
+
+ return nil
+}
diff --git a/command/certificates/lint.go b/command/certificates/lint.go
new file mode 100644
index 00000000..f30665cb
--- /dev/null
+++ b/command/certificates/lint.go
@@ -0,0 +1,109 @@
+package certificates
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "io/ioutil"
+ "os"
+ "strings"
+
+ "github.com/pkg/errors"
+ stepx509 "github.com/smallstep/cli/crypto/certificates/x509"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+ zx509 "github.com/zmap/zcrypto/x509"
+ "github.com/zmap/zlint"
+)
+
+func lintCommand() cli.Command {
+ return cli.Command{
+ Name: "lint",
+ Action: cli.ActionFunc(lintAction),
+ Usage: `lint certificate details.`,
+ UsageText: `step certificates lint CRT_FILE`,
+ Description: `UPDATE ME
+
+ POSITIONAL ARGUMENTS
+ CRT_FILE
+ The path to a certificate or certificate signing request (CSR) to inspect.`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "roots",
+ Usage: `Root certificate(s) to use in request to obtain remote server certificate.
+
+ ROOTS is a string containing a (FILE | LIST of FILES | DIRECTORY) defined in one of the following ways:
+ FILE
+ Relative or full path to a file. All certificates in the file will be used for path validation.
+ LIST of Files
+ Comma-separated list of relative or full file paths. Every PEM encoded certificate
+ from each file will be used for path validation.
+ DIRECTORY
+ Relative or full path to a directory. Every PEM encoded certificate from each file
+ in the directory will be used for path validation.`,
+ },
+ },
+ }
+}
+
+func lintAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 1); err != nil {
+ return err
+ }
+
+ var (
+ crtFile = ctx.Args().Get(0)
+ block *pem.Block
+ )
+ if strings.HasPrefix(crtFile, "https://") {
+ var (
+ err error
+ rootCAs *x509.CertPool
+ )
+ if roots := ctx.String("roots"); roots != "" {
+ rootCAs, err = stepx509.ReadCertPool(roots)
+ if err != nil {
+ errors.Wrapf(err, "failure to load root certificate pool from input path '%s'", roots)
+ }
+ }
+ addr := strings.TrimPrefix(crtFile, "https://")
+ if !strings.Contains(addr, ":") {
+ addr += ":443"
+ }
+ conn, err := tls.Dial("tcp", addr, &tls.Config{RootCAs: rootCAs})
+ if err != nil {
+ return errors.Wrapf(err, "failed to connect")
+ }
+ conn.Close()
+ crt := conn.ConnectionState().PeerCertificates[0]
+ block = &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: crt.Raw,
+ }
+ } else {
+ crtBytes, err := ioutil.ReadFile(crtFile)
+ if err != nil {
+ return errs.FileError(err, crtFile)
+ }
+ block, _ = pem.Decode(crtBytes)
+ if block == nil {
+ return errors.Errorf("could not parse certificate file '%s'", crtFile)
+ }
+ }
+
+ zcrt, err := zx509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ zlintResult := zlint.LintCertificate(zcrt)
+ b, err := json.MarshalIndent(struct {
+ *zlint.ResultSet
+ }{zlintResult}, "", " ")
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ os.Stdout.Write(b)
+
+ return nil
+}
diff --git a/command/certificates/sign.go b/command/certificates/sign.go
new file mode 100644
index 00000000..d020342e
--- /dev/null
+++ b/command/certificates/sign.go
@@ -0,0 +1,101 @@
+package certificates
+
+import (
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+
+ "github.com/pkg/errors"
+ stepx509 "github.com/smallstep/cli/crypto/certificates/x509"
+ "github.com/smallstep/cli/crypto/keys"
+ "github.com/smallstep/cli/errs"
+ "github.com/smallstep/cli/utils/reader"
+ "github.com/urfave/cli"
+)
+
+func signCommand() cli.Command {
+ return cli.Command{
+ Name: "sign",
+ Action: cli.ActionFunc(signAction),
+ Usage: "sign a certificate signing request (CSR).",
+ UsageText: `step certificates sign CSR_FILE CRT_FILE KEY_FILE [--token=TOKEN]`,
+ Description: `The 'step certificates sign' generates a signed certificate from a
+ certificate signing requests (CSR).
+
+ POSITIONAL ARGUMENTS
+ CSR_FILE
+ The path to a certificate signing request (CSR) to be signed.
+ CRT_FILE
+ The path to an issuing certificate.
+ KEY_FILE
+ The path to a private key for signing the CSR.`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "token",
+ Usage: `A provisioning token or bootstrap token for secure introduction and
+ mutual authentication with an unknown CA.`,
+ },
+ },
+ }
+}
+
+func signAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 3); err != nil {
+ return err
+ }
+
+ csrFile := ctx.Args().Get(0)
+ crtFile := ctx.Args().Get(1)
+ keyFile := ctx.Args().Get(2)
+
+ // Load the CSR into an x509 Certificate Template.
+ csrBytes, err := ioutil.ReadFile(csrFile)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ csr, err := stepx509.LoadCSRFromBytes(csrBytes)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ // Load the Issuer Certificate.
+ issuerCrt, _, err := stepx509.LoadCertificate(crtFile)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ // Load the Issuer Private Key.
+ keyBytes, err := ioutil.ReadFile(keyFile)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ key, err := keys.LoadPrivateKey(keyBytes, func() (string, error) {
+ var pass string
+ if err := reader.ReadPasswordSubtle(
+ fmt.Sprintf("Password with which to decrypt private key %s: ", keyFile),
+ &pass, "Password", reader.RetryOnEmpty); err != nil {
+ return "", err
+ }
+ return pass, nil
+ })
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ leafProfile, err := stepx509.NewLeafProfileWithCSR(csr, issuerCrt, key)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ crtBytes, err := leafProfile.CreateCertificate()
+ if err != nil {
+ return errors.Wrapf(err, "failure creating new leaf certificate from input csr")
+ }
+ block := &pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: crtBytes,
+ }
+ fmt.Printf("%s", string(pem.EncodeToMemory(block)))
+
+ //tok := ctx.String("token")
+ return nil
+}
diff --git a/command/certificates/verify.go b/command/certificates/verify.go
new file mode 100644
index 00000000..279269a0
--- /dev/null
+++ b/command/certificates/verify.go
@@ -0,0 +1,116 @@
+package certificates
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "io/ioutil"
+
+ "github.com/pkg/errors"
+ stepx509 "github.com/smallstep/cli/crypto/certificates/x509"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+func verifyCommand() cli.Command {
+ return cli.Command{
+ Name: "verify",
+ Action: cli.ActionFunc(verifyAction),
+ Usage: `verify a certificate.`,
+ UsageText: `step certificates verify CRT_FILE [--host=HOST]`,
+ Description: `The 'step certificates verify' command executes the certificate path validation
+algorithm for x.509 certificates defined in RFC 5280. If the certificate is valid
+this command will return '0'. If validation fails, or if an error occurs, this
+command will produce a non-zero return value.
+
+ POSITIONAL ARGUMENTS
+ CRT_FILE
+ The path to a certificate to validate.`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "host",
+ Usage: `Check whether the certificate is for the specified host.`,
+ },
+ cli.StringFlag{
+ Name: "roots",
+ Usage: `Root certificates to use in the path validation algorithm.
+
+ ROOTS is a string containing a (FILE | LIST of FILES | DIRECTORY) defined in one of the following ways:
+ FILE
+ Relative or full path to a file. All certificates in the file will be used for path validation.
+ LIST of Files
+ Comma-separated list of relative or full file paths. Every PEM encoded certificate
+ from each file will be used for path validation.
+ DIRECTORY
+ Relative or full path to a directory. Every PEM encoded certificate from each file
+ in the directory will be used for path validation.`,
+ },
+ },
+ }
+}
+
+func verifyAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 1); err != nil {
+ return err
+ }
+
+ crtFile := ctx.Args().Get(0)
+ crtBytes, err := ioutil.ReadFile(crtFile)
+ if err != nil {
+ return errs.FileError(err, crtFile)
+ }
+
+ var (
+ crt *x509.Certificate
+ ipems []byte
+ intermediatePool = x509.NewCertPool()
+ block *pem.Block
+ )
+ // The first certificate PEM in the file is our leaf Certificate.
+ // Any certificate after the first is added to the list of Intermediate
+ // certificates used for path validation.
+ for len(crtBytes) > 0 {
+ block, crtBytes = pem.Decode(crtBytes)
+ if block == nil {
+ break
+ }
+ if block.Type != "CERTIFICATE" {
+ continue
+ }
+ if crt == nil {
+ crt, err = x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ } else {
+ ipems = append(ipems, pem.EncodeToMemory(block)...)
+ }
+ }
+ if len(ipems) > 0 && !intermediatePool.AppendCertsFromPEM(ipems) {
+ return errors.Errorf("failure creating intermediate list from certificate '%s'", crtFile)
+ }
+
+ var (
+ host = ctx.String("host")
+ roots = ctx.String("roots")
+ rootPool *x509.CertPool
+ )
+
+ if roots != "" {
+ rootPool, err = stepx509.ReadCertPool(roots)
+ if err != nil {
+ errors.Wrapf(err, "failure to load root certificate pool from input path '%s'", roots)
+ }
+ }
+
+ opts := x509.VerifyOptions{
+ DNSName: host,
+ Roots: rootPool,
+ Intermediates: intermediatePool,
+ }
+
+ if _, err := crt.Verify(opts); err != nil {
+ return errors.Wrapf(err, "failed to verify certificate")
+ }
+
+ return nil
+}
diff --git a/command/command.go b/command/command.go
new file mode 100644
index 00000000..2d586114
--- /dev/null
+++ b/command/command.go
@@ -0,0 +1,161 @@
+package command
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/urfave/cli"
+)
+
+var cmds []cli.Command
+
+var helpCommand cli.Command
+
+func init() {
+ helpCommand = createHelpCommand()
+ cmds = []cli.Command{
+ helpCommand,
+ }
+}
+
+// Register adds the given command to the global list of commands
+func Register(c cli.Command) {
+ cmds = append(cmds, c)
+}
+
+// Retrieve returns all commands
+func Retrieve() []cli.Command {
+ return cmds
+}
+
+// helpCommand overwrites default urfvafe/cli help command to support one or
+// multiple subcommands like:
+// step help
+// step help crypto
+// step help crypto jwt
+// step help crypto jwt sign
+// ...
+func createHelpCommand() cli.Command {
+ return cli.Command{
+ Name: "help",
+ Aliases: []string{"h"},
+ Usage: "displays help for the specified command or command group",
+ ArgsUsage: "[command]",
+ Action: cli.ActionFunc(func(ctx *cli.Context) error {
+ args := ctx.Args()
+ if args.Present() {
+ last := len(args) - 1
+ lastName := args[last]
+ subcmd := ctx.App.Commands
+ parent := createParentCommand(ctx)
+
+ for _, name := range args[:last] {
+ for _, cmd := range subcmd {
+ if cmd.HasName(name) {
+ parent = cmd
+ subcmd = cmd.Subcommands
+ break
+ }
+ }
+ }
+
+ for _, cmd := range subcmd {
+ if cmd.HasName(lastName) {
+ cmd.HelpName = fmt.Sprintf("%s %s", ctx.App.HelpName, strings.Join(args, " "))
+ parent.HelpName = fmt.Sprintf("%s %s", ctx.App.HelpName, strings.Join(args[:last], " "))
+
+ ctx.Command = cmd
+ if len(cmd.Subcommands) == 0 {
+ ctx.App = createCliApp(ctx, parent)
+ return cli.ShowCommandHelp(ctx, lastName)
+ }
+
+ ctx.App = createCliApp(ctx, cmd)
+ return cli.ShowCommandHelp(ctx, "")
+ }
+ }
+
+ return cli.NewExitError(fmt.Sprintf("No help topic for '%s %s'", ctx.App.Name, strings.Join(args, " ")), 3)
+ }
+
+ cli.ShowAppHelp(ctx)
+ return nil
+ }),
+ }
+}
+
+// createParentCommand returns a command representation of the app.
+func createParentCommand(ctx *cli.Context) cli.Command {
+ return cli.Command{
+ Name: ctx.App.Name,
+ HelpName: ctx.App.HelpName,
+ Usage: ctx.App.Usage,
+ UsageText: ctx.App.UsageText,
+ ArgsUsage: ctx.App.ArgsUsage,
+ Description: ctx.App.Description,
+ Subcommands: ctx.App.Commands,
+ Flags: ctx.App.Flags,
+ }
+}
+
+// createCliApp is re-implementation of urfave/cli method (in command.go):
+//
+// func (c Command) startApp(ctx *Context) error
+//
+// It lets us show the subcommands when help is executed like:
+//
+// step help foo
+// step help foo bar
+// ...
+func createCliApp(ctx *cli.Context, cmd cli.Command) *cli.App {
+ app := cli.NewApp()
+ app.Metadata = ctx.App.Metadata
+
+ // set the name and usage
+ app.Name = cmd.HelpName
+ app.HelpName = cmd.HelpName
+
+ app.Usage = cmd.Usage
+ app.UsageText = cmd.UsageText
+ app.Description = cmd.Description
+ app.ArgsUsage = cmd.ArgsUsage
+
+ // set CommandNotFound
+ app.CommandNotFound = ctx.App.CommandNotFound
+ app.CustomAppHelpTemplate = cmd.CustomHelpTemplate
+
+ // set the flags and commands
+ app.Commands = cmd.Subcommands
+ app.Flags = cmd.Flags
+
+ app.Version = ctx.App.Version
+ app.Compiled = ctx.App.Compiled
+ app.Author = ctx.App.Author
+ app.Email = ctx.App.Email
+ app.Writer = ctx.App.Writer
+ app.ErrWriter = ctx.App.ErrWriter
+
+ // Do not show help or version on subcommands
+ app.HideHelp = true
+ app.HideVersion = true
+
+ // bash completion
+ app.EnableBashCompletion = ctx.App.EnableBashCompletion
+ if cmd.BashComplete != nil {
+ app.BashComplete = cmd.BashComplete
+ }
+
+ // set the actions
+ app.Before = cmd.Before
+ app.After = cmd.After
+
+ if cmd.Action != nil {
+ app.Action = cmd.Action
+ } else {
+ app.Action = helpCommand.Action
+ }
+ app.OnUsageError = cmd.OnUsageError
+
+ app.Setup()
+ return app
+}
diff --git a/command/crypto/crypto.go b/command/crypto/crypto.go
new file mode 100644
index 00000000..791223f2
--- /dev/null
+++ b/command/crypto/crypto.go
@@ -0,0 +1,166 @@
+package crypto
+
+import (
+ "github.com/smallstep/cli/command"
+ "github.com/smallstep/cli/command/crypto/hash"
+ "github.com/smallstep/cli/command/crypto/jwe"
+ "github.com/smallstep/cli/command/crypto/jwk"
+ "github.com/smallstep/cli/command/crypto/jwt"
+ "github.com/smallstep/cli/command/crypto/kdf"
+ "github.com/smallstep/cli/command/crypto/nacl"
+ "github.com/smallstep/cli/command/crypto/otp"
+ "github.com/urfave/cli"
+)
+
+func init() {
+ cmd := cli.Command{
+ Name: "crypto",
+ Usage: "useful cryptographic plumbing",
+ Description: `The **step crypto** command group provides a selection of useful cryptographic
+primitives that balances completeness and safety (cryptographic strength, ease
+of use, and misuse prevention). Subcommands include flags and arguments to
+select algorithms and fine-tune behaviors, but we've selected safe defaults for
+you wherever possible.
+
+Insecure or subtle cryptographic primitives and options are gated with flags to
+prevent accidental misuse. Such primitives and options will not work unless you
+pass the corresponding flags to indicate that you understand the risks
+(**--insecure** and **--subtle**, respectively). Our rationale for these
+decisions is usually documented in the **SECURITY CONSIDERATIONS** section of
+the help for each subcommand.
+
+## SECURITY CONSIDERATIONS
+
+The strength of cryptographic mechanisms depends on the strength of all links
+in the security chain. This includes the quality and strength of algorithms,
+random number generation, distribution mechanisms, etc. It also includes
+protection against hostile observation and tampering as well as the security of
+the overall system including the operating system and personnel, etc. Where
+possible, we've selected secure defaults. Whenever a subtle or insecure
+cryptographic operation is attempted affirmative confirmation via prompt or
+command line flag is required, indicating that you understand and accept the
+risks. That said, many of these factors are beyond the scope of this tool.
+
+**Key Length**
+
+: This tool enforces a minimum key size of **256 bits for symmetric keys**, which is
+ generally considered quantum-safe and accepted as sufficient for the
+ foreseeable future.
+
+: This tool enforces the NIST recommended minimum key size of **2048 bits for RSA**
+ keys, which RSA claims is equivalent in strength to 112 bit symmetric keys and
+ is likely to be sufficient until 2030. An RSA key length of at least 3072 bits,
+ which RSA claims is equivalent to 128 bit symmetric keys, should be used if
+ security is required beyond 2030.
+
+: Elliptic curve cryptography is generally believed to be secure with shorter
+ keys than RSA requires. NIST guidelines state that ECC keys should be twice the
+ length of the equivalent strength symmetric key. The rough equivalencies for
+ the elliptic curves supported by this tool are:
+
+: | key type | curve | RSA equivalent | symmetric key equivalent |
+ |----------|---------|----------------|--------------------------|
+ | EC | P-256 | ~3000 bits | ~128 bits |
+ | EC | P-384 | ~4096 bits | ~192 bits |
+ | EC | P-521 | ~15000 bits | ~256 bits |
+ | OKP | Ed25519 | ~3000 bits | ~140 bits |
+
+: Elliptic curve cryptography has the additional advantages of much smaller key
+ sizes for equivalent security levels, and much faster cryptographic operations
+ compared to RSA. The strength of these keys is generally considered sufficient
+ for the predictable and foreseeable future.
+
+: Note that for cryptographic protocols that have perfect forward secrecry and
+ only use asymmetric keys for symmetric key negotiation your system will remain
+ secure against future threats as long as the keys are large enough that they
+ cannot be cracked today. In other words, sizing your keys to protect against
+ potential future threats is largely irrelevant.
+
+**Key Use**
+
+: In general you should not use an asymmetric keypair for both signing and
+ encryption. Using a single key for both operations can introduce attack vectors
+ that would not otherwise exist. Attacks aside, signing keys and encryption
+ keys generally have different life cycles. Signing keys are generally destroyed
+ once they're no longer useful for singing new data. Encryption keys, on the
+ other hand, must be retained as long as data exists that was encrypted for the
+ key. So using a signing key for encryption may force you to retain a signing
+ key for longer than it's needed, leaving it susceptible to misuse.
+
+: Raw public or private keys don't have any associated data, therefore this
+ tool cannot enforce key use on raw keys and this responsibility is up to
+ you. For keys in an "envelope" the envelope typically includes key use
+ restrictions (e.g., the "use" parameter in JWKs and the "Key Usage"
+ attribute of X.509 certificates). This tool generally requires key use to be
+ specified when creating an enveloped key, and enforces key use restrictions
+ when an enveloped key is being used.
+
+**Safe Curves**
+
+: There is some concern that certain standard elliptic curves are very hard to
+ implement correctly. These concerns are not purely theoretical. Implementation
+ issues have been uncovered and real attacks have been demonstrated.
+
+: While we take these concerns seriously, these curves are widely used in
+ practice, largely because they are perceived to be stronger than RSA and have
+ been implemented in more places than the "safe curves". Therefore, **we've
+ opted not to gate non-safe curves**. We've further elected to make **P-256**
+ the default curve for EC keys.
+
+: Still, it is important to be aware of the security risks assocated with their
+ risk. You should consider using "safe curves" if possible. We may change our
+ mind as support for safe curves improves.
+
+: Safe and non-safe curves implemented by this tool are:
+
+: | key type | curve | safe |
+ |----------|---------|------|
+ | EC | P-256 | NO |
+ | EC | P-384 | NO |
+ | EC | P-521 | NO |
+ | OKP | Ed25519 | YES |
+
+: For more information see https://safecurves.cr.yp.to/
+
+**Quantum Safety**
+
+: Quantum-safe cryptography refers to keys and algorithms that are secure against
+ an attack by a quantum computer. As of 2018 most public key algorithms are not
+ quantum safe. In particular, **none of the public key algorithms implemented by
+ this tool are quantum safe**. However, no quantum computer exists that is
+ powerful enough to break current algorithms. Using cryptographic protocols with
+ forward secrecy is the best way to protect against future quantum attacks.
+
+**Forward Secrecy**
+
+: A cryptosystem or protocol has forward secrecy (or perfect forward secrecy) if,
+ for each session or interaction, a random key is generated such that an
+ attacker with access to all private keys would still not know the generated
+ key. This can be accomplished using Diffie-Hellman key exchange, for instance.
+
+: Forward secrecy can protect against an attacker who stores intercepted
+ communication and waits for your private key to be compromised, at which point
+ they could decrypt the stored communication. It also offers good protection
+ against quantum attacks since symmetric key cryptosystems like AES are already
+ considered quantum resistant with sufficiently large key sizes. The current
+ best quantum attack against symmetric key systems requires work proportional to
+ the square of the size of the key space. In other words, a symmetric key is
+ half as strong against a quantum attack vs. a conventional attack, so your key
+ needs to be twice as long for equivalent quantum-safe security. A 256 bit
+ symmetric key in the context of a quantum attack is equivalent in strength to a
+ 128 bit key in the context of a conventioanl attack.
+`,
+ Subcommands: cli.Commands{
+ jwk.Command(),
+ jwt.Command(),
+ jwe.Command(),
+ hash.Command(),
+ kdf.Command(),
+ nacl.Command(),
+ createKeyPairCommand(),
+ otp.Command(),
+ },
+ }
+
+ command.Register(cmd)
+}
diff --git a/command/crypto/hash/hash.go b/command/crypto/hash/hash.go
new file mode 100644
index 00000000..ebc549a0
--- /dev/null
+++ b/command/crypto/hash/hash.go
@@ -0,0 +1,276 @@
+package hash
+
+import (
+ "crypto/md5"
+ "crypto/sha1"
+ "crypto/sha256"
+ "crypto/sha512"
+ "crypto/subtle"
+ "encoding/binary"
+ "encoding/hex"
+ "fmt"
+ "hash"
+ "io"
+ "io/ioutil"
+ "os"
+ "path"
+ "strings"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+type hashConstructor func() hash.Hash
+
+// Command returns the jwk subcommand.
+func Command() cli.Command {
+ return cli.Command{
+ Name: "hash",
+ Usage: "generates and checks hashes of files and directories",
+ UsageText: "step crypto hash [SUBCOMMAND_FLAGS]",
+ Subcommands: cli.Commands{
+ digestCommand(),
+ compareCommand(),
+ },
+ }
+}
+
+func digestCommand() cli.Command {
+ return cli.Command{
+ Name: "digest",
+ Action: cli.ActionFunc(digestAction),
+ Usage: "generate a hash digest of a file or directory",
+ UsageText: "step crypto hash digest FILE_OR_DIRECTORY [--alg ALGORITHM]",
+ Description: `The 'step crypto hash digest' command generates a hash digest for a given
+file or directory. For a file, the output is the same as tools like 'shasum'.
+For directories, the tool computes a hash tree and outputs a single hash
+digest.
+
+POSITIONAL ARGUMENTS
+
+ FILE_OR_DIRECTORY
+ The path to a file or directory to hash.`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "alg",
+ Value: "sha256",
+ Usage: `The hash algorithm to use.
+
+ ALGORITHM must be one of:
+ sha1
+ sha224
+ sha256 (default)
+ sha384
+ sha512
+ sha512-224
+ sha512-256
+ sha
+ md5 (requires '--insecure')`,
+ },
+ cli.BoolFlag{
+ Name: "insecure",
+ Hidden: true,
+ },
+ },
+ }
+}
+
+func compareCommand() cli.Command {
+ return cli.Command{
+ Name: "compare",
+ Action: cli.ActionFunc(compareAction),
+ Usage: "verify the hash digest for a file or directory matches an expected value",
+ UsageText: "step crypto hash compare HASH FILE_OR_DIRECTORY [--alg ALGORITHM]",
+ Description: `The 'step crypto hash compare' command verifies that the expected hash value
+matches the computed hash value for a file or directory.
+
+POSITIONAL ARGUMENTS
+
+ HASH
+ The expected hash digest
+
+ FILE_OR_DIRECTORY
+ The path to a file or directory to hash.`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "alg",
+ Value: "sha256",
+ Usage: `The hash algorithm to use.
+
+ ALGORITHM must be one of:
+ sha1
+ sha224
+ sha256 (default)
+ sha384
+ sha512
+ sha512-224
+ sha512-256
+ sha
+ md5 (requires '--insecure')`,
+ },
+ cli.BoolFlag{
+ Name: "insecure",
+ Hidden: true,
+ },
+ },
+ }
+}
+
+func digestAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 1); err != nil {
+ return err
+ }
+
+ hc, err := getHash(ctx, ctx.String("alg"), ctx.Bool("insecure"))
+ if err != nil {
+ return err
+ }
+
+ filename := ctx.Args().Get(0)
+ st, err := os.Stat(filename)
+ if err != nil {
+ return errs.FileError(err, filename)
+ }
+
+ var sum []byte
+ if st.IsDir() {
+ sum, err = hashDir(hc, filename)
+ } else {
+ sum, err = hashFile(hc(), filename)
+ }
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("%x %s\n", sum, filename)
+
+ return err
+}
+
+func compareAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 2); err != nil {
+ return err
+ }
+
+ hc, err := getHash(ctx, ctx.String("alg"), ctx.Bool("insecure"))
+ if err != nil {
+ return err
+ }
+
+ hashStr := ctx.Args().Get(0)
+ hashBytes, err := hex.DecodeString(hashStr)
+ if err != nil {
+ return errs.Wrap(err, "error decoding %s", hashStr)
+ }
+
+ filename := ctx.Args().Get(1)
+ st, err := os.Stat(filename)
+ if err != nil {
+ return errs.FileError(err, filename)
+ }
+
+ var sum []byte
+ if st.IsDir() {
+ sum, err = hashDir(hc, filename)
+ } else {
+ sum, err = hashFile(hc(), filename)
+ }
+ if err != nil {
+ return err
+ }
+
+ if subtle.ConstantTimeCompare(sum, hashBytes) == 1 {
+ fmt.Println("ok")
+ return nil
+ }
+
+ return errors.New("fail")
+}
+
+// getHash returns a new hash constructor for the given algorithm. MD5
+// algorithm can only be used if the insecure flag is passed.
+func getHash(ctx *cli.Context, alg string, insecure bool) (hashConstructor, error) {
+ switch strings.ToLower(alg) {
+ case "sha", "sha1":
+ return func() hash.Hash { return sha1.New() }, nil
+ case "sha224":
+ return func() hash.Hash { return sha256.New224() }, nil
+ case "sha256":
+ return func() hash.Hash { return sha256.New() }, nil
+ case "sha384":
+ return func() hash.Hash { return sha512.New384() }, nil
+ case "sha512":
+ return func() hash.Hash { return sha512.New() }, nil
+ case "sha512-224":
+ return func() hash.Hash { return sha512.New512_224() }, nil
+ case "sha512-256":
+ return func() hash.Hash { return sha512.New512_256() }, nil
+ case "md5":
+ if insecure {
+ return func() hash.Hash { return md5.New() }, nil
+ }
+ return nil, errs.FlagValueInsecure(ctx, "alg", alg)
+ default:
+ return nil, errs.InvalidFlagValue(ctx, "alg", alg, "")
+ }
+}
+
+// hashFile returns the hash of the given file using the given hash function.
+func hashFile(h hash.Hash, filename string) ([]byte, error) {
+ f, err := os.Open(filename)
+ if err != nil {
+ return nil, errs.FileError(err, filename)
+ }
+
+ if _, err := io.Copy(h, f); err != nil {
+ return nil, errs.FileError(err, filename)
+ }
+
+ return h.Sum(nil), nil
+}
+
+// hashDir creates a hash of a directory adding the following data to the
+// hash:
+// 1. Add directory mode bits to the hash
+// 2. For each file/directory in directory:
+// 2.1 If file: add file mode bits and sum
+// 2.2 If directory: do hashDir and add sum
+// 3. return sum
+func hashDir(hc hashConstructor, dirname string) ([]byte, error) {
+ // ReadDir returns the entries sorted by filename
+ files, err := ioutil.ReadDir(dirname)
+ if err != nil {
+ return nil, errs.FileError(err, dirname)
+ }
+ st, err := os.Stat(dirname)
+ if err != nil {
+ return nil, errs.FileError(err, dirname)
+ }
+
+ var sum []byte
+ mode := make([]byte, 4)
+
+ // calculate sum of contents and mode
+ h := hc()
+ binary.LittleEndian.PutUint32(mode, uint32(st.Mode()))
+ h.Write(mode)
+ for _, fi := range files {
+ if fi.IsDir() {
+ sum, err = hashDir(hc, path.Join(dirname, fi.Name()))
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ binary.LittleEndian.PutUint32(mode, uint32(fi.Mode()))
+ h.Write(mode)
+ sum, err = hashFile(hc(), path.Join(dirname, fi.Name()))
+ if err != nil {
+ return nil, err
+ }
+ }
+ h.Write(sum)
+ }
+
+ return h.Sum(nil), nil
+}
diff --git a/command/crypto/internal/crypto/crypto.go b/command/crypto/internal/crypto/crypto.go
new file mode 100644
index 00000000..778cc6ea
--- /dev/null
+++ b/command/crypto/internal/crypto/crypto.go
@@ -0,0 +1,210 @@
+package crypto
+
+import (
+ "bytes"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/asn1"
+ "encoding/pem"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "golang.org/x/crypto/ed25519"
+)
+
+// GetRandomSalt generates a new salt of the given size.
+func GetRandomSalt(size int) ([]byte, error) {
+ salt := make([]byte, size)
+ _, err := io.ReadFull(rand.Reader, salt)
+ if err != nil {
+ return nil, errors.Wrap(err, "error generating salt")
+ }
+ return salt, nil
+}
+
+// ReadCertificate returns a *x509.Certificate from the given filename. It
+// supports certificates formats PEM and DER.
+func ReadCertificate(filename string) (*x509.Certificate, error) {
+ b, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error reading %s", filename)
+ }
+
+ // PEM format
+ if bytes.HasPrefix(b, []byte("-----BEGIN ")) {
+ crt, err := ReadPEM(filename)
+ if err != nil {
+ return nil, err
+ }
+ switch crt := crt.(type) {
+ case *x509.Certificate:
+ return crt, nil
+ default:
+ return nil, errors.Errorf("error decoding PEM: file '%s' does not contain a certificate", filename)
+ }
+ }
+
+ // DER format (binary)
+ crt, err := x509.ParseCertificate(b)
+ return crt, errors.Wrapf(err, "error parsing %s", filename)
+}
+
+// ParsePEM returns the key or certificate PEM-encoded in the given bytes.
+func ParsePEM(b []byte, filename string) (interface{}, error) {
+ block, rest := pem.Decode(b)
+ switch {
+ case block == nil:
+ return nil, errors.Errorf("error decoding PEM: file '%s' is not a valid PEM encoded key", filename)
+ case len(rest) > 0:
+ return nil, errors.Errorf("error decoding PEM: file '%s' contains more than one key", filename)
+ }
+
+ // PEM is encrypted: ask for password
+ if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
+ pass, err := utils.ReadPassword(fmt.Sprintf("Please enter the password to decrypt %s: ", filename))
+ if err != nil {
+ return nil, err
+ }
+
+ block.Bytes, err = x509.DecryptPEMBlock(block, pass)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error decrypting %s", filename)
+ }
+ }
+
+ switch block.Type {
+ case "PUBLIC KEY":
+ pub, err := ParsePKIXPublicKey(block.Bytes)
+ return pub, errors.Wrapf(err, "error parsing %s", filename)
+ case "RSA PRIVATE KEY":
+ priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+ return priv, errors.Wrapf(err, "error parsing %s", filename)
+ case "EC PRIVATE KEY":
+ priv, err := x509.ParseECPrivateKey(block.Bytes)
+ return priv, errors.Wrapf(err, "error parsing %s", filename)
+ case "PRIVATE KEY", "OPENSSH PRIVATE KEY":
+ priv, err := ParsePKCS8PrivateKey(block.Bytes)
+ return priv, errors.Wrapf(err, "error parsing %s", filename)
+ case "CERTIFICATE":
+ crt, err := x509.ParseCertificate(b)
+ return crt, errors.Wrapf(err, "error parsing %s", filename)
+ default:
+ return nil, errors.Errorf("error decoding PEM: file '%s' contains an unexpected header '%s'", filename, block.Type)
+ }
+}
+
+// ReadPEM returns the key or certificated encoded in the given PEM encoded
+// file. If the file is encrypted it will ask for a password and it will try
+// to decrypt it.
+//
+// Supported keys algorithms are RSA and EC. Supported standards for private
+// keys are PKCS#1, PKCS#8, RFC5915 for EC, and base64-encoded DER for
+// certificates and public keys.
+func ReadPEM(filename string) (interface{}, error) {
+ b, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error reading %s", filename)
+ }
+
+ return ParsePEM(b, filename)
+}
+
+// pkcs8 reflects an ASN.1, PKCS#8 PrivateKey. See
+// ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-8/pkcs-8v1_2.asn
+// and RFC 5208.
+type pkcs8 struct {
+ Version int
+ Algo pkix.AlgorithmIdentifier
+ PrivateKey []byte
+ // optional attributes omitted.
+}
+
+type publicKeyInfo struct {
+ Raw asn1.RawContent
+ Algo pkix.AlgorithmIdentifier
+ PublicKey asn1.BitString
+}
+
+// Algorithm Identifiers for Ed25519, Ed448, X25519 and X448 for use in the
+// Internet X.509 Public Key Infrastructure
+// https://tools.ietf.org/html/draft-ietf-curdle-pkix-10
+var (
+ // oidX25519 = asn1.ObjectIdentifier{1, 3, 101, 110}
+ oidEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112}
+)
+
+// ParsePKCS8PrivateKey parses an unencrypted, PKCS#8 private key. See RFC
+// 5208.
+//
+// Supported key types include RSA, ECDSA, and Ed25519. Unknown key types
+// result in an error.
+//
+// On success, key will be of type *rsa.PrivateKey, *ecdsa.PublicKey, or
+// ed25519.PrivateKey.
+func ParsePKCS8PrivateKey(der []byte) (key interface{}, err error) {
+ var privKey pkcs8
+ if _, err := asn1.Unmarshal(der, &privKey); err != nil {
+ return nil, err
+ }
+
+ switch {
+ case privKey.Algo.Algorithm.Equal(oidEd25519):
+ seed := make([]byte, ed25519.SeedSize)
+ copy(seed, privKey.PrivateKey[2:])
+ key = ed25519.NewKeyFromSeed(seed)
+ kk := key.(ed25519.PrivateKey)
+ fmt.Fprintf(os.Stderr, "% x\n% x\n", kk, kk.Public())
+ return key, nil
+ // Prove of concept for key agreement algorithm X25519.
+ // A real implementation would use their own types.
+ //
+ // case privKey.Algo.Algorithm.Equal(oidX25519):
+ // k := make([]byte, ed25519.PrivateKeySize)
+ // var pub, priv [32]byte
+ // copy(priv[:], privKey.PrivateKey[2:])
+ // curve25519.ScalarBaseMult(&pub, &priv)
+ // copy(k, priv[:])
+ // copy(k[32:], pub[:])
+ // key = ed25519.PrivateKey(k)
+ // return key, nil
+ default:
+ return x509.ParsePKCS8PrivateKey(der)
+ }
+}
+
+// ParsePKIXPublicKey parses a DER encoded public key. These values are
+// typically found in PEM blocks with "BEGIN PUBLIC KEY".
+//
+// Supported key types include RSA, DSA, ECDSA, and Ed25519. Unknown key types
+// result in an error.
+//
+// On success, pub will be of type *rsa.PublicKey, *dsa.PublicKey,
+// *ecdsa.PublicKey, or ed25519.PublicKey.
+func ParsePKIXPublicKey(derBytes []byte) (pub interface{}, err error) {
+ var pki publicKeyInfo
+ if rest, err := asn1.Unmarshal(derBytes, &pki); err != nil {
+ return nil, err
+ } else if len(rest) != 0 {
+ return nil, errors.New("x509: trailing data after ASN.1 of public-key")
+ }
+
+ switch {
+ case pki.Algo.Algorithm.Equal(oidEd25519):
+ pub = ed25519.PublicKey(pki.PublicKey.Bytes)
+ return pub, nil
+ // Prove of concept for key agreement algorithm X25519.
+ // A real implementation would use their own types.
+ //
+ // case pki.Algo.Algorithm.Equal(oidX25519):
+ // pub = ed25519.PublicKey(pki.PublicKey.Bytes)
+ // fmt.Fprintf(os.Stderr, "% x\n", pub)
+ // return pub, nil
+ default:
+ return x509.ParsePKIXPublicKey(derBytes)
+ }
+}
diff --git a/command/crypto/internal/jose/generate.go b/command/crypto/internal/jose/generate.go
new file mode 100644
index 00000000..6bc3894a
--- /dev/null
+++ b/command/crypto/internal/jose/generate.go
@@ -0,0 +1,185 @@
+package jose
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/crypto"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "golang.org/x/crypto/ed25519"
+)
+
+// GenerateJWK generates a JWK given the key type, curve, alg, use, kid and
+// the size of the RSA or oct keys if necessary.
+func GenerateJWK(kty, crv, alg, use, kid string, size int) (jwk *JSONWebKey, err error) {
+ switch kty {
+ case "EC":
+ return generateECKey(crv, alg, use, kid)
+ case "RSA":
+ return generateRSAKey(size, alg, use, kid)
+ case "OKP":
+ return generateOKPKey(crv, alg, use, kid)
+ case "oct":
+ return generateOctKey(size, alg, use, kid)
+ default:
+ return nil, errors.Errorf("missing or invalid value for flag '--kty'")
+ }
+}
+
+// GenerateJWKFromPEM returns an incomplete JSONWebKey using the key from a
+// PEM file.
+func GenerateJWKFromPEM(filename string) (*JSONWebKey, error) {
+ key, err := crypto.ReadPEM(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ switch key := key.(type) {
+ case *rsa.PrivateKey, *rsa.PublicKey:
+ return &JSONWebKey{Key: key}, nil
+ case *ecdsa.PrivateKey:
+ return &JSONWebKey{
+ Key: key,
+ Algorithm: getECAlgorithm(key.Curve),
+ }, nil
+ case *ecdsa.PublicKey:
+ return &JSONWebKey{
+ Key: key,
+ Algorithm: getECAlgorithm(key.Curve),
+ }, nil
+ case ed25519.PrivateKey, ed25519.PublicKey:
+ return &JSONWebKey{
+ Key: key,
+ Algorithm: EdDSA,
+ }, nil
+ default:
+ return nil, errors.Errorf("error parsing %s: unsupported key type '%T'", filename, key)
+ }
+}
+
+func generateECKey(crv, alg, use, kid string) (*JSONWebKey, error) {
+ var c elliptic.Curve
+ var sigAlg string
+ switch crv {
+ case P256, "": // default
+ c, sigAlg = elliptic.P256(), ES256
+ case P384:
+ c, sigAlg = elliptic.P384(), ES384
+ case P521:
+ c, sigAlg = elliptic.P521(), ES512
+ default:
+ return nil, errors.Errorf("missing or invalid value for flag '--crv'")
+ }
+
+ key, err := ecdsa.GenerateKey(c, rand.Reader)
+ if err != nil {
+ return nil, errors.Wrap(err, "error generating ECDSA key")
+ }
+
+ switch use {
+ case "enc":
+ if alg == "" {
+ alg = string(DefaultECKeyAlgorithm)
+ }
+ default:
+ if alg == "" {
+ alg = sigAlg
+ }
+ }
+
+ return &JSONWebKey{
+ Key: key,
+ Algorithm: alg,
+ Use: use,
+ KeyID: kid,
+ }, nil
+}
+
+func generateRSAKey(bits int, alg, use, kid string) (*JSONWebKey, error) {
+ if bits == 0 {
+ bits = DefaultRSASize
+ }
+
+ key, err := rsa.GenerateKey(rand.Reader, bits)
+ if err != nil {
+ return nil, errors.Wrap(err, "error generating RSA key")
+ }
+
+ switch use {
+ case "enc":
+ if alg == "" {
+ alg = string(DefaultRSAKeyAlgorithm)
+ }
+ default:
+ if alg == "" {
+ alg = DefaultRSASigAlgorithm
+ }
+ }
+
+ return &JSONWebKey{
+ Key: key,
+ Algorithm: alg,
+ Use: use,
+ KeyID: kid,
+ }, nil
+}
+
+func generateOKPKey(crv, alg, use, kid string) (*JSONWebKey, error) {
+ switch crv {
+ case Ed25519, "": // default
+ _, key, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ return nil, errors.Wrap(err, "error generating Ed25519 key")
+ }
+
+ switch use {
+ case "enc":
+ return nil, errors.New("invalid algorithm: Ed25519 cannot be used for encryption")
+ default:
+ if alg == "" {
+ alg = EdDSA
+ }
+ }
+
+ return &JSONWebKey{
+ Key: key,
+ Algorithm: alg,
+ Use: use,
+ KeyID: kid,
+ }, nil
+ default:
+ return nil, errors.Errorf("missing or invalid value for flag '--crv'")
+ }
+}
+
+func generateOctKey(size int, alg, use, kid string) (*JSONWebKey, error) {
+ if size == 0 {
+ size = DefaultOctSize
+ }
+
+ key, err := utils.RandAlphanumeric(size)
+ if err != nil {
+ return nil, err
+ }
+
+ switch use {
+ case "enc":
+ if alg == "" {
+ alg = string(DefaultOctKeyAlgorithm)
+ }
+ default:
+ if alg == "" {
+ alg = string(DefaultOctSigsAlgorithm)
+ }
+ }
+
+ return &JSONWebKey{
+ Key: []byte(key),
+ Algorithm: alg,
+ Use: use,
+ KeyID: kid,
+ }, nil
+}
diff --git a/command/crypto/internal/jose/parse.go b/command/crypto/internal/jose/parse.go
new file mode 100644
index 00000000..7f70c031
--- /dev/null
+++ b/command/crypto/internal/jose/parse.go
@@ -0,0 +1,260 @@
+package jose
+
+import (
+ "bytes"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rsa"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/crypto"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "golang.org/x/crypto/ed25519"
+ jose "gopkg.in/square/go-jose.v2"
+)
+
+type keyType int
+
+const (
+ jwkKeyType keyType = iota
+ pemKeyType
+ octKeyType
+)
+
+// MaxDecryptTries is the maximum number of attempts to decrypt a file.
+const MaxDecryptTries = 3
+
+// Decrypt returns the decrypted version of the given data if it's encrypted,
+// it will return the raw data if it's not encrypted or the format is not
+// valid.
+func Decrypt(prompt string, data []byte) ([]byte, error) {
+ enc, err := jose.ParseEncrypted(string(data))
+ if err != nil {
+ return data, nil
+ }
+
+ // Decrypt flow
+ var pass []byte
+ for i := 0; i < MaxDecryptTries; i++ {
+ pass, err = utils.ReadPassword(prompt)
+ if err != nil {
+ return nil, err
+ }
+
+ if data, err = enc.Decrypt(pass); err == nil {
+ return data, nil
+ }
+ }
+
+ return nil, errors.Wrap(err, "failed to decrypt")
+}
+
+// ParseKey returns a JSONWebKey from the given JWK file or a PEM file. For
+// password protected keys, it will ask the user for a password.
+func ParseKey(filename, use, alg, kid string, subtle bool) (*JSONWebKey, error) {
+ b, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error reading %s", filename)
+ }
+
+ jwk := new(JSONWebKey)
+ switch guessKeyType(alg, b) {
+ case jwkKeyType:
+ // Attempt to parse an encrypted file
+ prompt := fmt.Sprintf("Please enter the password to decrypt %s: ", filename)
+ if b, err = Decrypt(prompt, b); err != nil {
+ return nil, err
+ }
+
+ // Unmarshal the plain (or decrypted JWK)
+ if err := json.Unmarshal(b, jwk); err != nil {
+ return nil, errors.Errorf("error reading %s: unsupported format", filename)
+ }
+ case pemKeyType:
+ if jwk.Key, err = crypto.ParsePEM(b, filename); err != nil {
+ return nil, err
+ }
+ case octKeyType:
+ jwk.Key = b
+ }
+
+ // Validate key id
+ if kid != "" && jwk.KeyID != "" && kid != jwk.KeyID {
+ return nil, errors.Errorf("kid %s does not match the kid on %s", kid, filename)
+ }
+ if jwk.KeyID == "" {
+ jwk.KeyID = kid
+ }
+ if jwk.Use == "" {
+ jwk.Use = use
+ }
+
+ // Set the algorithm if empty
+ guessJWKAlgorithm(jwk, alg)
+
+ // Validate alg: if the flag '--subtle' is passed we will allow to overwrite it
+ if !subtle && alg != "" && jwk.Algorithm != "" && alg != jwk.Algorithm {
+ return nil, errors.Errorf("alg %s does not match the alg on %s", alg, filename)
+ }
+ if subtle && alg != "" {
+ jwk.Algorithm = alg
+ }
+
+ return jwk, nil
+}
+
+// ReadJWKSet reads a JWK Set from a URL or filename. URLs must start with "https://".
+func ReadJWKSet(filename string) ([]byte, error) {
+ if strings.HasPrefix(filename, "https://") {
+ resp, err := http.Get(filename)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error retrieving %s", filename)
+ }
+ defer resp.Body.Close()
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error retrieving %s", filename)
+ }
+ return b, nil
+ }
+ b, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error reading %s", filename)
+ }
+ return b, nil
+}
+
+// ParseKeySet returns the JWK with the given key after parsing a JWKSet from
+// a given file.
+func ParseKeySet(filename, alg, kid string, isSubtle bool) (*jose.JSONWebKey, error) {
+ b, err := ReadJWKSet(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ // Attempt to parse an encrypted file
+ prompt := fmt.Sprintf("Please enter the password to decrypt %s: ", filename)
+ if b, err = Decrypt(prompt, b); err != nil {
+ return nil, err
+ }
+
+ // Unmarshal the plain or decrypted JWKSet
+ jwkSet := new(jose.JSONWebKeySet)
+ if err := json.Unmarshal(b, jwkSet); err != nil {
+ return nil, errors.Errorf("error reading %s: unsupported format", filename)
+ }
+
+ jwks := jwkSet.Key(kid)
+ switch len(jwks) {
+ case 0:
+ return nil, errors.Errorf("cannot find key with kid %s on %s", kid, filename)
+ case 1:
+ jwk := &jwks[0]
+
+ // Set the algorithm if empty
+ guessJWKAlgorithm(jwk, alg)
+
+ // Validate alg: if the flag '--subtle' is passed we will allow the
+ // overwrite of the alg
+ if !isSubtle && alg != "" && jwk.Algorithm != "" && alg != jwk.Algorithm {
+ return nil, errors.Errorf("alg %s does not match the alg on %s", alg, filename)
+ }
+ if isSubtle && alg != "" {
+ jwk.Algorithm = alg
+ }
+ return jwk, nil
+ default:
+ return nil, errors.Errorf("multiple keys with kid %s have been found on %s", kid, filename)
+ }
+}
+
+// guessKeyType returns the key type of the given data. Key types are JWK, PEM
+// or oct.
+func guessKeyType(alg string, data []byte) keyType {
+ switch alg {
+ // jwk or file with oct data
+ case "HS256", "HS384", "HS512":
+ // Encrypted JWK ?
+ if _, err := jose.ParseEncrypted(string(data)); err == nil {
+ return jwkKeyType
+ }
+ // JSON JWK ?
+ if err := json.Unmarshal(data, &JSONWebKey{}); err == nil {
+ return jwkKeyType
+ }
+ // Default to oct
+ return octKeyType
+ default:
+ // PEM or default to JWK
+ if bytes.HasPrefix(data, []byte("-----BEGIN ")) {
+ return pemKeyType
+ }
+ return jwkKeyType
+ }
+}
+
+// guessJWKAlgorithm set the algorithm if it's not set and we can guess it
+func guessJWKAlgorithm(jwk *jose.JSONWebKey, defaultAlg string) {
+ if jwk.Algorithm == "" {
+ // Force default algorithm if passed.
+ if defaultAlg != "" {
+ jwk.Algorithm = defaultAlg
+ return
+ }
+
+ // Use defaults for each key type
+ switch k := jwk.Key.(type) {
+ case []byte:
+ if jwk.Use == "enc" {
+ jwk.Algorithm = string(DefaultOctKeyAlgorithm)
+ } else {
+ jwk.Algorithm = string(DefaultOctSigsAlgorithm)
+ }
+ case *ecdsa.PrivateKey:
+ if jwk.Use == "enc" {
+ jwk.Algorithm = string(DefaultECKeyAlgorithm)
+ } else {
+ jwk.Algorithm = getECAlgorithm(k.Curve)
+ }
+ case *ecdsa.PublicKey:
+ if jwk.Use == "enc" {
+ jwk.Algorithm = string(DefaultECKeyAlgorithm)
+ } else {
+ jwk.Algorithm = getECAlgorithm(k.Curve)
+ }
+ case *rsa.PrivateKey, *rsa.PublicKey:
+ if jwk.Use == "enc" {
+ jwk.Algorithm = string(DefaultRSAKeyAlgorithm)
+ } else {
+ jwk.Algorithm = string(DefaultRSASigAlgorithm)
+ }
+ // Ed25519 can only be used for signing operations
+ case ed25519.PrivateKey, ed25519.PublicKey:
+ jwk.Algorithm = EdDSA
+ case *ed25519.PrivateKey, *ed25519.PublicKey:
+ jwk.Algorithm = EdDSA
+ }
+ }
+}
+
+// getECAlgorithm returns the JWA algorithm name for the given elliptic curve.
+// If the curve is not supported it will return an empty string.
+//
+// Supported curves are P-256, P-384, and P-521.
+func getECAlgorithm(crv elliptic.Curve) string {
+ switch crv.Params().Name {
+ case P256:
+ return ES256
+ case P384:
+ return ES384
+ case P521:
+ return ES512
+ default:
+ return ""
+ }
+}
diff --git a/command/crypto/internal/jose/types.go b/command/crypto/internal/jose/types.go
new file mode 100644
index 00000000..3be60035
--- /dev/null
+++ b/command/crypto/internal/jose/types.go
@@ -0,0 +1,218 @@
+// Code generated (comment to force golint to ignore this file). DO NOT EDIT.
+
+package jose
+
+import (
+ "time"
+
+ jose "gopkg.in/square/go-jose.v2"
+ "gopkg.in/square/go-jose.v2/jwt"
+)
+
+// SupportsPBKDF2 constant to know if the underlaying library supports
+// password based cryptography algorithms.
+const SupportsPBKDF2 = true
+
+// JSONWebToken represents a JSON Web Token (as specified in RFC7519).
+type JSONWebToken = jwt.JSONWebToken
+
+// JSONWebKey represents a public or private key in JWK format.
+type JSONWebKey = jose.JSONWebKey
+
+// JSONWebKeySet represents a JWK Set object.
+type JSONWebKeySet = jose.JSONWebKeySet
+
+// JSONWebEncryption represents an encrypted JWE object after parsing.
+type JSONWebEncryption = jose.JSONWebEncryption
+
+// Recipient represents an algorithm/key to encrypt messages to.
+type Recipient = jose.Recipient
+
+// EncrypterOptions represents options that can be set on new encrypters.
+type EncrypterOptions = jose.EncrypterOptions
+
+// Encrypter represents an encrypter which produces an encrypted JWE object.
+type Encrypter = jose.Encrypter
+
+// ContentType represents type of the contained data.
+type ContentType = jose.ContentType
+
+// KeyAlgorithm represents a key management algorithm.
+type KeyAlgorithm = jose.KeyAlgorithm
+
+// ContentEncryption represents a content encryption algorithm.
+type ContentEncryption = jose.ContentEncryption
+
+// SignatureAlgorithm represents a signature (or MAC) algorithm.
+type SignatureAlgorithm = jose.SignatureAlgorithm
+
+// ErrCryptoFailure indicates an error in a cryptographic primitive.
+var ErrCryptoFailure = jose.ErrCryptoFailure
+
+// Claims represents public claim values (as specified in RFC 7519).
+type Claims = jwt.Claims
+
+// Builder is a utility for making JSON Web Tokens. Calls can be chained, and
+// errors are accumulated until the final call to CompactSerialize/FullSerialize.
+type Builder = jwt.Builder
+
+// NumericDate represents date and time as the number of seconds since the
+// epoch, including leap seconds. Non-integer values can be represented
+// in the serialized format, but we round to the nearest second.
+type NumericDate = jwt.NumericDate
+
+// Audience represents the recipients that the token is intended for.
+type Audience = jwt.Audience
+
+// Expected defines values used for protected claims validation.
+// If field has zero value then validation is skipped.
+type Expected = jwt.Expected
+
+// Signer represents a signer which takes a payload and produces a signed JWS object.
+type Signer = jose.Signer
+
+// SigningKey represents an algorithm/key used to sign a message.
+type SigningKey = jose.SigningKey
+
+// SignerOptions represents options that can be set when creating signers.
+type SignerOptions = jose.SignerOptions
+
+// ErrInvalidIssuer indicates invalid iss claim.
+var ErrInvalidIssuer = jwt.ErrInvalidIssuer
+
+// ErrInvalidAudience indicated invalid aud claim.
+var ErrInvalidAudience = jwt.ErrInvalidAudience
+
+// ErrNotValidYet indicates that token is used before time indicated in nbf claim.
+var ErrNotValidYet = jwt.ErrNotValidYet
+
+// ErrExpired indicates that token is used after expiry time indicated in exp claim.
+var ErrExpired = jwt.ErrExpired
+
+// ErrInvalidSubject indicates invalid sub claim.
+var ErrInvalidSubject = jwt.ErrInvalidSubject
+
+// ErrInvalidID indicates invalid jti claim.
+var ErrInvalidID = jwt.ErrInvalidID
+
+// Key management algorithms
+const (
+ RSA1_5 = KeyAlgorithm("RSA1_5") // RSA-PKCS1v1.5
+ RSA_OAEP = KeyAlgorithm("RSA-OAEP") // RSA-OAEP-SHA1
+ RSA_OAEP_256 = KeyAlgorithm("RSA-OAEP-256") // RSA-OAEP-SHA256
+ A128KW = KeyAlgorithm("A128KW") // AES key wrap (128)
+ A192KW = KeyAlgorithm("A192KW") // AES key wrap (192)
+ A256KW = KeyAlgorithm("A256KW") // AES key wrap (256)
+ DIRECT = KeyAlgorithm("dir") // Direct encryption
+ ECDH_ES = KeyAlgorithm("ECDH-ES") // ECDH-ES
+ ECDH_ES_A128KW = KeyAlgorithm("ECDH-ES+A128KW") // ECDH-ES + AES key wrap (128)
+ ECDH_ES_A192KW = KeyAlgorithm("ECDH-ES+A192KW") // ECDH-ES + AES key wrap (192)
+ ECDH_ES_A256KW = KeyAlgorithm("ECDH-ES+A256KW") // ECDH-ES + AES key wrap (256)
+ A128GCMKW = KeyAlgorithm("A128GCMKW") // AES-GCM key wrap (128)
+ A192GCMKW = KeyAlgorithm("A192GCMKW") // AES-GCM key wrap (192)
+ A256GCMKW = KeyAlgorithm("A256GCMKW") // AES-GCM key wrap (256)
+ PBES2_HS256_A128KW = KeyAlgorithm("PBES2-HS256+A128KW") // PBES2 + HMAC-SHA256 + AES key wrap (128)
+ PBES2_HS384_A192KW = KeyAlgorithm("PBES2-HS384+A192KW") // PBES2 + HMAC-SHA384 + AES key wrap (192)
+ PBES2_HS512_A256KW = KeyAlgorithm("PBES2-HS512+A256KW") // PBES2 + HMAC-SHA512 + AES key wrap (256)
+)
+
+// Signature algorithms
+const (
+ HS256 = "HS256" // HMAC using SHA-256
+ HS384 = "HS384" // HMAC using SHA-384
+ HS512 = "HS512" // HMAC using SHA-512
+ RS256 = "RS256" // RSASSA-PKCS-v1.5 using SHA-256
+ RS384 = "RS384" // RSASSA-PKCS-v1.5 using SHA-384
+ RS512 = "RS512" // RSASSA-PKCS-v1.5 using SHA-512
+ ES256 = "ES256" // ECDSA using P-256 and SHA-256
+ ES384 = "ES384" // ECDSA using P-384 and SHA-384
+ ES512 = "ES512" // ECDSA using P-521 and SHA-512
+ PS256 = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256
+ PS384 = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384
+ PS512 = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512
+ EdDSA = "EdDSA" // Ed25591
+)
+
+// Content encryption algorithms
+const (
+ A128CBC_HS256 = ContentEncryption("A128CBC-HS256") // AES-CBC + HMAC-SHA256 (128)
+ A192CBC_HS384 = ContentEncryption("A192CBC-HS384") // AES-CBC + HMAC-SHA384 (192)
+ A256CBC_HS512 = ContentEncryption("A256CBC-HS512") // AES-CBC + HMAC-SHA512 (256)
+ A128GCM = ContentEncryption("A128GCM") // AES-GCM (128)
+ A192GCM = ContentEncryption("A192GCM") // AES-GCM (192)
+ A256GCM = ContentEncryption("A256GCM") // AES-GCM (256)
+)
+
+// Elliptic curves
+const (
+ P256 = "P-256" // P-256 curve (FIPS 186-3)
+ P384 = "P-384" // P-384 curve (FIPS 186-3)
+ P521 = "P-521" // P-521 curve (FIPS 186-3)
+)
+
+// Ed25519 is the EdDSA signature scheme using SHA-512/256 and Curve25519
+const Ed25519 = "Ed25519"
+
+// Default key management, signature, and content encryption algorithms to use if none is specified.
+const (
+ // Key management algorithms
+ DefaultECKeyAlgorithm = ECDH_ES
+ DefaultRSAKeyAlgorithm = RSA_OAEP_256
+ DefaultOctKeyAlgorithm = A256GCMKW
+ // Signature algorithms
+ DefaultRSASigAlgorithm = RS256
+ DefaultOctSigsAlgorithm = HS256
+ // Content encryption algorithm
+ DefaultEncAlgorithm = A256GCM
+)
+
+// Default sizes
+const (
+ DefaultRSASize = 2048
+ DefaultOctSize = 32
+)
+
+// ParseEncrypted parses an encrypted message in compact or full serialization format.
+func ParseEncrypted(input string) (*JSONWebEncryption, error) {
+ return jose.ParseEncrypted(input)
+}
+
+// NewEncrypter creates an appropriate encrypter based on the key type.
+func NewEncrypter(enc ContentEncryption, rcpt Recipient, opts *EncrypterOptions) (Encrypter, error) {
+ return jose.NewEncrypter(enc, rcpt, opts)
+}
+
+// NewNumericDate constructs NumericDate from time.Time value.
+func NewNumericDate(t time.Time) NumericDate {
+ return jwt.NewNumericDate(t)
+}
+
+// NewSigner creates an appropriate signer based on the key type
+func NewSigner(sig SigningKey, opts *SignerOptions) (Signer, error) {
+ return jose.NewSigner(sig, opts)
+}
+
+// ParseSigned parses token from JWS form.
+func ParseSigned(s string) (*JSONWebToken, error) {
+ return jwt.ParseSigned(s)
+}
+
+// Signed creates builder for signed tokens.
+func Signed(sig Signer) Builder {
+ return jwt.Signed(sig)
+}
+
+// Determine whether a JSONWebKey is symmetric
+func IsSymmetric(k *JSONWebKey) bool {
+ switch k.Key.(type) {
+ case []byte:
+ return true
+ default:
+ return false
+ }
+}
+
+// Determine whether a JSONWebKey is asymmetric
+func IsAsymmetric(k *JSONWebKey) bool {
+ return !IsSymmetric(k)
+}
diff --git a/command/crypto/internal/jose/validate.go b/command/crypto/internal/jose/validate.go
new file mode 100644
index 00000000..34262d90
--- /dev/null
+++ b/command/crypto/internal/jose/validate.go
@@ -0,0 +1,123 @@
+package jose
+
+import (
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "fmt"
+
+ "github.com/pkg/errors"
+ "golang.org/x/crypto/ed25519"
+)
+
+// ValidateJWK validates the given JWK.
+func ValidateJWK(jwk *JSONWebKey) error {
+ switch jwk.Use {
+ case "sig":
+ return validateSigJWK(jwk)
+ case "enc":
+ return validateEncJWK(jwk)
+ default:
+ return validateGeneric(jwk)
+ }
+}
+
+// validateSigJWK validates the given JWK for signature operations.
+func validateSigJWK(jwk *JSONWebKey) error {
+ if jwk.Algorithm == "" {
+ return errors.New("flag '--alg' is required with the given key")
+ }
+ errctx := "the given key"
+
+ switch k := jwk.Key.(type) {
+ case []byte:
+ switch jwk.Algorithm {
+ case HS256, HS384, HS512:
+ return nil
+ }
+ errctx = "kty 'oct'"
+ case *rsa.PrivateKey, *rsa.PublicKey:
+ switch jwk.Algorithm {
+ case RS256, RS384, RS512:
+ return nil
+ case PS256, PS384, PS512:
+ return nil
+ }
+ errctx = "kty 'RSA'"
+ case *ecdsa.PrivateKey:
+ curve := k.Params().Name
+ switch {
+ case jwk.Algorithm == ES256 && curve == P256:
+ return nil
+ case jwk.Algorithm == ES384 && curve == P384:
+ return nil
+ case jwk.Algorithm == ES512 && curve == P521:
+ return nil
+ }
+ errctx = fmt.Sprintf("kty 'EC' and crv '%s'", curve)
+ case *ecdsa.PublicKey:
+ curve := k.Params().Name
+ switch {
+ case jwk.Algorithm == ES256 && curve == P256:
+ return nil
+ case jwk.Algorithm == ES384 && curve == P384:
+ return nil
+ case jwk.Algorithm == ES512 && curve == P521:
+ return nil
+ }
+ errctx = fmt.Sprintf("kty 'EC' and crv '%s'", curve)
+ case ed25519.PrivateKey, ed25519.PublicKey:
+ if jwk.Algorithm == EdDSA {
+ return nil
+ }
+ errctx = "kty 'OKP' and crv 'Ed25519'"
+ }
+
+ return errors.Errorf("alg '%s' is not compatible with %s", jwk.Algorithm, errctx)
+}
+
+// validatesEncJWK validates the given JWK for encryption operations.
+func validateEncJWK(jwk *JSONWebKey) error {
+ alg := KeyAlgorithm(jwk.Algorithm)
+ var kty string
+
+ switch jwk.Key.(type) {
+ case []byte:
+ switch alg {
+ case DIRECT, A128GCMKW, A192GCMKW, A256GCMKW, A128KW, A192KW, A256KW:
+ return nil
+ }
+ kty = "oct"
+ case *rsa.PrivateKey, *rsa.PublicKey:
+ switch alg {
+ case RSA1_5, RSA_OAEP, RSA_OAEP_256:
+ return nil
+ }
+ kty = "RSA"
+ case *ecdsa.PrivateKey, *ecdsa.PublicKey:
+ switch alg {
+ case ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW:
+ return nil
+ }
+ kty = "EC"
+ case ed25519.PrivateKey, ed25519.PublicKey:
+ return errors.New("key Ed25519 cannot be used for encryption")
+ }
+
+ return errors.Errorf("alg '%s' is not compatible with kty '%s'", jwk.Algorithm, kty)
+}
+
+// validateGeneric validates just the supported key types.
+func validateGeneric(jwk *JSONWebKey) error {
+ switch jwk.Key.(type) {
+ case []byte:
+ return nil
+ case *rsa.PrivateKey, *rsa.PublicKey:
+ return nil
+ case *ecdsa.PrivateKey, *ecdsa.PublicKey:
+ return nil
+ case ed25519.PrivateKey, ed25519.PublicKey:
+ return nil
+ }
+
+ return errors.Errorf("unsupported key type '%T'", jwk.Key)
+}
diff --git a/command/crypto/internal/utils/keys.go b/command/crypto/internal/utils/keys.go
new file mode 100644
index 00000000..d4e7ff09
--- /dev/null
+++ b/command/crypto/internal/utils/keys.go
@@ -0,0 +1,68 @@
+package utils
+
+// WritePublicKey encodes a crypto public key to a file on disk in PEM format.
+import (
+ "encoding/pem"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/crypto/keys"
+ "github.com/smallstep/cli/errs"
+)
+
+// WritePublicKey encodes a crypto public key to a file on disk in PEM format.
+// Any file with the same name will be overwritten.
+func WritePublicKey(key interface{}, out string) error {
+ // Remove any file with same name, if it exists.
+ if _, err := os.Stat(out); err == nil {
+ if err = os.Remove(out); err != nil {
+ return errs.FileError(err, out)
+ }
+ }
+ keyOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
+ os.FileMode(0600))
+ if err != nil {
+ return errs.FileError(err, out)
+ }
+ pubPEM, err := keys.PublicPEM(key)
+ if err != nil {
+ return errs.Wrap(err,
+ "failed to convert public key to PEM block")
+ }
+ err = pem.Encode(keyOut, pubPEM)
+ if err != nil {
+ return errs.Wrap(err,
+ "pem encode '%s' failed", out)
+ }
+ keyOut.Close()
+ return nil
+}
+
+// WritePrivateKey encodes a crypto private key to a file on disk in PEM format.
+// Any file with the same name will be overwritten.
+func WritePrivateKey(key interface{}, pass, out string) error {
+ // Remove any file with same name, if it exists.
+ // Permissions on private key files may be such that overwriting them is impossible.
+ if _, err := os.Stat(out); err == nil {
+ if err = os.Remove(out); err != nil {
+ return errs.FileError(err, out)
+ }
+ }
+ keyOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
+ os.FileMode(0600))
+ if err != nil {
+ return errs.FileError(err, out)
+ }
+ privPem, err := keys.PrivatePEM(key, keys.DefaultEncOpts(pass))
+ if err != nil {
+ return errors.Wrap(err,
+ "failed to convert private key to PEM block")
+ }
+ err = pem.Encode(keyOut, privPem)
+ if err != nil {
+ return errors.Wrapf(err,
+ "pem encode '%s' failed", out)
+ }
+ keyOut.Close()
+ return nil
+}
diff --git a/command/crypto/internal/utils/random.go b/command/crypto/internal/utils/random.go
new file mode 100644
index 00000000..266d6588
--- /dev/null
+++ b/command/crypto/internal/utils/random.go
@@ -0,0 +1,36 @@
+package utils
+
+import (
+ "crypto/rand"
+ "math/big"
+
+ "github.com/pkg/errors"
+)
+
+// RandString returns a random string of a given length using the characters
+// in the given string. It splits the string on runes to support UTF-8
+// characters.
+func RandString(length int, chars string) (string, error) {
+ result := make([]rune, length)
+ runes := []rune(chars)
+ for i := range result {
+ num, err := rand.Int(rand.Reader, big.NewInt(int64(len(runes))))
+ if err != nil {
+ return "", errors.Wrap(err, "error creating random number")
+ }
+ result[i] = runes[num.Int64()]
+ }
+ return string(result), nil
+}
+
+// RandHex returns a random string of the given length using the hexadecimal
+// characters in lower case (0-9+a-f).
+func RandHex(length int) (string, error) {
+ return RandString(length, "0123456789abcdef")
+}
+
+// RandAlphanumeric returns a random string of the given length using the 62
+// alphanumeric characters in the POSIX/C locale (a-z+A-Z+0-9).
+func RandAlphanumeric(length int) (string, error) {
+ return RandString(length, "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
+}
diff --git a/command/crypto/internal/utils/read.go b/command/crypto/internal/utils/read.go
new file mode 100644
index 00000000..b643c86b
--- /dev/null
+++ b/command/crypto/internal/utils/read.go
@@ -0,0 +1,71 @@
+package utils
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "strings"
+ "syscall"
+
+ "github.com/pkg/errors"
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+// ReadAll returns a slice of bytes with the content of the given reader.
+func ReadAll(r io.Reader) ([]byte, error) {
+ b, err := ioutil.ReadAll(r)
+ return b, errors.Wrap(err, "error reading data")
+}
+
+// ReadString reads one line from the given io.Reader.
+func ReadString(r io.Reader) (string, error) {
+ br := bufio.NewReader(r)
+ str, err := br.ReadString('\n')
+ if err != nil && err != io.EOF {
+ return "", errors.Wrap(err, "error reading string")
+ }
+ return strings.TrimSpace(str), nil
+}
+
+// ReadPassword asks the user for a password using the given prompt. If the
+// program is receiving data from STDIN using a pipe, we cannot use
+// terminal.ReadPassword on STDIN and we need to open the tty and read from
+// it.
+//
+// This solution works on darwin and linux, but it might not work on other
+// OSs.
+func ReadPassword(prompt string) ([]byte, error) {
+ fmt.Fprint(os.Stderr, prompt)
+ var fd int
+ if terminal.IsTerminal(syscall.Stdin) {
+ fd = syscall.Stdin
+ } else {
+ tty, err := os.Open("/dev/tty")
+ if err != nil {
+ return nil, errors.Wrap(err, "error allocating terminal")
+ }
+ defer tty.Close()
+ fd = int(tty.Fd())
+ }
+
+ pass, err := terminal.ReadPassword(fd)
+ fmt.Fprintln(os.Stderr)
+ return pass, errors.Wrap(err, "error reading password")
+}
+
+// ReadInput from stdin if something is detected or ask the user for an input
+// using the given prompt.
+func ReadInput(prompt string) ([]byte, error) {
+ st, err := os.Stdin.Stat()
+ if err != nil {
+ return nil, errors.Wrap(err, "error reading data")
+ }
+
+ if st.Size() > 0 {
+ return ReadAll(os.Stdin)
+ }
+
+ return ReadPassword(prompt)
+}
diff --git a/command/crypto/internal/utils/write.go b/command/crypto/internal/utils/write.go
new file mode 100644
index 00000000..a34535f9
--- /dev/null
+++ b/command/crypto/internal/utils/write.go
@@ -0,0 +1,70 @@
+package utils
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "strings"
+ "syscall"
+
+ "github.com/pkg/errors"
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+var (
+ // ErrFileExists is the error returned if a file exists.
+ ErrFileExists = errors.New("file exists")
+
+ // ErrIsDir is the error returned if the file is a directory.
+ ErrIsDir = errors.New("file is a directory")
+)
+
+// WriteFile wraps ioutil.WriteFile with a prompt to overwrite a file if the
+// file exists. It returns ErrFileExists if the user picks to not overwrite
+// the file.
+func WriteFile(filename string, data []byte, perm os.FileMode) error {
+ st, err := os.Stat(filename)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return ioutil.WriteFile(filename, data, perm)
+ }
+ return errors.Wrapf(err, "error reading information for %s", filename)
+ }
+
+ if st.IsDir() {
+ return ErrIsDir
+ }
+
+ // The file exists
+ var r io.Reader
+ if terminal.IsTerminal(syscall.Stdin) {
+ r = os.Stdin
+ } else {
+ tty, err := os.Open("/dev/tty")
+ if err != nil {
+ return errors.Wrap(err, "error allocating terminal")
+ }
+ r = tty
+ defer tty.Close()
+ }
+
+ br := bufio.NewReader(r)
+ for cont := true; cont; {
+ fmt.Fprintf(os.Stderr, "Would you like to overwrite %s [Y/n]: ", filename)
+ str, err := br.ReadString('\n')
+ if err != nil {
+ return errors.Wrap(err, "error reading line")
+ }
+ str = strings.ToLower(strings.TrimSpace(str))
+ switch str {
+ case "", "y", "yes":
+ cont = false
+ case "n", "no":
+ return ErrFileExists
+ }
+ }
+
+ return ioutil.WriteFile(filename, data, perm)
+}
diff --git a/command/crypto/jwe/decrypt.go b/command/crypto/jwe/decrypt.go
new file mode 100644
index 00000000..b6263751
--- /dev/null
+++ b/command/crypto/jwe/decrypt.go
@@ -0,0 +1,133 @@
+package jwe
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/smallstep/cli/errs"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/jose"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/urfave/cli"
+)
+
+func decryptCommand() cli.Command {
+ return cli.Command{
+ Name: "decrypt",
+ Action: cli.ActionFunc(decryptAction),
+ Usage: "verify a JWE and decrypt ciphertext",
+ UsageText: "step crypto jwe decrypt [--key JWK] [--jwks JWKS] [--kid KID]",
+ Description: `The 'step crypto jwe decrypt' command verifies a JWE read from STDIN and
+decrypts the ciphertext printing it to STDOUT. If verification fails a
+non-zero failure code is returned. If verification succeeds the command
+returns 0.`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "key",
+ Usage: `The JWE recipient's private key. The KEY argument should be the name of a
+file containing a private JWK (or a JWK encrypted as a JWE payload) or a PEM
+encoded private key (or a private key encrypted using [TODO: insert private
+key encryption mechanism]).`,
+ },
+ cli.StringFlag{
+ Name: "jwks",
+ Usage: `The JWK Set containing the recipient's private key. The JWKS argument should
+be the name of a file. The file contents should be a JWK Set or a JWE with a
+JWK Set payload. The '--jwks' flag requires the use of the '--kid' flag to
+specify which key to use.`,
+ },
+ cli.StringFlag{
+ Name: "kid",
+ Usage: `The ID of the recipient's private key. KID is a case-sensitive string. When
+used with '--jwk' the KID value must match the "kid" member of the JWK. When
+used with '--jwks' (a JWK Set) the KID value must match the "kid" member of
+one of the JWKs in the JWK Set.`,
+ },
+ },
+ }
+}
+
+func decryptAction(ctx *cli.Context) error {
+ data, err := utils.ReadAll(os.Stdin)
+ if err != nil {
+ return err
+ }
+
+ key := ctx.String("key")
+ jwks := ctx.String("jwks")
+ kid := ctx.String("kid")
+
+ obj, err := jose.ParseEncrypted(string(data))
+ if err != nil {
+ return errors.Wrap(err, "error parsing data")
+ }
+
+ alg := jose.KeyAlgorithm(obj.Header.Algorithm)
+
+ var isPBES2 bool
+ switch alg {
+ case jose.PBES2_HS256_A128KW, jose.PBES2_HS384_A192KW, jose.PBES2_HS512_A256KW:
+ isPBES2 = true
+ }
+
+ switch {
+ case isPBES2 && key != "":
+ return errors.Errorf("flag '--key' cannot be used with JWE algorithm '%s'", alg)
+ case isPBES2 && jwks != "":
+ return errors.Errorf("flag '--jwks' cannot be used with JWE algorithm '%s'", alg)
+ case !isPBES2 && key == "" && jwks == "":
+ return errs.RequiredOrFlag(ctx, "key", "jwk")
+ case key != "" && jwks != "":
+ return errs.MutuallyExclusiveFlags(ctx, "key", "jwks")
+ case jwks != "" && kid == "":
+ return errs.RequiredWithFlag(ctx, "kid", "jwks")
+ }
+
+ // Read key from --key or --jwks
+ var pbes2Key []byte
+ var jwk *jose.JSONWebKey
+ switch {
+ case key != "":
+ jwk, err = jose.ParseKey(key, "enc", "", kid, false)
+ case jwks != "":
+ jwk, err = jose.ParseKeySet(jwks, "", kid, false)
+ case isPBES2:
+ pbes2Key, err = utils.ReadPassword("Please enter the password to decrypt the content encryption key: ")
+ default:
+ return errs.RequiredOrFlag(ctx, "key", "jwk")
+ }
+ if err != nil {
+ return err
+ }
+
+ var decryptKey interface{}
+ if isPBES2 {
+ decryptKey = pbes2Key
+ } else {
+ // Private keys are used for decryption
+ if jwk.IsPublic() {
+ return errors.New("cannot use a public key for decryption")
+ }
+
+ if jwk.Use == "sig" {
+ return errors.New("invalid jwk use: found 'sig' (signature), expecting 'enc' (encryption)")
+ }
+
+ // Validate jwk
+ if err := jose.ValidateJWK(jwk); err != nil {
+ return err
+ }
+
+ decryptKey = jwk.Key
+ }
+
+ decrypted, err := obj.Decrypt(decryptKey)
+ if err != nil {
+ return errors.Wrap(err, "error decrypting data")
+ }
+
+ fmt.Printf(string(decrypted))
+
+ return nil
+}
diff --git a/command/crypto/jwe/encrypt.go b/command/crypto/jwe/encrypt.go
new file mode 100644
index 00000000..7c46d636
--- /dev/null
+++ b/command/crypto/jwe/encrypt.go
@@ -0,0 +1,316 @@
+package jwe
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/jose"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+func encryptCommand() cli.Command {
+ return cli.Command{
+ Name: "encrypt",
+ Action: cli.ActionFunc(encryptAction),
+ Usage: "encrypt a payload using JSON Web Encryption (JWE)",
+ UsageText: `step crypto jwe encrypt [--alg KEY_ENC_ALGORITHM] [--enc CONTENT_ENC_ALGORITHM]
+ [--key JWK] [--jwks JWKS] [--kid KID]`,
+ Description: `The 'step crypto jwe encrypt' command encrypts a payload using JSON Web
+Encryption (JWE). By default, the payload to encrypt is read from STDIN and
+the JWE data structure will be written to STDOUT.`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "alg, algorithm",
+ Usage: `The cryptographic algorithm used to encrypt or determine the value of the
+content encryption key (CEK). Algorithms are case-sensitive strings defined in
+RFC7518. The selected algorithm must be compatible with the key type. This
+flag is optional. If not specified, the 'alg' member of the JWK is used. If
+the JWK has no "alg" member then a default is selected depending on the JWK
+key type. If the JWK has an "alg" member and the "alg" flag is passed the two
+options must match unless the '--subtle' flag is also passed.
+
+ KEY_ENC_ALGORITHM is a case-sensitive string and must be one of:
+
+ RSA1_5
+ RSAES-PKCS1-v1_5
+ RSA-OAEP
+ RSAES OAEP using default parameters
+ RSA-OAEP-256
+ RSAES OAEP using SHA-256 and MGF1 with SHA-256
+ A128KW
+ AES Key Wrap with default initial value using 128-bit key
+ A192KW
+ AES Key Wrap with default initial value using 192-bit key
+ A256KW
+ AES Key Wrap with default initial value using 256-bit key
+ dir
+ Direct use of a shared symmetric key as the content encryption key (CEK)
+ ECDH-ES (default for EC keys)
+ Elliptic Curve Diffie-Hellman Ephemeral Static key agreement
+ ECDH-ES+A128KW
+ ECDH-ES using Concat KDF and CEK wrapped with "A128KW"
+ ECDH-ES+A192KW
+ ECDH-ES using Concat KDF and CEK wrapped with "A192KW"
+ ECDH-ES+A256KW
+ ECDH-ES using Concat KDF and CEK wrapped with "A256KW"
+ A128GCMKW
+ Key wrappiung with AES GCM using 128-bit key
+ A192GCMKW
+ Key wrappiung with AES GCM using 192-bit key
+ A256GCMKW
+ Key wrappiung with AES GCM using 256-bit key
+ PBES2-HS256+A128KW
+ PBES2 with HMAC SHA-256 and "A128KW" wrapping
+ PBES2-HS384+A192KW
+ PBES2 with HMAC SHA-256 and "A192KW" wrapping
+ PBES2-HS512+A256KW
+ PBES2 with HMAC SHA-256 and "A256KW" wrapping`,
+ },
+ cli.StringFlag{
+ Name: "enc, encryption-algorithm",
+ Value: "A256GCM",
+ Usage: `The cryptographic content encryption algorithm used to perform authenticated
+encryption on the plaintext payload (the content) to produce ciphertext and
+the authentication tag.
+
+ CONTENT_ENC_ALGORITHM is a case-sensitive string and must be one of:
+
+ A128CBC-HS256
+ AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm
+ A192CBC-HS384
+ AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm
+ A256CBC-HS512
+ AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm
+ A128GCM
+ AES GCM using 128-bit key
+ A192GCM
+ AES GCM using 192-bit key
+ A256GCM (default)
+ AES GCM using 256-bit key`,
+ },
+ cli.StringFlag{
+ Name: "key",
+ Usage: `The JWE recipient's public key. The KEY argument should be the name of a
+file. JWEs can be encrypted for a recipient using a public JWK or a PEM
+encoded public key.`,
+ },
+ cli.StringFlag{
+ Name: "jwks",
+ Usage: `The JWK Set containing the recipient's public key. The JWKS argument should
+be the name of a file. The file contents should be a JWK Set. The '--jwks'
+flag requires the use of the '--kid' flag to specify which key to use.`,
+ },
+ cli.StringFlag{
+ Name: "kid",
+ Usage: `The ID of the recipient's public key. KID is a case-sensitive string. When
+used with '--key' the KID value must match the "kid" member of the JWK. When
+used with '--jwks' (a JWK Set) the KID value must match the "kid" member of
+one of the JWKs in the JWK Set.`,
+ },
+ cli.StringFlag{
+ Name: "typ, type",
+ Usage: `The media type of the JWE, used for disambiguation in applications where
+more than one type of JWE may be processed. While this parameter might be
+useful to applications, it is ignored by JWE implementations.`,
+ },
+ cli.StringFlag{
+ Name: "cty, content-type",
+ Usage: `The media type of the JWE payload, used for disambiguation of JWE objects in
+applications where more than one JWE payload type may be present. This
+parameter is ignored by JWE implementations, but may be processed by
+applications that use JWE.`,
+ },
+ cli.BoolFlag{
+ Name: "subtle",
+ Hidden: true,
+ },
+ },
+ }
+}
+
+func encryptAction(ctx *cli.Context) error {
+ data, err := utils.ReadAll(os.Stdin)
+ if err != nil {
+ return err
+ }
+
+ // Validate parameters
+ alg, err := getRecipientAlg(ctx, ctx.String("alg"))
+ if err != nil {
+ return err
+ }
+
+ enc, err := getContentEncryptionAlg(ctx, ctx.String("enc"))
+ if err != nil {
+ return err
+ }
+
+ var isPBES2 bool
+ switch alg {
+ case jose.PBES2_HS256_A128KW, jose.PBES2_HS384_A192KW, jose.PBES2_HS512_A256KW:
+ isPBES2 = true
+ }
+
+ key := ctx.String("key")
+ jwks := ctx.String("jwks")
+ kid := ctx.String("kid")
+ typ := ctx.String("typ")
+ cty := ctx.String("cty")
+ isSubtle := ctx.Bool("subtle")
+
+ switch {
+ case isPBES2 && key != "":
+ return errs.MutuallyExclusiveFlags(ctx, "alg "+ctx.String("alg"), "key")
+ case isPBES2 && jwks != "":
+ return errs.MutuallyExclusiveFlags(ctx, "alg "+ctx.String("alg"), "jwks")
+ case !isPBES2 && key == "" && jwks == "":
+ return errs.RequiredOrFlag(ctx, "key", "jwk")
+ case key != "" && jwks != "":
+ return errs.MutuallyExclusiveFlags(ctx, "key", "jwks")
+ case jwks != "" && kid == "":
+ return errs.RequiredWithFlag(ctx, "kid", "jwks")
+ }
+
+ // Read key from --key, --jwks, or a user provided
+ var pbes2Key []byte
+ var jwk *jose.JSONWebKey
+ switch {
+ case key != "":
+ jwk, err = jose.ParseKey(key, "enc", string(alg), kid, isSubtle)
+ case jwks != "":
+ jwk, err = jose.ParseKeySet(jwks, string(alg), kid, isSubtle)
+ case isPBES2:
+ pbes2Key, err = utils.ReadPassword("Please enter the password to encrypt the content encryption key: ")
+ default:
+ return errs.RequiredOrFlag(ctx, "key", "jwks")
+ }
+ if err != nil {
+ return err
+ }
+
+ var recipient jose.Recipient
+ if isPBES2 {
+ recipient = jose.Recipient{
+ Algorithm: alg,
+ Key: pbes2Key,
+ KeyID: kid,
+ }
+ } else {
+ // Public keys are used for encryption
+ jwkPub := jwk.Public()
+ jwk = &jwkPub
+
+ if jwk.Use == "sig" {
+ return errors.New("invalid jwk use: found 'sig' (signature), expecting 'enc' (encryption)")
+ }
+
+ // Validate jwk
+ if err := jose.ValidateJWK(jwk); err != nil {
+ return err
+ }
+
+ // Prepare recipient
+ if alg == "" {
+ alg, err = getRecipientAlg(ctx, jwk.Algorithm)
+ if err != nil {
+ return err
+ }
+ }
+
+ recipient = jose.Recipient{
+ Algorithm: alg,
+ Key: jwk,
+ KeyID: kid,
+ }
+ }
+
+ // Add extra headers
+ opts := new(jose.EncrypterOptions)
+ if typ != "" {
+ opts.WithType(jose.ContentType(typ))
+ }
+ if cty != "" {
+ opts.WithContentType(jose.ContentType(cty))
+ }
+
+ // Encrypt
+ encrypter, err := jose.NewEncrypter(enc, recipient, opts)
+ if err != nil {
+ return errs.Wrap(err, "error creating cipher")
+ }
+
+ obj, err := encrypter.Encrypt(data)
+ if err != nil {
+ return errs.Wrap(err, "error encrypting data")
+ }
+
+ fmt.Println(obj.FullSerialize())
+ return nil
+}
+
+func getContentEncryptionAlg(ctx *cli.Context, enc string) (jose.ContentEncryption, error) {
+ switch enc {
+ case "":
+ return jose.A256GCM, nil
+ case "A128GCM":
+ return jose.A128GCM, nil
+ case "A192GCM":
+ return jose.A192GCM, nil
+ case "A256GCM":
+ return jose.A256GCM, nil
+ case "A128CBC-HS256":
+ return jose.A128CBC_HS256, nil
+ case "A192CBC-HS384":
+ return jose.A192CBC_HS384, nil
+ case "A256CBC-HS512":
+ return jose.A256CBC_HS512, nil
+ default:
+ return "", errs.InvalidFlagValue(ctx, "enc", enc, "")
+ }
+}
+
+func getRecipientAlg(ctx *cli.Context, alg string) (jose.KeyAlgorithm, error) {
+ switch alg {
+ case "":
+ return "", nil
+ case "RSA1_5":
+ return jose.RSA1_5, nil
+ case "RSA-OAEP":
+ return jose.RSA_OAEP, nil
+ case "RSA-OAEP-256":
+ return jose.RSA_OAEP_256, nil
+ case "A128KW":
+ return jose.A128KW, nil
+ case "A192KW":
+ return jose.A192KW, nil
+ case "A256KW":
+ return jose.A256KW, nil
+ case "DIRECT":
+ return jose.DIRECT, nil
+ case "ECDH-ES":
+ return jose.ECDH_ES, nil
+ case "ECDH-ES+A128KW":
+ return jose.ECDH_ES_A128KW, nil
+ case "ECDH-ES+A192KW":
+ return jose.ECDH_ES_A192KW, nil
+ case "ECDH-ES+A256KW":
+ return jose.ECDH_ES_A256KW, nil
+ case "A128GCMKW":
+ return jose.A128GCMKW, nil
+ case "A192GCMKW":
+ return jose.A192GCMKW, nil
+ case "A256GCMKW":
+ return jose.A256GCMKW, nil
+ case "PBES2-HS256+A128KW":
+ return jose.PBES2_HS256_A128KW, nil
+ case "PBES2-HS384+A192KW":
+ return jose.PBES2_HS384_A192KW, nil
+ case "PBES2-HS512+A256KW":
+ return jose.PBES2_HS512_A256KW, nil
+ default:
+ return "", errs.InvalidFlagValue(ctx, "alg", alg, "")
+ }
+}
diff --git a/command/crypto/jwe/jwe.go b/command/crypto/jwe/jwe.go
new file mode 100644
index 00000000..3aa85c8c
--- /dev/null
+++ b/command/crypto/jwe/jwe.go
@@ -0,0 +1,96 @@
+package jwe
+
+import "github.com/urfave/cli"
+
+// Command returns the jwe subcommand.
+func Command() cli.Command {
+ return cli.Command{
+ Name: "jwe",
+ Usage: "encrypt and decrypt data and keys using JSON Web Encryption (JWE)",
+ UsageText: "step crypto jwe [arguments] [global-flags] [subcommand-flags]",
+ Description: `The 'step crypto jwe' command group provides facilities for encrypting and
+decrypting content and representing encrypted content using JSON-based data
+structures as defined by the JSON Web Encryption (JWE) specification in
+RFC7516, using algorithms defined in the JSON Web Algorithms (JWA)
+specification in RFC7518. A JWE is a data structure representing an encrypted
+and integrity-protected message.
+
+ There are two JWE serializations: the compact serialization is a small, URL-
+safe representation that base64 encodes the JWE components. The compact
+serialization is a URL-safe string, suitable for space-constrained
+environments such as HTTP headers and URI query parameters. The JSON
+serialization represents JWEs as JSON objects and allows the same content to
+be encrypted to multiple parties (using multiple keys).
+
+ A typical JWE in compact serialization is a dot-separated string with five
+parts:
+ * Header: metadata describing how the plaintext payload was processed to
+ produce ciphertext (e.g., which algorithms were used to encrypt the
+ content encryption key and the plaintext payload)
+ * Encrypted Key: the "content encryption key" that was used to encrypt the
+ plaintext payload, encrypted for the JWE recipient(s) (see: "what's with
+ the encrypted key" below)
+ * Initialization Vector: an initialization vector for use with the specified
+ encryption algorithm, if applicable
+ * Ciphertext: the ciphertext value resulting produced from authenticated
+ encryption of the plaintext with additional authenticated data
+ * Authentication Tag: value resulting fromthe authenticated encryption of
+ the plaintext with additional authenticated data
+
+## What's with encrypted key?
+
+ This is somewhat confusing. Instead of directly encrypting the plaintext
+payload, JWE typically generates a new "content encryption key" then encrypts
+*that key* for the intended recipient(s). Todo: y tho?
+
+ While versatile, JWE is easy to use incorrectly. Therefore, any use of this
+subcommand requires the use of the '--subtle' flag as a misuse prevention
+mechanism. You should only use this subcommand if you know what you're doing.
+If possible, you're better off using the higher level 'crypto nacl' command
+group.
+
+EXAMPLES
+
+ This example demonstrates how to produce a JWE for a recipient using the
+RSA-OAEP algorithm to encrypt the content encryption key (producing the
+encrypted key), and the A256GCM (AES GCM with 256-bit key) algorithm to
+produce the ciphertext and authentication tag.
+
+ 1. Encode the JWE header with the desired "alg" and "enc" members then
+ encode it producing the *header*
+ BASE64URL(UTF8({"alg":"RSA-OAEP","enc":"A256GCM"}))
+ => eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ
+ 2. Generate a random content encryption key (CEK), encrypt it using
+ RSA-OAEP, producing the *encrypted key*
+ 3. Generate a random initialization vector
+ 4. Perform authenticated encryption over the plaintext using the content
+ encryption key and A256GCM algorithm with the base64-encoded JWE headers
+ provided as additional authenticated data producing the *ciphertext* and
+ *authentication tag*
+ 5. Assemble the final result (compact serialization) to produce the string:
+
+ BASE64URL(UTF8(header)) || '.'
+ || BASE64URL(encrypted key) || '.'
+ || BASE64URL(initialization vector) || '.'
+ || BASE64URL(ciphertext) || '.'
+ || BASE64URL(authentication tag)
+
+ Producing a result like (line breaks for display purposes only):
+
+ eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.
+ OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe
+ ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb
+ Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV
+ mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8
+ 1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi
+ 6UklfCpIMfIjf7iGdXKHzg.
+ 48V1_ALb6US04U3b.
+ 5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6ji
+ SdiwkIr3ajwQzaBtQD_A.
+ XFBoMYUZodetZdvTiFvSkQ`,
+ Subcommands: cli.Commands{
+ encryptCommand(),
+ decryptCommand(),
+ },
+ }
+}
diff --git a/command/crypto/jwk/create.go b/command/crypto/jwk/create.go
new file mode 100644
index 00000000..c8df99bb
--- /dev/null
+++ b/command/crypto/jwk/create.go
@@ -0,0 +1,619 @@
+package jwk
+
+import (
+ "bytes"
+ gocrypto "crypto"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/crypto"
+ "github.com/smallstep/cli/command/crypto/internal/jose"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/urfave/cli"
+)
+
+const (
+ // 128-bit salt
+ pbkdf2SaltSize = 16
+ // 100k iterations. Nist recommends at least 10k, 1Passsword uses 100k.
+ pbkdf2Iterations = 100000
+)
+
+func createCommand() cli.Command {
+ return cli.Command{
+ Name: "create",
+ Action: cli.ActionFunc(createAction),
+ Usage: "create a JWK (JSON Web Key)",
+ UsageText: `**step crypto jwk create**
+ [**--kty**=] [**--alg**=] [**--use**=]
+ [**--size**=] [**--crv**=] [**--kid**=]
+ [**--from-pem**=]`,
+ Description: `**step crypto jwk create** generates a new JWK (JSON Web Key) or constructs a
+JWK from an existing key. The generated JWK conforms to RFC7517 and can be used
+to sign and encrypt data using JWT, JWS, and JWE.
+
+Files containing private keys are encrypted by default. You'll be prompted for
+a password. Keys are written with file mode **0600** (i.e., readable and
+writable only by the current user).
+
+All flags are optional. Defaults are suitable for most use cases.
+
+## POSITIONAL ARGUMENTS
+
+
+: Path to which the the public JWK should be written
+
+
+: Path to which the (JWE encrypted) private JWK should be written
+
+## EXIT CODES
+
+This command returns 0 on success and \>0 if any error occurs.
+
+## SECURITY CONSIDERATIONS
+
+All security considerations from **step help crypto** are relevant here.
+
+**Preventing hostile disclosure of non-public key material**
+
+: It is critical that any private and symmetric key material be protected from
+ unauthorized disclosure or modification. This includes the private key for
+ asymmetric key types (RSA, EC, and OKP) and the shared secret for symmetric key
+ types (oct). One means of protection is encryption. Keys can also be stored in
+ hardware or software "security enclaves" such as HSMs and TPMs or operating
+ system keychain management tools.
+
+**Key provenance and bindings**
+
+: Key provenance should always be scrutinized. You should not trust a key that
+ was obtained in an untrustworthy manner (e.g., non-TLS HTTP).
+
+: Usually applications use keys to make authorization decisions based on
+ attributes "bound" to the key such as the key owner's name or role. In these
+ scenarios the strength of the system's security depends on the strength of
+ these "bindings". There are a variety of mechanisms for securely binding
+ attributes to keys, including:
+
+ * Cryptographically binding attributes to the public key using x509
+ certificates (e.g., as defined in PKIX / RFC2580)
+ * Cryptographically binding attributes to the public key using JWTs
+ * Storing the public key or (hashed) shared secret along with the bound
+ attributes in a secure database
+
+: Cryptographic mechanisms require establishing a "root of trust" that can sign
+ the bindings (the certificates or JWTs) asserting that the bound attributes are
+ correct.
+
+## STANDARDS
+
+[RFC7517]
+: Jones, M., "JSON Web Key (JWK)", https://tools.ietf.org/html/rfc7517
+
+[RFC7518]
+: Jones, M., "JSON Web Algorithms (JWA)", https://tools.ietf.org/html/rfc7518
+
+[RFC7638]
+: M. Jones, N. Sakimura., "JSON Web Key (JWK) Thumbprint",
+ https://tools.ietf.org/html/rfc7638
+
+[RFC8037]
+: I. Liusvaara., "CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in
+ JSON Object Signing and Encryption (JOSE)",
+ https://tools.ietf.org/html/rfc8037
+
+## EXAMPLES
+
+Create a new JWK using default options:
+
+'''
+$ step crypto jwk create jwk.pub.json jwk.json
+'''
+
+Create an RSA JWK:
+
+'''
+$ step crypto jwk create rsa.pub.json rsa.json --kty RSA
+'''
+
+Create a symmetric key (oct key type):
+
+'''
+$ step crypto jwk create oct.pub.json oct.json --kty oct
+'''
+
+Create a key for use with the Ed25519 cryptosystem:
+
+'''
+$ step crypto jwk create ed.pub.json ed.json \
+ --kty OKP --crv Ed25519
+'''
+
+Create a key from an existing PEM file:
+
+'''
+$ step crypto jwk create jwk.pub.json jwk.json
+ --from-pem key.pem
+'''
+
+Create an 4096 bit RSA encryption key:
+
+'''
+$ step crypto jwk create rsa-enc.pub.json rsa-enc.json \
+ --kty RSA --size 4096 --use enc
+'''
+
+Create a 192 bit symmetric encryption key for use with AES Key Wrap:
+
+'''
+$ step crypto jwk create kw.pub.json kw.json \
+ --kty oct --size 192 --use enc --alg A192GCMKW
+'''
+`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "kty, type",
+ Value: "EC",
+ Usage: `The of key to create. Corresponds to the **"kty"** JWK parameter.
+If unset, default is EC.
+
+: is a case-sensitive string and must be one of:
+
+ **EC**
+ : Create an **elliptic curve** keypair
+
+ **oct**
+ : Create a **symmetric key** (octet stream)
+
+ **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
+`,
+ },
+ cli.StringFlag{
+ Name: "alg, algorithm",
+ Usage: `The intended for use with this key. Corresponds to the
+**"alg"** JWK parameter. is case-sensitive. If unset, the default
+depends on the key use, key type, and curve (for EC and OKP keys). Defaults are:
+
+: | key type | use | curve | default algorithm |
+ |----------|-----|---------|-------------------|
+ | EC | sig | P-256 | ES256 |
+ | EC | sig | P-384 | ES384 |
+ | EC | sig | P-521 | ES512 |
+ | oct | sig | N/A | HS256 |
+ | RSA | sig | N/A | RS256 |
+ | OKP | sig | Ed25519 | EdDSA |
+ | EC | enc | P-256 | ECDH-ES |
+ | EC | enc | P-384 | ECDH-ES |
+ | EC | enc | P-521 | ECDH-ES |
+ | oct | enc | N/A | A256GCMKW |
+ | RSA | enc | N/A | RSA-OAP-256 |
+
+: If the key **"use"** is **"sig"** (signing) must be one of:
+
+ **HS256**
+ : HMAC using SHA-256
+
+ **HS384**
+ : HMAC using SHA-384
+
+ **HS512**
+ : HMAC using SHA-512
+
+ **RS256**
+ : RSASSA-PKCS1-v1_5 using SHA-256
+
+ **RS384**
+ : RSASSA-PKCS1-v1_5 using SHA-384
+
+ **RS512**
+ : RSASSA-PKCS1-v1_5 using SHA-512
+
+ **ES256**
+ : ECDSA using P-256 and SHA-256
+
+ **ES384**
+ : ECDSA using P-384 and SHA-384
+
+ **ES512**
+ : ECDSA using P-521 and SHA-512
+
+ **PS256**
+ : RSASSA-PSS using SHA-256 and MGF1 with SHA-256
+
+ **PS384**
+ : RSASSA-PSS using SHA-384 and MGF1 with SHA-384
+
+ **PS512**
+ : RSASSA-PSS using SHA-512 and MGF1 with SHA-512
+
+ **EdDSA**
+ : EdDSA signature algorithm
+
+: If the key **"use"** is **"enc"** (encryption) must be one of:
+
+ **RSA1_5**
+ : RSAES-PKCS1-v1_5
+
+ **RSA-OAEP**
+ : RSAES OAEP using default parameters
+
+ **RSA-OAEP-256**
+ : RSAES OAEP using SHA-256 and MGF1 with SHA-256
+
+ **A128KW**
+ : AES Key Wrap with default initial value using 128-bit key
+
+ **A192KW**
+ : AES Key Wrap with default initial value using 192-bit key
+
+ **A256KW**
+ : AES Key Wrap with default initial value using 256-bit key
+
+ **dir**
+ : Direct use of a shared symmetric key as the content encryption key (CEK)
+
+ **ECDH-ES**
+ : Elliptic Curve Diffie-Hellman Ephemeral Static key agreement
+
+ **ECDH-ES+A128KW**
+ : ECDH-ES using Concat KDF and CEK wrapped with "A128KW"
+
+ **ECDH-ES+A192KW**
+ : ECDH-ES using Concat KDF and CEK wrapped with "A192KW"
+
+ **ECDH-ES+A256KW**
+ : ECDH-ES using Concat KDF and CEK wrapped with "A256KW"
+
+ **A128GCMKW**
+ : Key wrapping with AES GCM using 128-bit key
+
+ **A192GCMKW**
+ : Key wrapping with AES GCM using 192-bit key
+
+ **A256GCMKW**
+ : Key wrapping with AES GCM using 256-bit key
+
+ **PBES2-HS256+A128KW**
+ : PBES2 with HMAC SHA-256 and "A128KW" wrapping
+
+ **PBES2-HS384+A192KW**
+ : PBES2 with HMAC SHA-256 and "A192KW" wrapping
+
+ **PBES2-HS512+A256KW**
+ : PBES2 with HMAC SHA-256 and "A256KW" wrapping`,
+ },
+ cli.StringFlag{
+ Name: "use",
+ Value: "sig",
+ Usage: `The intended of the public key. Corresponds to the "use" JWK parameter.
+The "use" parameter indicates whether the public key is used for encrypting
+data or verifying the signature on data.
+
+: is a case-sensitive string and may be one of:
+
+ **sig**
+ : The public key is used for verifying signatures.
+
+ **enc**
+ : The public key is used for encrypting data.
+
+: Other values may be used but the generated JWKs will not work for signing or
+encryption with this tool.`,
+ },
+ cli.StringFlag{
+ Name: "kid",
+ Usage: `The (key ID) for this JWK. Corresponds to the
+"kid" JWK parameter. Used to identify an individual key in a JWK Set, for
+example. is a case-sensitive string. If unset, the JWK Thumbprint
+[RFC7638] is used as . See **step help crypto jwk thumbprint** for more
+information on JWK Thumbprints.`,
+ },
+ cli.StringFlag{
+ Name: "key-ops",
+ Hidden: true, // Not currently implemented
+ Usage: `The operation(s) for which the key is intended to be used. Corresponds to
+the "key_ops" JWK parameter. The '--key-ops' flag can be used multiple times
+to indicate multiple intended operations.
+
+ can be one of the values defined in RFC7517:
+ sign
+ Compute digital signature or MAC
+ verify
+ Verify digital signature or MAC
+ encrypt
+ Encrypt content
+ decrypt
+ Decrypt content and validate decryption, if applicable
+ wrapKey
+ Encrypt key
+ unwrapKey
+ Decrypt key and validate decryption, if applicable
+ deriveKey
+ Derive key
+ deriveBits
+ Derive bits not to be used as a key
+
+ The key operation values are case-sensitive strings. Other values may be
+used, but values must not be duplicated.
+
+ The '--use' and '--key-ops' flags cannot be used together without also
+passing the '--subtle' flag. The '--subtle' flag allows both flags to be used
+in a consistent way (e.g., '--key-ops=encrypt --key-ops=decrypt --use=enc').
+Multiple unrelated operations (e.g., '--key-ops=encrypt --key-ops=sign') or
+inconsistent combinations of '--use' and '--key-ops' (e.g., '--use=enc
+--key-ops=sign') are not allowed without also passing the '--insecure' flag
+because of potential vulnerabilities associated with using the same key with
+multiple algorithms.
+
+ Related operations include:
+ sign + verify
+ encrypt + decrypt
+ wrapKey + unwrapKey
+ If multiple values are passed and at least one is a non-standard value the
+'--subtle' flag is required as you must verify that the operations are
+related.`,
+ },
+ cli.StringSliceFlag{
+ Name: "from-certificate",
+ Usage: `TODO: usage is missing.`,
+ Hidden: true,
+ },
+ cli.StringFlag{
+ Name: "from-pem",
+ Usage: `Create a JWK representing the key encoded in an
+existing instead of creating a new key.`,
+ },
+ cli.BoolFlag{
+ Name: "no-password",
+ Usage: `Do not ask for a password to encrypt the JWK. Sensitive
+key material will be written to disk unencrypted. This is not
+recommended. Requires **--insecure** flag.`,
+ },
+ cli.BoolFlag{
+ Name: "subtle",
+ Hidden: true,
+ },
+ cli.BoolFlag{
+ Name: "insecure",
+ Hidden: true,
+ },
+ },
+ }
+}
+
+func createAction(ctx *cli.Context) error {
+ switch ctx.NArg() {
+ case 0:
+ return errors.New("missing positional arguments 'PUB_FILE' 'PRIV_FILE'")
+ case 1:
+ return errors.New("missing positional argument 'PRIV_FILE'")
+ case 2: // ok
+ default:
+ return errors.New("too many positional arguments use only 'PUB_FILE' 'PRIV_FILE'")
+ }
+
+ // Use password to protect private JWK by default
+ usePassword := true
+ if ctx.Bool("no-password") {
+ if ctx.Bool("insecure") {
+ usePassword = false
+ } else {
+ return errors.New("flag '--no-password' requires the '--insecure' flag")
+ }
+ }
+
+ pubFile := ctx.Args().Get(0)
+ privFile := ctx.Args().Get(1)
+ if pubFile == privFile {
+ return errors.New("positional arguments 'PUB_FILE' 'PRIV_FILE' cannot be equal")
+ }
+
+ kty := ctx.String("kty")
+ crv := ctx.String("crv")
+ alg := ctx.String("alg")
+ use := ctx.String("use")
+ kid := ctx.String("kid")
+ size := ctx.Int("size")
+ pemFile := ctx.String("from-pem")
+
+ switch kty {
+ case "EC":
+ if ctx.IsSet("size") {
+ return errors.New("flag '--size' is incompatible with '--kty EC'")
+ }
+ case "RSA":
+ if ctx.IsSet("crv") {
+ return errors.New("flag '--crv' is incompatible with '--kty RSA'")
+ }
+ // If size is not set it will use a safe default
+ if ctx.IsSet("size") {
+ if size < 2048 && !ctx.Bool("insecure") {
+ return errors.New("minimum '--size' for RSA keys is 2048 bits without '--insecure' flag")
+ }
+ if size <= 0 {
+ return errors.New("flag '--size' must be >= 0")
+ }
+ }
+ case "OKP":
+ if ctx.IsSet("size") {
+ return errors.New("flag '--size' is incompatible with '--kty OKP'")
+ }
+ case "oct":
+ if ctx.IsSet("crv") {
+ return errors.New("flag '--crv' is incompatible with '--kty oct'")
+ }
+ // If size is not set it will use a safe default
+ if ctx.IsSet("size") {
+ if size < 16 && !ctx.Bool("insecure") {
+ return errors.New("minimum '--size' for oct keys is 16 bytes without '--insecure' flag")
+ }
+ if size <= 0 {
+ return errors.New("flag '--size' must be >= 0")
+ }
+ }
+ default:
+ return errors.New("missing or invalid value for flag '--kty'")
+ }
+
+ // Generate or read secrets
+ var err error
+ var jwk *jose.JSONWebKey
+ switch {
+ case pemFile != "":
+ jwk, err = jose.GenerateJWKFromPEM(pemFile)
+ default:
+ jwk, err = jose.GenerateJWK(kty, crv, alg, use, kid, size)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ if ctx.IsSet("kid") {
+ jwk.KeyID = ctx.String("kid")
+ } else {
+ // A hash of a symmetric key can leak information, so we only thumbprint asymmetric keys.
+ if kty != "oct" {
+ hash, err := jwk.Thumbprint(gocrypto.SHA256)
+ if err != nil {
+ return errors.Wrap(err, "error generating JWK thumbprint")
+ }
+ jwk.KeyID = base64.RawURLEncoding.EncodeToString(hash)
+ }
+ }
+ jwk.Use = use
+
+ if jwk.Algorithm == "" {
+ jwk.Algorithm = alg
+ }
+
+ if err := jose.ValidateJWK(jwk); err != nil {
+ return err
+ }
+
+ // Add x5c (X.509 Certificate Chain) parameter
+ crtFiles := ctx.StringSlice("from-certificate")
+ for _, name := range crtFiles {
+ crt, err := crypto.ReadCertificate(name)
+ if err != nil {
+ return err
+ }
+ jwk.Certificates = append(jwk.Certificates, crt)
+ }
+
+ var jwkPub jose.JSONWebKey
+ if jose.IsSymmetric(jwk) {
+ jwkPub = *jwk
+ } else {
+ jwkPub = jwk.Public()
+ }
+
+ // Create and write public JWK
+ b, err := json.MarshalIndent(jwkPub, "", " ")
+ if err != nil {
+ return errors.Wrap(err, "error marshaling JWK")
+ }
+ if err := utils.WriteFile(pubFile, b, 0600); err != nil {
+ return errors.Wrap(err, "error creating JWK")
+ }
+
+ if jwk.IsPublic() {
+ fmt.Fprintln(os.Stderr, "Only the public JWK was generated.")
+ fmt.Fprintln(os.Stderr, "Cannot retrieve a private key from a public one.")
+ return nil
+ }
+
+ // Create and write private JWK
+ if usePassword {
+ var rcpt jose.Recipient
+ // Generate JWE encryption key.
+ if jose.SupportsPBKDF2 {
+ key, err := utils.ReadPassword("Please enter the password to encrypt the private JWK: ")
+ if err != nil {
+ return errors.Wrap(err, "error reading password")
+ }
+
+ salt, err := crypto.GetRandomSalt(pbkdf2SaltSize)
+ if err != nil {
+ return err
+ }
+
+ rcpt = jose.Recipient{
+ Algorithm: jose.PBES2_HS256_A128KW,
+ Key: []byte(key),
+ P2C: pbkdf2Iterations,
+ P2S: salt,
+ }
+ } else {
+ key, err := utils.RandAlphanumeric(32)
+ if err != nil {
+ return errors.Wrap(err, "error generating password")
+ }
+ fmt.Printf("Private JWK file '%s' will be encrypted with the key:\n%s\n", privFile, key)
+ rcpt = jose.Recipient{Algorithm: jose.A128KW, Key: []byte(key)}
+ }
+
+ b, err = json.Marshal(jwk)
+ if err != nil {
+ return errors.Wrap(err, "error marshaling JWK")
+ }
+
+ encrypter, err := jose.NewEncrypter(jose.A128GCM, rcpt, nil)
+ if err != nil {
+ return errors.Wrap(err, "error creating cipher")
+ }
+
+ 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 {
+ return errors.Wrap(err, "error formatting JSON")
+ }
+ b = out.Bytes()
+ } else {
+ b, err = json.MarshalIndent(jwk, "", " ")
+ if err != nil {
+ return errors.Wrap(err, "error marshaling JWK")
+ }
+ }
+ if err := utils.WriteFile(privFile, b, 0600); err != nil {
+ return errors.Wrap(err, "error creating JWK")
+ }
+
+ return nil
+}
diff --git a/command/crypto/jwk/jwk.go b/command/crypto/jwk/jwk.go
new file mode 100644
index 00000000..59a98c10
--- /dev/null
+++ b/command/crypto/jwk/jwk.go
@@ -0,0 +1,34 @@
+package jwk
+
+import "github.com/urfave/cli"
+
+// Command returns the jwk subcommand.
+func Command() cli.Command {
+ return cli.Command{
+ Name: "jwk",
+ Usage: "create JWKs (JSON Web Keys) and manage JWK Key Sets",
+ UsageText: "step crypto jwk SUBCOMMAND [ARGUMENTS] [GLOBAL_FLAGS] [SUBCOMMAND_FLAGS]",
+ Description: `The **step crypto jwk** command group provides facilities for creating JWKs
+(JSON Web Keys) as defined in RFC7517. It also includes command line utilities
+for managing Key Sets and working with encrypted keys.
+
+ A JWK is a JSON data structure that represents a cryptographic key. The
+members of this data structure represent properties of the key, including its
+value. A JWK Set is a simple data structure for representing a set of JWKs. A
+JWK Set is a JSON object with a "keys" member whose value is an array of JWKs.
+Cryptographic algorithms and identifiers for used by JWKs are defined by the
+JSON Web Algorithms (JWA) specification in RFC7518. This tool also supports
+extensions defined in standards track RFC8037 defining curve and algorithm
+identifiers for Edwards-curve Digial Signatures.
+
+ JWKs and JWK Sets are used in the JSON Web Signature (JWS; RFC7515) and JSON
+Web Encryption (JWE; RFC7516) specifications for signing and encrypting JSON
+data, respectively.`,
+ Subcommands: cli.Commands{
+ createCommand(),
+ keysetCommand(),
+ publicCommand(),
+ thumbprintCommand(),
+ },
+ }
+}
diff --git a/command/crypto/jwk/keyset.go b/command/crypto/jwk/keyset.go
new file mode 100644
index 00000000..30210183
--- /dev/null
+++ b/command/crypto/jwk/keyset.go
@@ -0,0 +1,302 @@
+package jwk
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "syscall"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/jose"
+ "github.com/urfave/cli"
+)
+
+func keysetCommand() cli.Command {
+ return cli.Command{
+ Name: "keyset",
+ Usage: "add, remove, and find JWKs in JWK Sets",
+ UsageText: "step crypto jwk set COMMAND [ARGUMENTS] [GLOBAL_FLAGS] [SUBCOMMAND_FLAGS]",
+ Description: `The 'step crypto jwk set' command group provides facilities for managing and
+inspecting JWK Sets. A is a JSON object that represents a set of JWKs. They
+are defined in RFC7517.
+
+ A JWK Set is simply a JSON object with a "keys" member whose value is an
+array of JWKs. Additional members are allowed in the object. They will be
+preserved by this tool, but otherwise ignored. Duplicate member names are not
+allowed.`,
+ Subcommands: cli.Commands{
+ keysetAddCommand(),
+ keysetRemoveCommand(),
+ keysetListCommand(),
+ keysetFindCommand(),
+ },
+ }
+}
+
+func keysetAddCommand() cli.Command {
+ return cli.Command{
+ Name: "add",
+ Action: cli.ActionFunc(keysetAddAction),
+ Usage: "a JWK to a JWK Set",
+ UsageText: "step crypto jwk set add JWKS_FILE",
+ Description: `The 'step crypto jwk set add' command reads a JWK from STDIN and
+adds it to the JWK Set in JWKS_FILE. Modifications to JWKS_FILE are in-place.
+The file is 'flock'd while it's being read and modified.
+
+POSITIONAL ARGUMENTS:
+
+ JWKS_FILE
+ File containing a JWK Set`,
+ }
+}
+
+func keysetRemoveCommand() cli.Command {
+ return cli.Command{
+ Name: "remove",
+ Action: cli.ActionFunc(keysetRemoveAction),
+ Usage: "a JWK from a JWK Set",
+ UsageText: "step crypto jwk set remove JWKS_FILE --kid [KID]",
+ Description: `The 'step crypto jwk set remove' command removes the JWK with a key ID
+matching KID from the JWK Set stored in JWKS_FILE. Modifications to JWKS_FILE
+are in-place. The file is 'flock'd while it's being read and modified.
+
+POSITONAL_ARGUMENTS:
+
+ JWKS_FILE
+ File containing a JWK Set`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "kid",
+ Usage: `The key ID of the JWK to remove from the JWK Set. KID is a case-sensitive
+string.`,
+ },
+ },
+ }
+}
+
+func keysetListCommand() cli.Command {
+ return cli.Command{
+ Name: "list",
+ Action: cli.ActionFunc(keysetListAction),
+ Usage: "key IDs of JWKs in a JWK Set",
+ UsageText: "step crypto jwk set list JWKS_FILE",
+ Description: `The 'step crypto jwk set list' command lists the IDs ("kid" parameters) of
+JWKs in a JWK Set.
+
+POSITONAL_ARGUMENTS:
+
+ JWKS_FILE
+ File containing a JWK Set`,
+ }
+}
+
+func keysetFindCommand() cli.Command {
+ return cli.Command{
+ Name: "find",
+ Action: cli.ActionFunc(keysetFindAction),
+ Usage: "a JWK in a JWK Set",
+ UsageText: "step crypto jwk set find JWKS_FILE --kid [KID]",
+ Description: `The 'step crypto jwk set find' command locates the JWK with a key ID
+matching KID from the JWK Set stored in JWKS_FILE. The matching JWK is printed
+to STDOUT.
+
+POSITONAL_ARGUMENTS:
+
+ JWKS_FILE
+ File containing a JWK Set`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "kid",
+ Usage: `The key ID of the JWK to remove from the JWK Set. KID is a case-sensitive
+string.`,
+ },
+ },
+ }
+}
+
+func keysetAddAction(ctx *cli.Context) error {
+ if ctx.NArg() != 1 {
+ return errors.Errorf("not enough positional arguments, use '%s'", ctx.Command.UsageText)
+ }
+
+ b, err := ioutil.ReadAll(os.Stdin)
+ if err != nil {
+ return errors.Wrap(err, "error reading STDIN")
+ }
+
+ // Attempt to parse an encrypted file
+ if b, err = jose.Decrypt("Please enter the password to decrypt JWK: ", b); err != nil {
+ return err
+ }
+
+ // Unmarshal the plain (or decrypted JWK)
+ var jwk jose.JSONWebKey
+ if err := json.Unmarshal(b, &jwk); err != nil {
+ return errors.New("error reading JWK: unsupported format")
+ }
+
+ jwksFile := ctx.Args().Get(0)
+ jwks, writeFunc, err := rwLockKeySet(jwksFile)
+ if err != nil {
+ return err
+ }
+
+ // According to RFC7517 there are cases where multiple keys can share the
+ // same "kid". One example is if they have different "kty" values but are
+ // considered to be equivalent alternatives by the application using them.
+ jwks.Keys = append(jwks.Keys, jwk)
+ return writeFunc(true)
+}
+
+func keysetRemoveAction(ctx *cli.Context) error {
+ if ctx.NArg() != 1 {
+ return errors.Errorf("not enough positional arguments, use '%s'", ctx.Command.UsageText)
+ }
+
+ kid := ctx.String("kid")
+
+ jwksFile := ctx.Args().Get(0)
+ jwks, writeFunc, err := rwLockKeySet(jwksFile)
+ if err != nil {
+ return err
+ }
+
+ // Filtering without allocating
+ keys := jwks.Keys[:0]
+ for _, key := range jwks.Keys {
+ if key.KeyID != kid {
+ keys = append(keys, key)
+ }
+ }
+ jwks.Keys = keys
+ return writeFunc(true)
+}
+
+func keysetListAction(ctx *cli.Context) error {
+ if ctx.NArg() != 1 {
+ return errors.Errorf("not enough positional arguments, use '%s'", ctx.Command.UsageText)
+ }
+
+ jwksFile := ctx.Args().Get(0)
+ jwks, writeFunc, err := rwLockKeySet(jwksFile)
+ if err != nil {
+ return err
+ }
+
+ for _, key := range jwks.Keys {
+ fmt.Println(key.KeyID)
+ }
+
+ return writeFunc(false)
+}
+
+func keysetFindAction(ctx *cli.Context) error {
+ if ctx.NArg() != 1 {
+ return errors.Errorf("not enough positional arguments, use '%s'", ctx.Command.UsageText)
+ }
+
+ kid := ctx.String("kid")
+
+ jwksFile := ctx.Args().Get(0)
+ jwks, writeFunc, err := rwLockKeySet(jwksFile)
+ if err != nil {
+ return err
+ }
+
+ for _, key := range jwks.Keys {
+ if key.KeyID == kid {
+ b, err := json.MarshalIndent(key, "", " ")
+ if err != nil {
+ return errors.Wrap(err, "error marshaling JWK")
+ }
+ fmt.Println(string(b))
+ }
+ }
+
+ return writeFunc(false)
+}
+
+func rwLockKeySet(filename string) (jwks *jose.JSONWebKeySet, writeFunc func(bool) error, err error) {
+ var f *os.File
+
+ f, err = os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600)
+ if err != nil {
+ err = errors.Wrapf(err, "error reading %s", filename)
+ return
+ }
+
+ fd := int(f.Fd())
+
+ // non-blocking exclusive lock
+ err = syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB)
+ switch err {
+ case nil: // continue
+ case syscall.EWOULDBLOCK:
+ f.Close()
+ err = errors.Errorf("%s is locked", filename)
+ return
+ default:
+ f.Close()
+ err = errors.Wrapf(err, "error locking %s", filename)
+ return
+ }
+
+ // close and unlock file on errors
+ defer func() {
+ if err != nil {
+ syscall.Flock(fd, syscall.LOCK_UN)
+ f.Close()
+ }
+ }()
+
+ // Read key set
+ var b []byte
+ b, err = ioutil.ReadAll(f)
+ if err != nil {
+ err = errors.Wrapf(err, "error reading %s", filename)
+ return
+ }
+
+ // Unmarshal the plain JWKSet
+ jwks = new(jose.JSONWebKeySet)
+ if len(b) > 0 {
+ if err = json.Unmarshal(b, jwks); err != nil {
+ err = errors.Wrapf(err, "error reading %s", filename)
+ return
+ }
+ }
+
+ writeFunc = func(write bool) (err error) {
+ if write {
+ if b, err1 := json.MarshalIndent(jwks, "", " "); err1 != nil {
+ err = errors.Wrapf(err1, "error marshaling %s", filename)
+ } else {
+ if err1 := f.Truncate(0); err1 != nil {
+ err = errors.Wrapf(err1, "error writing %s", filename)
+ } else {
+ n, err1 := f.WriteAt(b, 0)
+ switch {
+ case err1 != nil:
+ err = errors.Wrapf(err1, "error writing %s", filename)
+ case n < len(b):
+ err = errors.Wrapf(io.ErrShortWrite, "error writing %s", filename)
+ }
+ }
+ }
+ }
+
+ if err1 := syscall.Flock(fd, syscall.LOCK_UN); err1 != nil {
+ err = errors.Wrapf(err1, "error unlocking %s", filename)
+ }
+
+ if err1 := f.Close(); err1 != nil {
+ err = errors.Wrapf(err1, "error closing %s", filename)
+ }
+
+ return err
+ }
+
+ return
+}
diff --git a/command/crypto/jwk/public.go b/command/crypto/jwk/public.go
new file mode 100644
index 00000000..b947c1eb
--- /dev/null
+++ b/command/crypto/jwk/public.go
@@ -0,0 +1,50 @@
+package jwk
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+
+ "github.com/smallstep/cli/command/crypto/internal/jose"
+
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+)
+
+func publicCommand() cli.Command {
+ return cli.Command{
+ Name: "public",
+ Action: cli.ActionFunc(publicAction),
+ Usage: "extract a public JSON Web Key (JWK) from a private JWK",
+ UsageText: `step crypto jwk public`,
+ Description: `The 'step crypto jwk public' command reads a JWK from STDIN, derives
+the corresponding public JWK, and prints the derived JWK to STDOUT.`,
+ }
+}
+
+func publicAction(ctx *cli.Context) error {
+ b, err := ioutil.ReadAll(os.Stdin)
+ if err != nil {
+ return errors.Wrap(err, "error reading from STDIN")
+ }
+
+ jwk := new(jose.JSONWebKey)
+ // Attempt to decrypt if encrypted
+ if b, err = jose.Decrypt("Please enter the password to decrypt your private JWK: ", b); err != nil {
+ return err
+ }
+
+ // Unmarshal the plain (or decrypted JWK)
+ if err := json.Unmarshal(b, jwk); err != nil {
+ return errors.New("error reading JWK: unsupported format")
+ }
+
+ b, err = json.MarshalIndent(jwk.Public(), "", " ")
+ if err != nil {
+ return errors.Wrap(err, "error marshaling JWK")
+ }
+
+ fmt.Println(string(b))
+ return nil
+}
diff --git a/command/crypto/jwk/thumbprint.go b/command/crypto/jwk/thumbprint.go
new file mode 100644
index 00000000..f478a2de
--- /dev/null
+++ b/command/crypto/jwk/thumbprint.go
@@ -0,0 +1,51 @@
+package jwk
+
+import (
+ "crypto"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/jose"
+ "github.com/urfave/cli"
+)
+
+func thumbprintCommand() cli.Command {
+ return cli.Command{
+ Name: "thumbprint",
+ Action: cli.ActionFunc(thumbprintAction),
+ Usage: "compute thumbprint for a JWK",
+ UsageText: `step crypto jwk thumbprint`,
+ Description: `The 'step crypto jwk thumbprint' command reads a JWK from STDINT, derives the
+corresponding JWK Thumbprint (RFC7638), and prints the base64-urlencoded
+thumbprint to STDOUT.`,
+ }
+}
+
+func thumbprintAction(ctx *cli.Context) error {
+ b, err := ioutil.ReadAll(os.Stdin)
+ if err != nil {
+ return errors.Wrap(err, "error reading from STDIN")
+ }
+
+ jwk := new(jose.JSONWebKey)
+ // Attempt to decrypt if encrypted
+ if b, err = jose.Decrypt("Please enter the password to decrypt your private JWK: ", b); err != nil {
+ return err
+ }
+
+ // Unmarshal the plain (or decrypted JWK)
+ if err := json.Unmarshal(b, jwk); err != nil {
+ return errors.New("error reading JWK: unsupported format")
+ }
+
+ hash, err := jwk.Thumbprint(crypto.SHA256)
+ if err != nil {
+ return errors.Wrap(err, "error generating JWK thumbprint")
+ }
+ fmt.Println(base64.RawURLEncoding.EncodeToString(hash))
+ return nil
+}
diff --git a/command/crypto/jwt/inspect.go b/command/crypto/jwt/inspect.go
new file mode 100644
index 00000000..496c00e3
--- /dev/null
+++ b/command/crypto/jwt/inspect.go
@@ -0,0 +1,76 @@
+package jwt
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+func inspectCommand() cli.Command {
+ return cli.Command{
+ Name: "inspect",
+ Action: cli.ActionFunc(inspectAction),
+ Usage: `return the decoded JWT without verification`,
+ UsageText: `step crypto jwt inspect --insecure`,
+ Description: `The 'step crypto jwt inspect' command reads a JWT data structure from STDIN,
+decodes it, and outputs the header and payload on STDERR. Since this command
+does not verify the JWT you must pass '--insecure' as a misuse prevention
+mechanism.`,
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "insecure",
+ Hidden: true,
+ },
+ },
+ }
+}
+
+func inspectAction(ctx *cli.Context) error {
+ token, err := utils.ReadString(os.Stdin)
+ if err != nil {
+ return err
+ }
+
+ if !ctx.Bool("insecure") {
+ return errs.InsecureCommand(ctx)
+ }
+
+ return printToken(token)
+}
+
+func printToken(token string) error {
+ parts := strings.Split(token, ".")
+ if len(parts) != 3 {
+ return errors.New("error decoding token: JWT must have three parts")
+ }
+
+ header, err := base64.RawURLEncoding.DecodeString(parts[0])
+ if err != nil {
+ return errors.Wrapf(err, "error decoding token")
+ }
+
+ payload, err := base64.RawURLEncoding.DecodeString(parts[1])
+ if err != nil {
+ return errors.Wrapf(err, "error decoding token")
+ }
+
+ m := make(map[string]json.RawMessage)
+ m["header"] = header
+ m["payload"] = payload
+ m["signature"] = []byte(`"` + parts[2] + `"`)
+
+ b, err := json.MarshalIndent(m, "", " ")
+ if err != nil {
+ return errors.Wrapf(err, "error marshaling token data")
+ }
+ fmt.Fprintln(os.Stderr, string(b))
+
+ return nil
+}
diff --git a/command/crypto/jwt/jwt.go b/command/crypto/jwt/jwt.go
new file mode 100644
index 00000000..c161930f
--- /dev/null
+++ b/command/crypto/jwt/jwt.go
@@ -0,0 +1,37 @@
+package jwt
+
+import (
+ "github.com/urfave/cli"
+)
+
+// Command returns the cli.Command for jwt and related subcommands.
+func Command() cli.Command {
+ return cli.Command{
+ Name: "jwt",
+ Usage: "sign and verify data using JSON Web Tokens (JWT)",
+ UsageText: "step crypto jwt SUBCOMMAND [SUBCOMMAND_ARGUMENTS] [GLOBAL_FLAGS] [SUBCOMMAND_FLAGS]",
+ Description: `A JSON Web Token or JWT (pronounced "jot") is a compact data structure used
+to represent some JSON encoded "claims" that are passed as the payload of a
+JWS or JWE structure, enabling the claims to be digitally signed and/or
+encrypted. The "claims" (or "claim set") are represented as an ordinary JSON
+object. JWTs are represented using a compact format that's URL safe and can be
+used in space-constrained environments. JWTs can be passed in HTTP
+Authorization headers and as URI query parameters.
+
+ A "claim" is a piece of information asserted about a subject, represented as
+a key/value pair. Logically a verified JWT can be interpreted as "ISSUER says
+to AUDIENCE that SUBJECT's CLAIM_NAME is CLAIM_VALUE" for each claim.
+
+ A JWT signed using JWS has three parts:
+ 1. A base64 encoded JSON object representing the JOSE (JSON Object Signing
+ and Encryption) header that describes the cryptographic operations
+ applied to the JWT Claims Set
+ 2. A base64 encoded JSON object representing the JWT Claims Set
+ 3. A base64 encoded digital signature of message authentication code`,
+ Subcommands: cli.Commands{
+ signCommand(),
+ verifyCommand(),
+ inspectCommand(),
+ },
+ }
+}
diff --git a/command/crypto/jwt/sign.go b/command/crypto/jwt/sign.go
new file mode 100644
index 00000000..3303f0e2
--- /dev/null
+++ b/command/crypto/jwt/sign.go
@@ -0,0 +1,340 @@
+package jwt
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "time"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/jose"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+func signCommand() cli.Command {
+ return cli.Command{
+ Name: "sign",
+ Action: cli.ActionFunc(signAction),
+ Usage: "create a signed JWT data structure",
+ UsageText: `step crypto jwt sign [- | FILENAME] [--alg ALGORITHM] [--aud AUDIENCE] [--iss ISSUER] [--sub SUB]
+ [--exp EXPIRATION] [--iat ISSUED_AT] [--nbf NOT_BEFORE] [--key JWK]
+ [--jwks JWKS] [--kid KID] [--jti JTI]`,
+ Description: `The 'step crypto jwt sign' command generates a signed JSON Web Token (JWT)
+by computing a digital signature or message authentication code for a JSON
+payload. By default, the payload to sign is read from STDIN and the JWT will
+be written to STDOUT. The suggested pronunciation of JWT is the same as the
+English word "jot".
+
+ A JWT is a compact data structure used to represent some JSON encoded
+"claims" that are passed as the payload of a JWS or JWE structure, enabling
+the claims to be digitally signed and/or encrypted. The "claims" (or "claim
+set") are represented as an ordinary JSON object. JWTs are represented using a
+compact format that's URL safe and can be used in space-constrained
+environments. JWTs can be passed in HTTP Authorization headers and as URI
+query parameters.
+
+ A "claim" is a piece of information asserted about a subject, represented as
+a key/value pair. Logically a verified JWT should be interpreted as "ISSUER
+says to AUDIENCE that SUBJECT's CLAIM_NAME is CLAIM_VALUE" for each claim.
+
+ Some optional arguments introduce subtle security considerations if omitted.
+These considerations should be carefully analyzed. Therefore, omitting SUBTLE
+arguments requires the use of the '--subtle' flag as a misuse prevention
+mechanism.
+
+ A JWT signed using JWS has three parts:
+ 1. A base64 encoded JSON object representing the JOSE (JSON Object Signing
+ and Encryption) header that describes the cryptographic operations
+ applied to the JWT Claims Set
+ 2. A base64 encoded JSON object representing the JWT Claims Set
+ 3. A base64 encoded digital signature of message authentication code`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "alg, algorithm",
+ Usage: `The signature or MAC algorithm to use. Algorithms are case-sensitive strings
+defined in RFC7518. The selected algorithm must be compatible with the key
+type. This flag is optional. If not specified, the "alg" member of the JWK is
+used. If the JWK has no "alg" member then a default is selected depending on
+the JWK key type. If the JWK has an "alg" member and the "alg" flag is passed
+the two options must match unless the '--subtle' flag is also passed.
+
+ ALGORITHM is a case-sensitive string and must be one of:
+
+ HS256
+ HMAC using SHA-256 (default for "oct" key type)
+ HS384
+ HMAC using SHA-384
+ HS512
+ HMAC using SHA-512
+ RS256
+ RSASSA-PKCS1-v1_5 using SHA-256 (default for "RSA" key type)
+ RS384
+ RSASSA-PKCS1-v1_5 using SHA-384
+ RS512
+ RSASSA-PKCS1-v1_5 using SHA-512
+ ES256
+ ECDSA using P-256 and SHA-256 (default for "EC" key type)
+ ES384
+ ECDSA using P-384 and SHA-384
+ ES512
+ ECDSA using P-521 and SHA-512
+ PS256
+ RSASSA-PSS using SHA-256 and MGF1 with SHA-256
+ PS384
+ RSASSA-PSS using SHA-384 and MGF1 with SHA-384
+ PS512
+ RSASSA-PSS using SHA-512 and MGF1 with SHA-512
+ EdDSA
+ EdDSA signature algorithm`,
+ },
+ cli.StringFlag{
+ Name: "iss, issuer",
+ Usage: `The issuer of this JWT. The processing of this claim is generally
+application specific. Typically, the ISSUER must match the name of some
+trusted entity (e.g., an identity provider like "https://accounts.google.com")
+and identify which key(s) to use for JWT verification and/or decryption (e.g.,
+the keys at "https://www.googleapis.com/oauth2/v3/certs"). ISSUER is a
+case-sensitive string.`,
+ },
+ cli.StringSliceFlag{
+ Name: "aud, audience",
+ Usage: `The intended recipient(s) of the JWT, encoded as the "aud" claim in the JWT.
+Recipient(s) must identify themselves with one or more of the values in the
+"aud" claim. The "aud" claim can be a string (indicating a single recipient)
+or an array (indicating multiple potential recipients). This flag can be used
+multiple times to generate a JWK with multiple intended recipients. Each
+AUDIENCE is a case-sensitive string.`,
+ },
+ cli.StringFlag{
+ Name: "sub, subject",
+ Usage: `The subject of this JWT. The "claims" are normally interpreted as statements
+about this subject. The subject must either be locally unique in the context
+of the issuer or globally unique. The processing of this claim is generally
+application specific. SUBJECT is a case-sensitive string.`,
+ },
+ cli.Int64Flag{
+ Name: "exp, expiration",
+ Usage: `The expiration time on or after which the JWT must not be accepted. EXPIRATION
+must be a numeric value representing a Unix timestamp.`,
+ },
+ cli.Int64Flag{
+ Name: "nbf, not-before",
+ Usage: `The time before which the JWT must not be accepted. NOT_BEFORE must be a
+numeric value representing a Unix timestamp. If not provided, the current time
+is used.`,
+ },
+ cli.Int64Flag{
+ Name: "iat, issued-at",
+ Usage: `The time at which the JWT was issued, used to determine the age of the JWT.
+ISSUED_AT must be a numeric value representing a Unix timestamp. If not
+provided, the current time is used.`,
+ },
+ cli.StringFlag{
+ Name: "jti, jwt-id",
+ Usage: `A unique identifier for the JWT. The identifier must be assigned in a manner
+that ensures that there is a negligible probability that the same value will
+be accidentally assigned to multiple JWTs. The JTI claim can be used to
+prevent a JWT from being replayed (i.e., recipient(s) can use JTI to make a
+JWT one-time-use). The JTI argument is a case-sensitive string. If the '--jti'
+flag is used without an argument a JTI will be generated randomly with
+sufficient entropy to satisfy the collision-resistance criteria.`,
+ },
+ cli.StringFlag{
+ Name: "key",
+ Usage: `The key to use to sign the JWT. The KEY argument should be the name of a file.
+JWTs can be signed using a private JWK (or a JWK encrypted as a JWE payload)
+or a PEM encoded private key (or a private key encrypted using [TODO: insert
+private key encryption mechanism]).`,
+ },
+ cli.StringFlag{
+ Name: "jwks",
+ Usage: `The JWK Set containing the key to use to sign the JWT. The JWKS argument
+should be the name of a file. The file contents should be a JWK Set or a JWE
+with a JWK Set payload. The '--jwks' flag requires the use of the '--kid' flag
+to specify which key to use.`,
+ },
+ cli.StringFlag{
+ Name: "kid",
+ Usage: `The ID of the key used to sign the JWT. The KID argument is a case-sensitive
+string. When used with '--jwk' the KID value must match the
+"kid" member of the JWK. When used with '--jwks' (a JWK Set) the KID value must
+match the "kid" member of one of the JWKs in the JWK Set.`,
+ },
+ cli.BoolFlag{
+ Name: "subtle",
+ Hidden: true,
+ },
+ cli.BoolFlag{
+ Name: "no-kid",
+ Hidden: true,
+ },
+ },
+ }
+}
+
+func signAction(ctx *cli.Context) error {
+ var err error
+ var payload interface{}
+
+ // Read payload if provided
+ args := ctx.Args()
+ switch len(args) {
+ case 0:
+ // empty extra payload
+ payload = make(map[string]interface{})
+ case 1:
+ // read payload from file or stdin (-)
+ if payload, err = readPayload(args[0]); err != nil {
+ return err
+ }
+ default:
+ return errors.Errorf("unknown arguments %v", args[1:])
+ }
+
+ isSubtle := ctx.Bool("subtle")
+ alg := ctx.String("alg")
+
+ // Validate key, jwks and kid
+ key := ctx.String("key")
+ jwks := ctx.String("jwks")
+ kid := ctx.String("kid")
+ switch {
+ case key == "" && jwks == "":
+ return errs.RequiredOrFlag(ctx, "key", "jwks")
+ case key != "" && jwks != "":
+ return errs.MutuallyExclusiveFlags(ctx, "key", "jwks")
+ case jwks != "" && kid == "":
+ return errs.RequiredWithFlag(ctx, "kid", "jwks")
+ }
+
+ // Read key from --key or --jwks
+ var jwk *jose.JSONWebKey
+ switch {
+ case key != "":
+ jwk, err = jose.ParseKey(key, "sig", alg, kid, isSubtle)
+ case jwks != "":
+ jwk, err = jose.ParseKeySet(jwks, alg, kid, isSubtle)
+ default:
+ return errs.RequiredOrFlag(ctx, "key", "jwks")
+ }
+ if err != nil {
+ return err
+ }
+
+ // Public keys cannot be used for signing
+ if jwk.IsPublic() {
+ return errors.New("cannot use a public key for signing")
+ }
+
+ // Key "use" must be "sig" to use for signing
+ if jwk.Use != "sig" && jwk.Use != "" {
+ return errors.Errorf("invalid jwk use: found '%s', expecting 'sig' (signature)", jwk.Use)
+ }
+
+ // At this moment jwk.Algorithm should have an alg from:
+ // * alg parameter
+ // * jwk or jwkset
+ // * guessed for ecdsa and Ed25519 keys
+ if jwk.Algorithm == "" {
+ return errors.New("flag '--alg' is required with the given key")
+ }
+ if err := jose.ValidateJWK(jwk); err != nil {
+ return err
+ }
+
+ // Add claims
+ c := &jose.Claims{
+ Issuer: ctx.String("iss"),
+ Subject: ctx.String("sub"),
+ Audience: ctx.StringSlice("aud"),
+ Expiry: jose.NumericDate(ctx.Int64("exp")),
+ NotBefore: jose.NumericDate(ctx.Int64("nbf")),
+ IssuedAt: jose.NumericDate(ctx.Int64("iat")),
+ ID: ctx.String("jti"),
+ }
+ now := time.Now()
+ if c.NotBefore == 0 {
+ c.NotBefore = jose.NewNumericDate(now)
+ }
+ if c.IssuedAt == 0 {
+ c.IssuedAt = jose.NewNumericDate(now)
+ }
+ if c.ID == "" && ctx.IsSet("jti") {
+ if c.ID, err = utils.RandHex(40); err != nil {
+ return errors.Wrap(err, "error creating random jti")
+ }
+ }
+
+ // Validate recommended claims
+ if !isSubtle {
+ switch {
+ case len(c.Issuer) == 0:
+ return errors.New("flag '--iss' is required unless '--subtle' is used")
+ case len(c.Audience) == 0:
+ return errors.New("flag '--aud' is required unless '--subtle' is used")
+ case len(c.Subject) == 0:
+ return errors.New("flag '--sub' is required unless '--subtle' is used")
+ case c.Expiry == 0:
+ return errors.New("flag '--exp' is required unless '--subtle' is used")
+ case c.Expiry.Time().Before(time.Now()):
+ return errors.New("flag '--exp' must be in the future unless '--subtle' is used")
+ }
+ }
+
+ // Sign
+ so := new(jose.SignerOptions)
+ so.WithType("JWT")
+ if !ctx.Bool("no-kid") && jwk.KeyID != "" {
+ so.WithHeader("kid", jwk.KeyID)
+ }
+
+ signer, err := jose.NewSigner(jose.SigningKey{
+ Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
+ Key: jwk.Key,
+ }, so)
+ if err != nil {
+ return errors.Wrapf(err, "error creating JWT signer")
+ }
+
+ // Some implementations only accept "aud" as a string.
+ // Using claim overwriting for this special case.
+ aud := make(map[string]interface{})
+ if len(c.Audience) == 1 {
+ aud["aud"] = c.Audience[0]
+ }
+
+ raw, err := jose.Signed(signer).Claims(c).Claims(aud).Claims(payload).CompactSerialize()
+ if err != nil {
+ return errors.Wrapf(err, "error serializing JWT")
+ }
+
+ fmt.Println(raw)
+ return nil
+}
+
+func readPayload(filename string) (interface{}, error) {
+ var r io.Reader
+ if filename == "-" {
+ r = os.Stdin
+ } else {
+ b, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, errs.FileError(err, filename)
+ }
+ r = bytes.NewReader(b)
+ }
+
+ v := make(map[string]interface{})
+ if err := json.NewDecoder(r).Decode(&v); err != nil {
+ if filename == "-" {
+ return nil, errors.Wrap(err, "error decoding JSON from STDIN")
+ }
+ return nil, errors.Wrapf(err, "error decoding JSON from %s", filename)
+ }
+ return v, nil
+}
diff --git a/command/crypto/jwt/verify.go b/command/crypto/jwt/verify.go
new file mode 100644
index 00000000..bffa9db9
--- /dev/null
+++ b/command/crypto/jwt/verify.go
@@ -0,0 +1,231 @@
+package jwt
+
+import (
+ "os"
+ "strings"
+ "time"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/jose"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+func verifyCommand() cli.Command {
+ return cli.Command{
+ Name: "verify",
+ Action: cli.ActionFunc(verifyAction),
+ Usage: "verify a signed JWT data structure and return the payload",
+ Description: `The 'step crypto jwt verify' command reads a JWT data structure from STDIN;
+checks that the audience, issuer, and algorithm are in agreement with
+expectations; verifies the digital signature or message authentication code as
+appropriate; and outputs the decoded payload of the JWT on STDOUT. If
+verification fails a non-zero failure code is returned. If verification
+succeeds the command returns 0.
+
+ For a JWT to be verified successfully:
+ * The JWT must be well formed (no errors during deserialization)
+ * The ALGORITHM must match the "alg" member in the JWT header
+ * The ISSUER and AUIENCE must match the "iss" and "aud" claims in the JWT,
+ respectively
+ * The KID must match the "kid" member in the JWT header (if both are
+ present) and must match the "kid" in the JWK or the "kid" of one of the
+ JWKs in JWKS
+ * The JWT signature must be successfully verified`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "iss, issuer",
+ Usage: `The issuer of this JWT. The ISSUER must match the value of the "iss" claim in
+the JWT. ISSUER is a case-sensitive string.`,
+ },
+ cli.StringFlag{
+ Name: "aud, audience",
+ Usage: `The identity of the principal running this command. The AUDIENCE specified
+must match one of the values in the "aud" claim, indicating the intended
+recipient(s) of the JWT. AUDIENCE is a case-sensitive string.`,
+ },
+ cli.StringFlag{
+ Name: "alg, algorithm",
+ Usage: `The signature or MAC algorithm to use. Algorithms are case-sensitive strings
+defined in RFC7518. If the KEY used do verify the JWT is not a JWK, or if it
+is a JWK but does not have an "alg" member indicating its the intended
+algorithm for use with the key, then the '--alg' flag is required to prevent
+algorithm downgrade attacks. To disable this protection you can pass the
+'--insecure' flag and omit the '--alg' flag.`,
+ },
+ cli.StringFlag{
+ Name: "key",
+ Usage: `The key to use to verify the JWS. The KEY argument should be the name of a
+file. The contents of the file can be a public or private JWK (or a JWK
+encrypted as a JWE payload) or a public or private PEM (or a private key
+encrypted using.`,
+ },
+ cli.StringFlag{
+ Name: "jwks",
+ Usage: `The JWK Set containing the key to use to verify the JWS. The JWKS argument
+should be the name of a file. The file contents should be a JWK Set or a JWE
+with a JWK Set payload. The JWS being verified should have a "kid" member that
+matches the "kid" of one of the JWKs in the JWK Set. If the JWS does not have
+a "kid" member the '--kid' flag can be used.`,
+ },
+ cli.StringFlag{
+ Name: "kid",
+ Usage: `The ID of the key used to sign the JWK, used to select a JWK from a JWK Set.
+The KID argument is a case-sensitive string. If the input JWS has a "kid"
+member its value must match KID or verification will fail.`,
+ },
+ cli.BoolFlag{
+ Name: "subtle",
+ Hidden: true,
+ },
+ cli.BoolFlag{
+ Name: "no-exp-check",
+ Hidden: true,
+ },
+ cli.BoolFlag{
+ Name: "insecure",
+ Hidden: true,
+ },
+ },
+ }
+}
+
+// Get the public key for a JWK.
+func publicKey(jwk *jose.JSONWebKey) interface{} {
+ if jose.IsSymmetric(jwk) {
+ return jwk.Key
+ }
+ return jwk.Public().Key
+}
+
+func verifyAction(ctx *cli.Context) error {
+ token, err := utils.ReadString(os.Stdin)
+ if err != nil {
+ return errors.Wrap(err, "error reading token")
+ }
+
+ tok, err := jose.ParseSigned(token)
+ if err != nil {
+ return errors.Errorf("error parsing token: %s", strings.TrimPrefix(err.Error(), "square/go-jose: "))
+ }
+
+ // Validate key, jwks and kid
+ key := ctx.String("key")
+ jwks := ctx.String("jwks")
+ kid := ctx.String("kid")
+ alg := ctx.String("alg")
+ switch {
+ case key == "" && jwks == "":
+ return errs.RequiredOrFlag(ctx, "key", "jwks")
+ case key != "" && jwks != "":
+ return errs.MutuallyExclusiveFlags(ctx, "key", "jwks")
+ case jwks != "" && kid == "":
+ if tok.Headers[0].KeyID == "" {
+ return errs.RequiredWithFlag(ctx, "kid", "jwks")
+ }
+ kid = tok.Headers[0].KeyID
+ }
+
+ // Validate subtled
+ isSubtle := ctx.Bool("subtle")
+ iss := ctx.String("iss")
+ aud := ctx.String("aud")
+ if !isSubtle {
+ switch {
+ case len(iss) == 0:
+ return errs.RequiredSubtleFlag(ctx, "iss")
+ case len(aud) == 0:
+ return errs.RequiredSubtleFlag(ctx, "aud")
+ }
+ }
+
+ // Read key from --key or --jwks
+ var jwk *jose.JSONWebKey
+ switch {
+ case key != "":
+ jwk, err = jose.ParseKey(key, "sig", alg, kid, isSubtle)
+ case jwks != "":
+ jwk, err = jose.ParseKeySet(jwks, alg, kid, isSubtle)
+ default:
+ return errs.RequiredOrFlag(ctx, "key", "jwks")
+ }
+ if err != nil {
+ return err
+ }
+
+ // At this moment jwk.Algorithm should have an alg from:
+ // * alg parameter
+ // * jwk or jwkset
+ // * guessed for ecdsa and ed25519 keys
+ if jwk.Algorithm == "" {
+ return errors.New("flag '--alg' is required with the given key")
+ }
+ if err := jose.ValidateJWK(jwk); err != nil {
+ return err
+ }
+
+ // We don't support multiple signatures or any critical headers
+ if len(tok.Headers) > 1 {
+ return errors.New("validation failed: multiple signatures are not supported")
+ }
+ if _, ok := tok.Headers[0].ExtraHeaders["crit"]; ok {
+ return errors.New("validation failed: unrecognized critical headers (crit)")
+ }
+ if !isSubtle && alg != "" && tok.Headers[0].Algorithm != "" && alg != tok.Headers[0].Algorithm {
+ return errors.Errorf("alg %s does not match the alg on JWT (%s)", alg, tok.Headers[0].Algorithm)
+ }
+
+ claims := jose.Claims{}
+ if err := tok.Claims(publicKey(jwk), &claims); err != nil {
+ switch err {
+ case jose.ErrCryptoFailure:
+ return errors.New("validation failed: invalid signature")
+ default:
+ return errors.Wrap(err, "claim verify failed")
+ }
+ }
+
+ expected := jose.Expected{Issuer: iss}
+ if aud != "" {
+ expected.Audience = jose.Audience{aud}
+ }
+ if !ctx.Bool("no-exp-check") {
+ // TODO: The `go-jose` library makes it hard for us to differentiate
+ // between a JWT that has no "exp" paramater and one that has an "exp"
+ // paramater set to 0. We conflate the two cases here. This is
+ // definitely not correct as an explicit 0 should be rejected.
+ if claims.Expiry == 0 {
+ if !ctx.Bool("subtle") {
+ return errors.New(`jwt must have "exp" property unless '--subtle' is used`)
+ }
+ } else {
+ expected.Time = time.Now()
+ }
+ } else {
+ if !ctx.Bool("insecure") {
+ return errs.RequiredInsecureFlag(ctx, "no-exp-check")
+ }
+ }
+
+ if err := claims.ValidateWithLeeway(expected, 0); err != nil {
+ switch err {
+ case jose.ErrInvalidIssuer:
+ return errors.New("validation failed: invalid issuer claim (iss)")
+ case jose.ErrInvalidAudience:
+ return errors.New("validation failed: invalid audience claim (aud)")
+ case jose.ErrNotValidYet:
+ return errors.New("validation failed: token not valid yet (nbf)")
+ case jose.ErrExpired:
+ return errors.Errorf("validation failed: token is expired by %s (exp)", expected.Time.Sub(claims.Expiry.Time()).Round(time.Millisecond))
+ case jose.ErrInvalidSubject: // we're not currently checking the subject
+ return errors.New("validation failed: invalid subject subject (sub)")
+ case jose.ErrInvalidID: // we're not currently checking the id
+ return errors.New("validation failed: invalid ID claim (jti)")
+ default:
+ return errors.Wrap(err, "validation failed")
+ }
+ }
+
+ return printToken(token)
+}
diff --git a/command/crypto/kdf/kdf.go b/command/crypto/kdf/kdf.go
new file mode 100644
index 00000000..988fb555
--- /dev/null
+++ b/command/crypto/kdf/kdf.go
@@ -0,0 +1,240 @@
+package kdf
+
+import (
+ "crypto/subtle"
+ "fmt"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+
+ "golang.org/x/crypto/bcrypt"
+ "golang.org/x/crypto/scrypt"
+)
+
+// Command returns the cli.Command for kdf and related subcommands.
+func Command() cli.Command {
+ return cli.Command{
+ Name: "kdf",
+ Usage: "key derivation functions for password hashing and verification",
+ UsageText: "step crypto kdf [SUBCOMMAND_FLAGS]",
+ Subcommands: cli.Commands{
+ hashCommand(),
+ compareCommand(),
+ },
+ }
+}
+
+func hashCommand() cli.Command {
+ return cli.Command{
+ Name: "hash",
+ Action: cli.ActionFunc(hashAction),
+ Usage: "derive a secret key from a secret value (e.g., a password)",
+ UsageText: "step crypto kdf hash [INPUT] [--alg ALGORITHM]",
+ Description: `The 'step crypto kdf hash' command uses a key derivation function (KDF) to
+produce a pseudorandom secret key based on some (presumably secret) input
+value. This is useful for password verification approaches based on password
+hashing. Key derivation functions are designed to be computationally
+intensive, making it more difficult for attackers to perform brute-force
+attacks on password databases.
+
+ If this command is run without the optional INPUT argument and STDIN is a
+TTY (i.e., you're running the command in an interactive terminal and not
+piping input to it) you'll be prompted to enter a value on STDERR. If STDIN is
+not a TTY it will be read without prompting.
+
+ This command will produce a string encoding of the KDF output along with the
+algorithm used, salt, and any parameters required for validation in PHC string
+format.
+
+ The KDFs are run with parameters that are considered safe. The 'scrypt'
+parameters are currently fixed at N=32768, r=8 and p=1. The 'bcrypt' work
+factor is currently fixed at 10.
+
+POSITIONAL ARGUMENTS
+
+ INPUT
+ The input to the key derivation function. INPUT is optional and its use is
+not recommended. If this argument is provided the '--insecure' flag must also
+be provided because your (presumably secret) INPUT will likely be logged and
+appear in places you might not expect. If omitted input is read from STDIN.
+ `,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "alg",
+ Value: "scrypt",
+ Usage: `The KDF algorithm to use.
+
+ ALGORITHM must be one of:
+ scrypt
+ A password-based KDF designed to use exponential time and memory.
+ bcrypt
+ A password-based KDF designed to use exponential time.`,
+ },
+ cli.BoolFlag{
+ Name: "insecure",
+ Hidden: true,
+ },
+ },
+ }
+}
+
+func hashAction(ctx *cli.Context) error {
+ var err error
+ var input []byte
+
+ // Get kdf method
+ var kdf func([]byte) (string, error)
+ switch alg := ctx.String("alg"); alg {
+ case "scrypt":
+ kdf = doScrypt
+ case "bcrypt":
+ kdf = doBcrypt
+ default:
+ return errs.InvalidFlagValue(ctx, "alg", alg, "")
+ }
+
+ // Grab input from terminal or arguments
+ switch ctx.NArg() {
+ case 0:
+ input, err = utils.ReadInput("Enter password to hash: ")
+ if err != nil {
+ return err
+ }
+ case 1:
+ if !ctx.Bool("insecure") {
+ return errs.InsecureArgument(ctx, "INPUT")
+ }
+ input = []byte(ctx.Args().Get(0))
+ default:
+ return errs.TooManyArguments(ctx)
+ }
+
+ // Hash input
+ hash, err := kdf(input)
+ if err != nil {
+ return err
+ }
+
+ fmt.Println(hash)
+ return nil
+}
+
+// doScrypt uses scrypt-32768 to derive the given password.
+func doScrypt(password []byte) (string, error) {
+ salt, err := phcGetSalt(16)
+ if err != nil {
+ return "", errors.Wrap(err, "error creating salt")
+ }
+ // use scrypt-32768 by default
+ p := scryptParams[scryptHash32768]
+ hash, err := scrypt.Key(password, salt, p.N, p.r, p.p, p.kl)
+ if err != nil {
+ return "", errors.Wrap(err, "error deriving password")
+ }
+
+ return phcEncode("scrypt", p.getParams(), salt, hash), nil
+}
+
+// doBcrypt uses bcrypt to derive the given password.
+func doBcrypt(password []byte) (string, error) {
+ hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
+ if err != nil {
+ return "", errors.Wrap(err, "error deriving password")
+
+ }
+ return string(hash), nil
+}
+
+func compareCommand() cli.Command {
+ return cli.Command{
+ Name: "compare",
+ Action: cli.ActionFunc(compareAction),
+ Usage: "compare a plaintext value (e.g., a password) and a hash",
+ UsageText: "step crypto kdf compare PHC_HASH [INPUT]",
+ Description: `The 'step crypto kdf compare' command compares a plaintext value (e.g., a
+password) with an existing KDF password hash in PHC string format. The PHC
+string input indicates which KDF algorithm and parameters to use.
+
+ If the input matches PHC_HASH the command prints a human readable message
+indicating success to STDERR and returns 0. If the input does not match an
+error will be printed to STDERR and the command will exit with a non-zero
+return code.
+
+ If this command is run without the optional INPUT argument and STDIN is a
+TTY (i.e., you're running the command in an interactive terminal and not
+piping input to it) you'll be prompted to enter a value on STDERR. If STDIN is
+not a TTY it will be read without prompting.
+
+POSITIONAL ARGUMENTS
+
+ INPUT
+ The plaintext value to compare with PHC_HASH. INPUT is optional and its
+use is not recommended. If this argument is provided the '--insecure' flag
+must also be provided because your (presumably secret) INPUT will likely be
+logged and appear in places you might not expect. If omitted input is read
+from STDIN.`,
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "insecure",
+ Hidden: true,
+ },
+ },
+ }
+}
+
+func compareAction(ctx *cli.Context) error {
+ var err error
+ var hashStr string
+ var input []byte
+
+ switch ctx.NArg() {
+ case 0:
+ return errs.MissingArguments(ctx, "PHC_HASH")
+ case 1:
+ hashStr = ctx.Args().Get(0)
+ input, err = utils.ReadInput("Enter password to compare: ")
+ if err != nil {
+ return err
+ }
+ case 2:
+ if !ctx.Bool("insecure") {
+ return errs.InsecureArgument(ctx, "INPUT")
+ }
+ args := ctx.Args()
+ hashStr, input = args[0], []byte(args[1])
+ default:
+ return errs.TooManyArguments(ctx)
+ }
+
+ id, params, salt, hash, err := phcDecode(hashStr)
+ if err != nil {
+ return errors.Wrap(err, "error decoding hash")
+ }
+
+ var valid bool
+ switch id {
+ case bcryptHash:
+ valid = (bcrypt.CompareHashAndPassword(hash, input) == nil)
+ case scryptHash:
+ p, err := newScryptParams(params)
+ if err != nil {
+ return err
+ }
+ hashedPass, err := scrypt.Key(input, salt, p.N, p.r, p.p, len(hash))
+ if err != nil {
+ return errors.Wrap(err, "error deriving input")
+ }
+ valid = (subtle.ConstantTimeCompare(hash, hashedPass) == 1)
+ default:
+ return errors.Errorf("invalid or unsupported hash method with id '%s'", id)
+ }
+
+ if valid {
+ fmt.Println("ok")
+ return nil
+ }
+
+ return errors.New("fail")
+}
diff --git a/command/crypto/kdf/phc.go b/command/crypto/kdf/phc.go
new file mode 100644
index 00000000..39e7f7c6
--- /dev/null
+++ b/command/crypto/kdf/phc.go
@@ -0,0 +1,106 @@
+package kdf
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "io"
+ "strconv"
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+// phcEncoding is the alphabet used to encode/decode the hashes. It's based on
+// the PHC string format:
+//
+// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
+var phcEncoding = base64.RawStdEncoding
+
+// phcGetSalt is a helper that returns a random slice of n bytes.
+func phcGetSalt(n int) ([]byte, error) {
+ salt := make([]byte, n)
+ if _, err := io.ReadFull(rand.Reader, salt); err != nil {
+ return nil, err
+ }
+ return salt, nil
+}
+
+// phcAtoi returns the number in the string value or n if value is empty.
+func phcAtoi(value string, n int) (int, error) {
+ if value == "" {
+ return n, nil
+ }
+ return strconv.Atoi(value)
+}
+
+// phcParamsToMap parses the parameters in the string s and returns them in a
+// map of keys and values.
+func phcParamsToMap(s string) map[string]string {
+ parameters := strings.Split(s, ",")
+ m := make(map[string]string, len(parameters))
+ for _, p := range parameters {
+ subs := strings.SplitN(p, "=", 2)
+ if len(subs) == 2 {
+ m[subs[0]] = subs[1]
+ } else {
+ m[subs[0]] = ""
+ }
+ }
+ return m
+}
+
+// phcEncode creates a string using the PHC format.
+func phcEncode(identifier, params string, salt, hash []byte) string {
+ ret := "$" + identifier
+ if len(params) > 0 {
+ ret += "$" + params
+ }
+ if len(salt) > 0 {
+ ret += "$" + phcEncoding.EncodeToString(salt)
+ }
+ if len(hash) > 0 {
+ ret += "$" + phcEncoding.EncodeToString(hash)
+ }
+ return ret
+}
+
+// phcDecode returns the different parts of a PHC encoded string.
+func phcDecode(s string) (id string, params string, salt []byte, hash []byte, err error) {
+ subs := strings.SplitN(s, "$", 5)
+ if subs[0] != "" || len(subs) < 2 || (subs[1] == bcryptHash && len(subs) != 4) {
+ return "", "", nil, nil, errors.Errorf("cannot decode password hash %s", s)
+ }
+
+ // Special case for bcrypt
+ // return just the id and the full hash
+ if subs[1] == bcryptHash {
+ return bcryptHash, "", nil, []byte(s), nil
+ }
+
+ switch len(subs) {
+ case 5: // id + params + salt + hash
+ if hash, err = phcEncoding.DecodeString(subs[4]); err != nil {
+ return "", "", nil, nil, err
+ }
+ if salt, err = phcEncoding.DecodeString(subs[3]); err != nil {
+ return "", "", nil, nil, err
+ }
+ id, params = subs[1], subs[2]
+ case 4: // id + salt + hash
+ if hash, err = phcEncoding.DecodeString(subs[3]); err != nil {
+ return "", "", nil, nil, err
+ }
+ if salt, err = phcEncoding.DecodeString(subs[2]); err != nil {
+ return "", "", nil, nil, err
+ }
+ id = subs[1]
+ case 3: // id + params
+ id, params = subs[1], subs[2]
+ case 2: // id
+ id = subs[1]
+ default:
+ return "", "", nil, nil, errors.Errorf("cannot decode password hash %s", s)
+ }
+
+ return
+}
diff --git a/command/crypto/kdf/scrypt.go b/command/crypto/kdf/scrypt.go
new file mode 100644
index 00000000..3d13bb2b
--- /dev/null
+++ b/command/crypto/kdf/scrypt.go
@@ -0,0 +1,62 @@
+package kdf
+
+import (
+ "fmt"
+ "math"
+
+ "github.com/pkg/errors"
+)
+
+const (
+ bcryptHash = "2a"
+ scryptHash = "scrypt"
+ scryptHash16384 = "scrypt-16384"
+ scryptHash32768 = "scrypt-32768"
+ scryptHash65536 = "scrypt-65536"
+)
+
+var scryptParams = map[string]scryptParam{
+ scryptHash16384: {16384, 8, 1, 32},
+ scryptHash32768: {32768, 8, 1, 32},
+ scryptHash65536: {65536, 8, 1, 32},
+}
+
+type scryptParam struct {
+ N, r, p int
+ kl int
+}
+
+func newScryptParams(s string) (*scryptParam, error) {
+ sp := new(scryptParam)
+ params := phcParamsToMap(s)
+
+ if ln, err := phcAtoi(params["ln"], 16); err != nil {
+ return nil, err
+ } else if ln < 1 {
+ return nil, errors.Errorf("invalid scrypt parameter ln=%s", params["ln"])
+ } else {
+ sp.N = int(math.Pow(2, float64(ln)))
+ }
+
+ if r, err := phcAtoi(params["r"], 8); err != nil {
+ return nil, err
+ } else if r < 1 {
+ return nil, errors.Errorf("invalid scrypt parameter r=%s", params["r"])
+ } else {
+ sp.r = r
+ }
+
+ if p, err := phcAtoi(params["p"], 1); err != nil {
+ return nil, err
+ } else if p < 1 {
+ return nil, errors.Errorf("invalid scrypt parameter p=%s", params["p"])
+ } else {
+ sp.p = p
+ }
+
+ return sp, nil
+}
+
+func (s *scryptParam) getParams() string {
+ return fmt.Sprintf("ln=%d,r=%d,p=%d", int(math.Log2(float64(s.N))), s.r, s.p)
+}
diff --git a/command/crypto/keypair.go b/command/crypto/keypair.go
new file mode 100644
index 00000000..ce064c6b
--- /dev/null
+++ b/command/crypto/keypair.go
@@ -0,0 +1,146 @@
+package crypto
+
+import (
+ "fmt"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/smallstep/cli/crypto/keys"
+ "github.com/smallstep/cli/errs"
+ "github.com/smallstep/cli/utils/reader"
+ "github.com/urfave/cli"
+)
+
+func createKeyPairCommand() cli.Command {
+ return cli.Command{
+ Name: "keypair",
+ Action: cli.ActionFunc(createAction),
+ Usage: "generate a public /private keypair in PEM format.",
+ UsageText: `step crypto keypair PUB_FILE PRIV_FILE [--type=TYPE] [--size=SIZE] [--curve=CURVE]`,
+ Description: `The 'step crypto keypair' command generates a raw public / private keypair
+ in PEM format. These keys can be used by other operations to sign
+ and encrypt data, and the public key can be bound to an identity in a CSR and
+ signed by a CA to produce a certificate.
+
+ Private keys are encrypted using a password. You'll be prompted for this password
+ automatically when the key is used.
+
+POSITIONAL ARGUMENTS:
+ PUB_FILE
+ The path to write the public key.
+
+ PRIV_FILE
+ The path to write the private key.`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "type",
+ Value: "EC",
+ Usage: `The type of key to generate.
+
+ TYPE is a case-sensitive string and must be one of:
+ EC
+ Generate an asymmetric Elliptic Curve Key Pair.
+ RSA
+ Generate an asymmetric RSA (Rivest–Shamir–Adleman) Key Pair.
+ OKP
+ Generate an asymmetric Octet Key Pair.`,
+ },
+ cli.IntFlag{
+ Name: "size",
+ Usage: `The size (in bits) of the key for RSA and oct key types. RSA keys require a
+ minimum key size of 2048 bits.`,
+ Value: 2048,
+ },
+ cli.StringFlag{
+ Name: "crv, curve",
+ Value: "P-256",
+ Usage: `The elliptic curve to use for this keypair for EC and OKP key types.
+
+ CURVE is a case-sensitive string and must be one of:
+ P-256
+ NIST P-256 Curve; compatible with 'EC' key type only
+ P-384
+ NIST P-384 Curve; compatible with 'EC' key type only
+ P-521
+ NIST P-521 Curve; compatible with 'EC' key type only
+ Ed25519
+ EdDSA Curve 25519; compatible with 'OKP' key type only`,
+ },
+ cli.BoolFlag{
+ Name: "insecure",
+ Hidden: true,
+ },
+ cli.BoolFlag{
+ Name: "no-password",
+ Usage: `The directive to leave the private key unencrypted. This is not recommended.`,
+ },
+ },
+ }
+}
+
+func createAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 2); err != nil {
+ return err
+ }
+
+ pubFile := ctx.Args().Get(0)
+ privFile := ctx.Args().Get(1)
+ if pubFile == privFile {
+ return errs.EqualArguments(ctx, "PUB_FILE", "PRIV_FILE")
+ }
+
+ typ := ctx.String("type")
+ crv := ctx.String("crv")
+ if ctx.IsSet("crv") {
+ switch typ {
+ case "EC", "OKP":
+ default:
+ return errors.Errorf("key type '%s' is not compatible with flag '--crv'", typ)
+ }
+ } else {
+ switch typ {
+ // If crv not set and the key type is OKP then set cruve Ed25519.
+ // The cli assumes a default curve for EC key type.
+ case "OKP":
+ crv = "Ed25519"
+ }
+ }
+ if ctx.IsSet("size") && typ != "RSA" {
+ return errors.Errorf("key type '%s' is not compatible with flag '--size'", typ)
+ }
+ size := ctx.Int("size")
+ insecure := ctx.Bool("insecure")
+ noPass := ctx.Bool("no-password")
+
+ if noPass && !insecure {
+ return errs.RequiredWithFlag(ctx, "insecure", "no-password")
+ }
+ if size < 2048 && !insecure {
+ return errs.MinSizeInsecureFlag(ctx, "size", "2048")
+ }
+ if size <= 0 {
+ return errs.MinSizeFlag(ctx, "size", "0")
+ }
+
+ pub, priv, err := keys.GenerateKeyPair(typ, crv, size)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ if err := utils.WritePublicKey(pub, pubFile); err != nil {
+ return errors.WithStack(err)
+ }
+
+ var pass string
+ if !noPass {
+ reader.ReadPasswordSubtle(
+ fmt.Sprintf("Password with which to encrypt private key file `%s`: ", privFile),
+ &pass, "Password", reader.RetryOnEmpty)
+
+ }
+ if err := utils.WritePrivateKey(priv, pass, privFile); err != nil {
+ return errors.WithStack(err)
+ }
+
+ return nil
+}
diff --git a/command/crypto/nacl/auth.go b/command/crypto/nacl/auth.go
new file mode 100644
index 00000000..16c987c9
--- /dev/null
+++ b/command/crypto/nacl/auth.go
@@ -0,0 +1,109 @@
+package nacl
+
+import (
+ "encoding/hex"
+ "fmt"
+ "io/ioutil"
+ "os"
+
+ "github.com/smallstep/cli/errs"
+
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "golang.org/x/crypto/nacl/auth"
+
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+)
+
+func authCommand() cli.Command {
+ return cli.Command{
+ Name: "auth",
+ Usage: "authenticates a message using a secret key",
+ UsageText: "step crypto nacl auth SUBCOMMAND [SUBCOMMAND_FLAGS]",
+ Subcommands: cli.Commands{
+ authDigestCommand(),
+ authVerifyCommand(),
+ },
+ }
+}
+
+func authDigestCommand() cli.Command {
+ return cli.Command{
+ Name: "digest",
+ Action: cli.ActionFunc(authDigestAction),
+ Usage: "generates a 32-byte digest for a message",
+ UsageText: "step crypto nacl auth digest KEY_FILE",
+ }
+}
+
+func authVerifyCommand() cli.Command {
+ return cli.Command{
+ Name: "verify",
+ Action: cli.ActionFunc(authVerifyAction),
+ Usage: "checks digest is a valid for a message",
+ UsageText: "step crypto nacl auth verify KEY_FILE DIGEST",
+ }
+}
+
+func authDigestAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 1); err != nil {
+ return err
+ }
+
+ keyFile := ctx.Args().Get(0)
+
+ key, err := ioutil.ReadFile(keyFile)
+ if err != nil {
+ return errs.FileError(err, keyFile)
+ } else if len(key) != auth.KeySize {
+ return errors.Errorf("invalid key file: key size is not %d bytes", auth.KeySize)
+ }
+
+ input, err := utils.ReadAll(os.Stdin)
+ if err != nil {
+ return errs.Wrap(err, "error reading from STDIN")
+ }
+
+ var k [32]byte
+ copy(k[:], key)
+
+ sum := auth.Sum(input, &k)
+ fmt.Println(hex.EncodeToString(sum[:]))
+ return nil
+}
+
+func authVerifyAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 2); err != nil {
+ return err
+ }
+
+ args := ctx.Args()
+ keyFile, digest := args[0], args[1]
+
+ key, err := ioutil.ReadFile(keyFile)
+ if err != nil {
+ return errs.FileError(err, keyFile)
+ } else if len(key) != auth.KeySize {
+ return errors.Errorf("invalid key file: key size is not %d bytes", auth.KeySize)
+ }
+
+ sum, err := hex.DecodeString(digest)
+ if err != nil {
+ return errors.Wrap(err, "error decoding digest")
+ }
+
+ input, err := utils.ReadAll(os.Stdin)
+ if err != nil {
+ return errs.Wrap(err, "error reading from STDIN")
+ }
+
+ var k [32]byte
+ copy(k[:], key)
+
+ if auth.Verify(sum, input, &k) {
+ fmt.Println("ok")
+ return nil
+ }
+
+ return errors.New("fail")
+}
diff --git a/command/crypto/nacl/box.go b/command/crypto/nacl/box.go
new file mode 100644
index 00000000..3e9cc592
--- /dev/null
+++ b/command/crypto/nacl/box.go
@@ -0,0 +1,243 @@
+package nacl
+
+import (
+ "crypto/rand"
+ "fmt"
+ "io/ioutil"
+ "os"
+
+ "github.com/smallstep/cli/errs"
+
+ "golang.org/x/crypto/nacl/box"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/urfave/cli"
+)
+
+func boxCommand() cli.Command {
+ return cli.Command{
+ Name: "box",
+ Usage: "authenticate and encrypt small messages using public-key cryptography",
+ UsageText: "step crypto nacl box SUBCOMMAND [SUBCOMMAND_FLAGS]",
+ Subcommands: cli.Commands{
+ boxKeypairCommand(),
+ boxOpenCommand(),
+ boxSealCommand(),
+ },
+ }
+}
+
+func boxKeypairCommand() cli.Command {
+ return cli.Command{
+ Name: "keypair",
+ Action: cli.ActionFunc(boxKeypairAction),
+ Usage: "generate a key for use with seal and open",
+ UsageText: "step crypto nacl box keypair PUB_FILE PRIV_FILE",
+ Description: `Generates a new public/private keypair suitable for use with seal and open.
+The private key is encrypted using a password in a nacl secretbox.
+
+POSITIONAL ARGUMENTS
+
+ PUB_FILE
+ The path to write the public key.
+
+ PRIV_FILE
+ The path to write the encrypted private key.`,
+ }
+}
+
+func boxKeypairAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 2); err != nil {
+ return err
+ }
+
+ args := ctx.Args()
+ pubFile, privFile := args[0], args[1]
+ if pubFile == privFile {
+ return errs.EqualArguments(ctx, "PUB_FILE", "PRIV_FILE")
+ }
+
+ pub, priv, err := box.GenerateKey(rand.Reader)
+ if err != nil {
+ return errors.Wrap(err, "error generating key")
+ }
+
+ if err := utils.WriteFile(pubFile, pub[:], 0600); err != nil {
+ return errs.FileError(err, pubFile)
+ }
+
+ if err := utils.WriteFile(privFile, priv[:], 0600); err != nil {
+ return errs.FileError(err, privFile)
+ }
+
+ return nil
+}
+
+func boxOpenCommand() cli.Command {
+ return cli.Command{
+ Name: "open",
+ Action: cli.ActionFunc(boxOpenAction),
+ Usage: "authenticate and decrypt a box produced by seal",
+ UsageText: "step crypto nacl box open NONCE SENDER_PUB_KEY PRIV_KEY [--raw]",
+ Description: `Authenticate and decrypt a box produced by seal using the specified KEY. If
+PRIV_KEY is encrypted you will be prompted for the password. The sealed box is
+read from STDIN and the decrypted plaintext is written to STDOUT.
+
+POSITIONAL ARGUMENTS
+
+ NONCE
+ The nonce provided when the box was sealed.
+
+ SENDER_PUB_KEY
+ The path to the public key of the peer that produced the sealed box.
+
+ PRIV_KEY
+ The path to the private key used to open the box.`,
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "raw",
+ Usage: "Indicates that input is not base64 encoded",
+ },
+ },
+ }
+}
+
+func boxOpenAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 3); err != nil {
+ return err
+ }
+
+ args := ctx.Args()
+ nonce, pubFile, privFile := []byte(args[0]), args[1], args[2]
+
+ if len(nonce) > 24 {
+ return errors.New("nonce cannot be longer than 24 bytes")
+ }
+
+ pub, err := ioutil.ReadFile(pubFile)
+ if err != nil {
+ return errs.FileError(err, pubFile)
+ } else if len(pub) != 32 {
+ return errors.New("invalid public key: key size is not 32 bytes")
+ }
+
+ priv, err := ioutil.ReadFile(privFile)
+ if err != nil {
+ return errs.FileError(err, privFile)
+ } else if len(priv) != 32 {
+ return errors.New("invalid private key: key size is not 32 bytes")
+ }
+
+ input, err := utils.ReadAll(os.Stdin)
+ if err != nil {
+ return errs.Wrap(err, "error reading input")
+ }
+
+ var rawInput []byte
+ if ctx.Bool("raw") {
+ rawInput = input
+ } else {
+ // DecodeLen returns the maximum length,
+ // Decode will return the actual length.
+ rawInput = make([]byte, b64Encoder.DecodedLen(len(input)))
+ n, err := b64Encoder.Decode(rawInput, input)
+ if err != nil {
+ return errors.Wrap(err, "error decoding base64 input")
+ }
+ rawInput = rawInput[:n]
+ }
+
+ var n [24]byte
+ var pb, pv [32]byte
+ copy(n[:], nonce)
+ copy(pb[:], pub)
+ copy(pv[:], priv)
+
+ // Fixme: if we prepend the nonce in the seal we can use use rawInput[24:]
+ // as the message and rawInput[:24] as the nonce instead of requiring one.
+ raw, ok := box.Open(nil, rawInput, &n, &pb, &pv)
+ if !ok {
+ return errors.New("error authenticating or decrypting input")
+ }
+
+ os.Stdout.Write(raw)
+ return nil
+}
+
+func boxSealCommand() cli.Command {
+ return cli.Command{
+ Name: "seal",
+ Action: cli.ActionFunc(boxSealAction),
+ Usage: "produce an authenticated and encrypted ciphertext",
+ UsageText: "step crypto nacl box seal NONCE RECIPIENT_PUB_KEY PRIV_KEY [--raw]",
+ Description: `Reads plaintext from STDIN and writes an encrypted and authenticated
+ciphertext to STDOUT. The "box" can be open by the a recipient who has access
+to the private key corresponding to RECIPIENT_PUB_KEY.
+
+POSITIONAL ARGUMENTS
+
+ NONCE
+ Must be unique for each distinct message for a given pair of keys.
+
+ RECIPIENT_PUB_KEY
+ The path to the public key of the intended recipient of the sealed box.
+
+ PRIV_KEY
+ The path to the private key used for authentication.`,
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "raw",
+ Usage: "Do not base64 encode output",
+ },
+ },
+ }
+}
+
+func boxSealAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 3); err != nil {
+ return err
+ }
+
+ args := ctx.Args()
+ nonce, pubFile, privFile := []byte(args[0]), args[1], args[2]
+
+ if len(nonce) > 24 {
+ return errors.New("nonce cannot be longer than 24 bytes")
+ }
+
+ pub, err := ioutil.ReadFile(pubFile)
+ if err != nil {
+ return errs.FileError(err, pubFile)
+ } else if len(pub) != 32 {
+ return errors.New("invalid public key: key size is not 32 bytes")
+ }
+
+ priv, err := ioutil.ReadFile(privFile)
+ if err != nil {
+ return errs.FileError(err, privFile)
+ } else if len(priv) != 32 {
+ return errors.New("invalid private key: key size is not 32 bytes")
+ }
+
+ input, err := utils.ReadInput("Write text to seal: ")
+ if err != nil {
+ return errors.Wrap(err, "error reading input")
+ }
+
+ var n [24]byte
+ var pb, pv [32]byte
+ copy(n[:], nonce)
+ copy(pb[:], pub)
+ copy(pv[:], priv)
+
+ // Fixme: we can prepend nonce[:] so it's not necessary in the open.
+ raw := box.Seal(nil, input, &n, &pb, &pv)
+ if ctx.Bool("raw") {
+ os.Stdout.Write(raw)
+ } else {
+ fmt.Println(b64Encoder.EncodeToString(raw))
+ }
+
+ return nil
+}
diff --git a/command/crypto/nacl/nacl.go b/command/crypto/nacl/nacl.go
new file mode 100644
index 00000000..0399e437
--- /dev/null
+++ b/command/crypto/nacl/nacl.go
@@ -0,0 +1,47 @@
+package nacl
+
+import (
+ "encoding/base64"
+
+ "github.com/urfave/cli"
+)
+
+// Command returns the cli.Command for nacl and related subcommands.
+func Command() cli.Command {
+ return cli.Command{
+ Name: "nacl",
+ Usage: "easy-to-use high-speed tools for encryption and signing",
+ UsageText: "step crypto nacl SUBCOMMAND [SUBCOMMAND_FLAGS]",
+ Description: `The 'step crypto nacl' command group is a thin CLI wrapper around the NaCl
+(pronounced "salt") cryptography library. NaCl's goal is to provide all of the
+core operations needed to build higher-level cryptographic tools.
+
+ Perhaps its biggest advantage is simplicity. NaCl was designed to be easy to
+use and hard to misuse. Typical cryptographic libraries force you to specify
+choices for cryptographic primitives and constructions (e.g., sign this
+message with 4096-bit RSA using PKCS#1 v2.0 with SHA-256). But most people are
+not cryptographers. These choices become foot guns. By contrast, NaCl allows
+you to simply say "sign this message". NaCl ships with a preselected choice --
+a state-of-the-art signature system suitable for most applications -- and it
+has a side mechanism through which a cryptographer can easily override the
+choice of signature system.
+
+ There are language bindings and pure implementations of NaCl for all major
+languages. For internal use cases where compatibility with open standards like
+JWT are not an issue, NaCl should be your default choice for cryptographic
+needs.
+
+ TODO: Are we NaCl or libsodium compliant? Maybe have a flag at this level to
+decide? The golang default is libsodium compatible. I think all that changes
+are the defaults for the 'box' operations -- NaCl doesn't support Curve25519
+yet.`,
+ Subcommands: cli.Commands{
+ authCommand(),
+ boxCommand(),
+ secretboxCommand(),
+ signCommand(),
+ },
+ }
+}
+
+var b64Encoder = base64.RawURLEncoding
diff --git a/command/crypto/nacl/secretbox.go b/command/crypto/nacl/secretbox.go
new file mode 100644
index 00000000..e2388121
--- /dev/null
+++ b/command/crypto/nacl/secretbox.go
@@ -0,0 +1,153 @@
+package nacl
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+
+ "golang.org/x/crypto/nacl/secretbox"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+func secretboxCommand() cli.Command {
+ return cli.Command{
+ Name: "secretbox",
+ Usage: "encrypts and authenticates small messages using secret-key cryptography",
+ UsageText: "step crypto nacl secretbox SUBCOMMAND [SUBCOMMAND_FLAGS]",
+ Description: `TODO`,
+ Subcommands: cli.Commands{
+ secretboxOpenCommand(),
+ secretboxSealCommand(),
+ },
+ }
+}
+
+func secretboxOpenCommand() cli.Command {
+ return cli.Command{
+ Name: "open",
+ Action: cli.ActionFunc(secretboxOpenAction),
+ Usage: "authenticates and decrypts a box produced by seal",
+ UsageText: "step crypto nacl secretbox open NONCE KEY_FILE [--raw]",
+ Description: `TODO`,
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "raw",
+ Usage: "Indicates that input is not base64 encoded",
+ },
+ },
+ }
+}
+
+func secretboxOpenAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 2); err != nil {
+ return err
+ }
+
+ args := ctx.Args()
+ nonce, keyFile := []byte(args[0]), args[1]
+
+ if len(nonce) > 24 {
+ return errors.New("nonce cannot be longer than 24 bytes")
+ }
+
+ key, err := ioutil.ReadFile(keyFile)
+ if err != nil {
+ return errs.FileError(err, keyFile)
+ } else if len(key) != 32 {
+ return errors.New("invalid key file: key size is not 32 bytes")
+ }
+
+ input, err := utils.ReadAll(os.Stdin)
+ if err != nil {
+ return errors.Wrap(err, "error reading input")
+ }
+
+ var rawInput []byte
+ if ctx.Bool("raw") {
+ rawInput = input
+ } else {
+ // DecodeLen returns the maximum length,
+ // Decode will return the actual length.
+ rawInput = make([]byte, b64Encoder.DecodedLen(len(input)))
+ n, err := b64Encoder.Decode(rawInput, input)
+ if err != nil {
+ return errors.Wrap(err, "error decoding base64 input")
+ }
+ rawInput = rawInput[:n]
+ }
+
+ var n [24]byte
+ var k [32]byte
+ copy(n[:], nonce)
+ copy(k[:], key)
+
+ // Fixme: if we prepend the nonce in the seal we can use use rawInput[24:]
+ // as the message and rawInput[:24] as the nonce instead of requiring one.
+ raw, ok := secretbox.Open(nil, rawInput, &n, &k)
+ if !ok {
+ return errors.New("error authenticating or decrypting input")
+ }
+
+ os.Stdout.Write(raw)
+ return nil
+}
+
+func secretboxSealCommand() cli.Command {
+ return cli.Command{
+ Name: "seal",
+ Action: cli.ActionFunc(secretboxSealAction),
+ Usage: "produces an encrypted ciphertext",
+ UsageText: "step crypto nacl secretbox seal NONCE KEY_FILE [--raw]",
+ Description: `TODO`,
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "raw",
+ Usage: "Do not base64 encode output",
+ },
+ },
+ }
+}
+
+func secretboxSealAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 2); err != nil {
+ return err
+ }
+
+ args := ctx.Args()
+ nonce, keyFile := []byte(args[0]), args[1]
+
+ if len(nonce) > 24 {
+ return errors.New("nonce cannot be longer than 24 bytes")
+ }
+
+ key, err := ioutil.ReadFile(keyFile)
+ if err != nil {
+ return errs.FileError(err, keyFile)
+ } else if len(key) != 32 {
+ return errors.New("invalid key: key size is not 32 bytes")
+ }
+
+ input, err := utils.ReadInput("Write text to seal: ")
+ if err != nil {
+ return errors.Wrap(err, "error reading input")
+ }
+
+ var n [24]byte
+ var k [32]byte
+ copy(n[:], nonce)
+ copy(k[:], key)
+
+ // Fixme: we can prepend nonce[:] so it's not necessary in the open.
+ raw := secretbox.Seal(nil, input, &n, &k)
+ if ctx.Bool("raw") {
+ os.Stdout.Write(raw)
+ } else {
+ fmt.Println(b64Encoder.EncodeToString(raw))
+ }
+
+ return nil
+}
diff --git a/command/crypto/nacl/sign.go b/command/crypto/nacl/sign.go
new file mode 100644
index 00000000..7170d34c
--- /dev/null
+++ b/command/crypto/nacl/sign.go
@@ -0,0 +1,169 @@
+package nacl
+
+import (
+ "crypto/rand"
+ "fmt"
+ "io/ioutil"
+ "os"
+
+ "golang.org/x/crypto/nacl/sign"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+func signCommand() cli.Command {
+ return cli.Command{
+ Name: "sign",
+ Usage: "signs small messages using public-key cryptography",
+ UsageText: "step crypto nacl sign SUBCOMMAND [SUBCOMMAND_FLAGS]",
+ Subcommands: cli.Commands{
+ signKeypairCommand(),
+ signOpenCommand(),
+ signSignCommand(),
+ },
+ }
+}
+
+func signKeypairCommand() cli.Command {
+ return cli.Command{
+ Name: "keypair",
+ Action: cli.ActionFunc(signKeypairAction),
+ Usage: "generates a pair for use with sign and open",
+ UsageText: "step crypto nacl sign keypair PUB_FILE PRIV_FILE",
+ }
+}
+
+func signOpenCommand() cli.Command {
+ return cli.Command{
+ Name: "open",
+ Action: cli.ActionFunc(signOpenAction),
+ Usage: "verifies a signed message produced by sign",
+ UsageText: "step crypto nacl sign open PUB_FILE",
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "raw",
+ Usage: "Indicates that input is not base64 encoded",
+ },
+ },
+ }
+}
+
+func signSignCommand() cli.Command {
+ return cli.Command{
+ Name: "sign",
+ Action: cli.ActionFunc(signSignAction),
+ Usage: "signs a message using Ed25519",
+ UsageText: "step crypto nacl sign sign PRIV_FILE",
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "raw",
+ Usage: "Do not base64 encode output",
+ },
+ },
+ }
+}
+
+func signKeypairAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 2); err != nil {
+ return err
+ }
+
+ args := ctx.Args()
+ pubFile, privFile := args[0], args[1]
+ if pubFile == privFile {
+ return errs.EqualArguments(ctx, "PUB_FILE", "PRIV_FILE")
+ }
+
+ pub, priv, err := sign.GenerateKey(rand.Reader)
+ if err != nil {
+ return errors.Wrap(err, "error generating key")
+ }
+
+ if err := utils.WriteFile(pubFile, pub[:], 0600); err != nil {
+ return errs.FileError(err, pubFile)
+ }
+
+ if err := utils.WriteFile(privFile, priv[:], 0600); err != nil {
+ return errs.FileError(err, privFile)
+ }
+
+ return nil
+}
+
+func signOpenAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 1); err != nil {
+ return err
+ }
+
+ pubFile := ctx.Args().Get(0)
+ pub, err := ioutil.ReadFile(pubFile)
+ if err != nil {
+ return errs.FileError(err, pubFile)
+ } else if len(pub) != 32 {
+ return errors.New("invalid public key: key size is not 32 bytes")
+ }
+
+ input, err := utils.ReadAll(os.Stdin)
+ if err != nil {
+ return errors.Wrap(err, "error reading input")
+ }
+
+ var rawInput []byte
+ if ctx.Bool("raw") {
+ rawInput = input
+ } else {
+ // DecodeLen returns the maximum length,
+ // Decode will return the actual length.
+ rawInput = make([]byte, b64Encoder.DecodedLen(len(input)))
+ n, err := b64Encoder.Decode(rawInput, input)
+ if err != nil {
+ return errors.Wrap(err, "error decoding base64 input")
+ }
+ rawInput = rawInput[:n]
+ }
+
+ var pb [32]byte
+ copy(pb[:], pub)
+
+ raw, ok := sign.Open(nil, rawInput, &pb)
+ if !ok {
+ return errors.New("error authenticating input")
+ }
+
+ os.Stdout.Write(raw)
+ return nil
+}
+
+func signSignAction(ctx *cli.Context) error {
+ if err := errs.NumberOfArguments(ctx, 1); err != nil {
+ return err
+ }
+
+ privFile := ctx.Args().Get(0)
+ priv, err := ioutil.ReadFile(privFile)
+ if err != nil {
+ return errs.FileError(err, privFile)
+ } else if len(priv) != 64 {
+ return errors.New("invalid private key: key size is not 64 bytes")
+ }
+
+ input, err := utils.ReadInput("Write text to sign: ")
+ if err != nil {
+ return errors.Wrap(err, "error reading input")
+ }
+
+ var pv [64]byte
+ copy(pv[:], priv)
+
+ raw := sign.Sign(nil, input, &pv)
+ if ctx.Bool("raw") {
+ os.Stdout.Write(raw)
+ } else {
+ fmt.Println(b64Encoder.EncodeToString(raw))
+ }
+
+ return nil
+}
diff --git a/command/crypto/otp/generate.go b/command/crypto/otp/generate.go
new file mode 100644
index 00000000..c3538f2f
--- /dev/null
+++ b/command/crypto/otp/generate.go
@@ -0,0 +1,100 @@
+package otp
+
+import (
+ "bytes"
+ "fmt"
+ "image/png"
+
+ "github.com/smallstep/cli/command/crypto/internal/utils"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+func generateCommand() cli.Command {
+ return cli.Command{
+ Name: "generate",
+ Action: cli.ActionFunc(generateAction),
+ Usage: "one-time password",
+ UsageText: `step crypto otp generate`,
+ Description: `The 'step crypto otp generate' command does TOTP and HTOP`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "issuer, iss",
+ Usage: `Name of the issuing organization (e.g., smallstep.com)`,
+ },
+ cli.StringFlag{
+ Name: "account",
+ Usage: `Name of the user's account (e.g., a username or email
+address)`,
+ },
+ cli.IntFlag{
+ Name: "period",
+ Usage: `Number of seconds a TOTP hash is valid. Defaults to 30
+seconds.`,
+ Value: 30,
+ },
+ cli.IntFlag{
+ Name: "length, digits",
+ Usage: `Length of one-time passwords. Defaults to 6.`,
+ Value: 6,
+ },
+ cli.IntFlag{
+ Name: "secret-size",
+ Usage: `Size of generated TOTP secret. Defaults to 20.`,
+ Value: 20,
+ },
+ cli.StringFlag{
+ Name: "alg, algorithm",
+ Usage: `Algorithm to use for HMAC. Defaults to SHA1. Must be
+one of: SHA1, SHA256, SHA512`,
+ Value: "SHA1",
+ },
+ cli.BoolFlag{
+ Name: "url",
+ Usage: `Output a TOTP Key URI. See
+https://github.com/google/google-authenticator/wiki/Key-Uri-Format`,
+ },
+ cli.StringFlag{
+ Name: "qr",
+ Usage: `Write a QR code to the specified path`,
+ },
+ },
+ }
+}
+
+func generateAction(ctx *cli.Context) error {
+ switch {
+ case len(ctx.String("issuer")) == 0:
+ return errs.RequiredFlag(ctx, "issuer")
+ case len(ctx.String("account")) == 0:
+ return errs.RequiredFlag(ctx, "account")
+ }
+
+ key, err := generate(ctx)
+ if err != nil {
+ return err
+ }
+
+ if ctx.IsSet("qr") {
+ filename := ctx.String("qr")
+
+ // Convert TOTP key into a PNG
+ var buf bytes.Buffer
+ img, err := key.Image(200, 200)
+ if err != nil {
+ return err
+ }
+ png.Encode(&buf, img)
+ if err := utils.WriteFile(filename, buf.Bytes(), 0644); err != nil {
+ return errs.FileError(err, filename)
+ }
+ }
+
+ if ctx.Bool("url") {
+ fmt.Println(key.String())
+ } else {
+ fmt.Println(key.Secret())
+ }
+
+ return nil
+}
diff --git a/command/crypto/otp/otp.go b/command/crypto/otp/otp.go
new file mode 100644
index 00000000..90ec6ea4
--- /dev/null
+++ b/command/crypto/otp/otp.go
@@ -0,0 +1,52 @@
+package otp
+
+import (
+ "strings"
+
+ "github.com/pquerna/otp"
+ "github.com/pquerna/otp/totp"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+// Command returns the cli.Command for jwt and related subcommands.
+func Command() cli.Command {
+ return cli.Command{
+ Name: "otp",
+ Usage: "generate and verify one-time passwords",
+ UsageText: "step crypto otp SUBCOMMAND [SUBCOMMAND_ARGUMENTS] [GLOBAL_FLAGS] [SUBCOMMAND_FLAGS]",
+ Description: `Implements TOTP and HOTP one-time passwords (mention RFCs)`,
+ Subcommands: cli.Commands{
+ generateCommand(),
+ verifyCommand(),
+ },
+ }
+}
+
+func algFromString(ctx *cli.Context, alg string) (otp.Algorithm, error) {
+ switch strings.ToUpper(alg) {
+ case "SHA1":
+ return otp.AlgorithmSHA1, nil
+ case "SHA256":
+ return otp.AlgorithmSHA256, nil
+ case "SHA512":
+ return otp.AlgorithmSHA512, nil
+ default:
+ return 0, errs.InvalidFlagValue(ctx, "alg", alg, "SHA1, SHA256, or SHA512")
+ }
+}
+
+func generate(ctx *cli.Context) (*otp.Key, error) {
+ alg, err := algFromString(ctx, ctx.String("alg"))
+ if err != nil {
+ return nil, err
+ }
+ return totp.Generate(totp.GenerateOpts{
+ Issuer: ctx.String("issuer"),
+ AccountName: ctx.String("account"),
+ Period: uint(ctx.Int("period")),
+ SecretSize: uint(ctx.Int("secret-size")),
+ Digits: otp.Digits(ctx.Int("length")),
+ Algorithm: alg,
+ })
+}
diff --git a/command/crypto/otp/verify.go b/command/crypto/otp/verify.go
new file mode 100644
index 00000000..0543ec08
--- /dev/null
+++ b/command/crypto/otp/verify.go
@@ -0,0 +1,96 @@
+package otp
+
+import (
+ "bufio"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+
+ "github.com/pquerna/otp"
+ "github.com/pquerna/otp/totp"
+ "github.com/smallstep/cli/errs"
+ "github.com/urfave/cli"
+)
+
+func verifyCommand() cli.Command {
+ return cli.Command{
+ Name: "verify",
+ Action: cli.ActionFunc(verifyAction),
+ Usage: "one-time password",
+ UsageText: `step crypto otp verify`,
+ Description: `The 'step crypto otp verify' command does TOTP and HTOP`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "secret",
+ Usage: `A file containing the OTP secret`,
+ },
+ cli.IntFlag{
+ Name: "period",
+ Usage: `Number of seconds a TOTP hash is valid. Defaults to 30
+seconds.`,
+ Value: 30,
+ },
+ cli.IntFlag{
+ Name: "skew",
+ Usage: `Periods before or after current time to allow. Defaults
+to 0. Values greater than 1 require '--insecure'`,
+ Value: 0,
+ },
+ cli.IntFlag{
+ Name: "length digits",
+ Usage: `Length of one-time passwords. Defaults to 6.`,
+ Value: 6,
+ },
+ cli.StringFlag{
+ Name: "alg algorithm",
+ Usage: `Algorithm to use for HMAC. Defaults to SHA1. Must be
+one of: SHA1, SHA256, SHA512`,
+ Value: "SHA1",
+ },
+ cli.IntFlag{
+ Name: "time",
+ Usage: `Time to use for TOTP calculation. Defaults to now.`,
+ },
+ },
+ }
+}
+
+func promptForPasscode() string {
+ reader := bufio.NewReader(os.Stdin)
+ fmt.Print("Enter Passcode: ")
+ text, _ := reader.ReadString('\n')
+ return text
+}
+
+func verifyAction(ctx *cli.Context) error {
+ filename := ctx.String("secret")
+ if len(filename) == 0 {
+ return errs.RequiredFlag(ctx, "secret")
+ }
+
+ b, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return errs.FileError(err, filename)
+ }
+ secret := string(b)
+
+ if strings.HasPrefix(secret, "otpauth://") {
+ url, err := otp.NewKeyFromURL(secret)
+ if err != nil {
+ return err
+ }
+ secret = url.Secret()
+ }
+
+ passcode := promptForPasscode()
+ valid := totp.Validate(passcode, secret)
+ if valid {
+ fmt.Println("ok")
+ os.Exit(0)
+ } else {
+ fmt.Println("fail")
+ os.Exit(1)
+ }
+ return nil
+}
diff --git a/command/oauth/cmd.go b/command/oauth/cmd.go
new file mode 100644
index 00000000..be8043f1
--- /dev/null
+++ b/command/oauth/cmd.go
@@ -0,0 +1,640 @@
+package oauth
+
+import (
+ "bufio"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+
+ "github.com/smallstep/cli/command"
+ "github.com/smallstep/cli/crypto"
+ "github.com/smallstep/cli/errs"
+ "github.com/smallstep/cli/exec"
+ jose "gopkg.in/square/go-jose.v2"
+ "gopkg.in/square/go-jose.v2/jwt"
+)
+
+// These are the OAuth2.0 client IDs from the Step CLI. This application is
+// using the OAuth2.0 flow for installed applications described on
+// https://developers.google.com/identity/protocols/OAuth2InstalledApp
+//
+// The Step CLI app and these client IDs do not have any APIs or services are
+// enabled and it should be only used for OAuth 2.0 authorization.
+//
+// Due to the fact that the app cannot keep the client_secret confidential,
+// incremental authorization with installed apps are not supported by Google.
+//
+// Google is also distributing the client ID and secret on the cloud SDK
+// available here https://cloud.google.com/sdk/docs/quickstarts
+const (
+ defaultClientID = "1087160488420-8qt7bavg3qesdhs6it824mhnfgcfe8il.apps.googleusercontent.com"
+ defaultClientNotSoSecret = "udTrOT3gzrO7W9fDPgZQLfYJ"
+
+ // The URN for getting verification token offline
+ oobCallbackUrn = "urn:ietf:wg:oauth:2.0:oob"
+ // The URN for token request grant type jwt-bearer
+ jwtBearerUrn = "urn:ietf:params:oauth:grant-type:jwt-bearer"
+)
+
+type token struct {
+ AccessToken string `json:"access_token"`
+ IDToken string `json:"id_token"`
+ RefreshToken string `json:"refresh_token"`
+ ExpiresIn int `json:"expires_in"`
+ TokenType string `json:"token_type"`
+ Err string `json:"error,omitempty"`
+ ErrDesc string `json:"error_description,omitempty"`
+}
+
+func init() {
+ cmd := cli.Command{
+ Name: "oauth",
+ Usage: "Authenticate to Smallstep using OAuth OIDC",
+ UsageText: `
+**step oauth** [**--provider**=] [**--client-id**= **--client-secret**=]
+ [**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
+
+**step oauth** **--authorization-endpoint**= **--token-endpoint**=
+ **--client-id**= **--client-secret**= [**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
+
+**step oauth** [**--account**=] [**--authorization-endpoint**= **--token-endpoint**=]
+ [**--scope**= ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
+
+**step oauth** **--account**= **--jwt** [**--scope**= ...] [**--header**] [**-bare**]
+`,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "provider, idp",
+ Usage: "OAuth provider for authentication",
+ Value: "google",
+ },
+ cli.StringFlag{
+ Name: "email, e",
+ Usage: "Email to authenticate",
+ },
+ cli.BoolFlag{
+ Name: "console, c",
+ Usage: "Complete the flow while remaining only inside the terminal",
+ },
+ cli.StringFlag{
+ Name: "client-id",
+ Usage: "OAuth Client ID",
+ },
+ cli.StringFlag{
+ Name: "client-secret",
+ Usage: "OAuth Client Secret",
+ },
+ cli.StringFlag{
+ Name: "account",
+ Usage: "JSON file containing account details",
+ },
+ cli.StringFlag{
+ Name: "authorization-endpoint",
+ Usage: "OAuth Authorization Endpoint",
+ },
+ cli.StringFlag{
+ Name: "token-endpoint",
+ Usage: "OAuth Token Endpoint",
+ },
+ cli.BoolFlag{
+ Name: "header",
+ Usage: "Output HTTP Authorization Header (suitable for use with curl)",
+ },
+ cli.BoolFlag{
+ Name: "oidc",
+ Usage: "Output OIDC Token instead of OAuth Access Token",
+ },
+ cli.BoolFlag{
+ Name: "bare",
+ Usage: "Only output the token",
+ },
+ cli.StringSliceFlag{
+ Name: "scope",
+ Usage: "OAuth scopes",
+ },
+ cli.BoolFlag{
+ Name: "jwt",
+ Usage: "Generate a JWT Auth token instead of an OAuth Token (only works with service accounts)",
+ },
+ },
+ Action: oauthCmd,
+ }
+
+ command.Register(cmd)
+}
+
+func oauthCmd(c *cli.Context) error {
+ opts := &options{
+ Provider: c.String("provider"),
+ Email: c.String("email"),
+ Console: c.Bool("console"),
+ }
+ if err := opts.Validate(); err != nil {
+ return errs.UsageExitError(c, err)
+ }
+ if (opts.Provider != "google" || c.IsSet("authorization-endpoint")) && !c.IsSet("client-id") {
+ return errors.New("flag '--client-id' required with '--provider'")
+ }
+
+ clientID := defaultClientID
+ clientSecret := defaultClientNotSoSecret
+ if c.IsSet("client-id") {
+ clientID = c.String("client-id")
+ clientSecret = c.String("client-secret")
+ }
+
+ authzEp := ""
+ tokenEp := ""
+ if c.IsSet("authorization-endpoint") {
+ if !c.IsSet("token-endpoint") {
+ return errors.New("flag '--authorization-endpoint' requires flag '--token-endpoint'")
+ }
+ opts.Provider = ""
+ authzEp = c.String("authorization-endpoint")
+ tokenEp = c.String("token-endpoint")
+ }
+
+ do2lo := false
+ issuer := ""
+ // This code supports Google service accounts. Probably maybe also support JWKs?
+ if c.IsSet("account") {
+ opts.Provider = ""
+ filename := c.String("account")
+ b, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return errors.Wrapf(err, "error reading account from %s", filename)
+ }
+ account := make(map[string]interface{})
+ if err := json.Unmarshal(b, &account); err != nil {
+ return errors.Wrapf(err, "error reading %s: unsupported format", filename)
+ }
+
+ if _, ok := account["installed"]; ok {
+ details := account["installed"].(map[string]interface{})
+ authzEp = details["auth_uri"].(string)
+ tokenEp = details["token_uri"].(string)
+ clientID = details["client_id"].(string)
+ clientSecret = details["client_secret"].(string)
+ } else if accountType, ok := account["type"]; ok && "service_account" == accountType {
+ authzEp = account["auth_uri"].(string)
+ tokenEp = account["token_uri"].(string)
+ clientID = account["private_key_id"].(string)
+ clientSecret = account["private_key"].(string)
+ issuer = account["client_email"].(string)
+ do2lo = true
+ } else {
+ return errors.Wrapf(err, "error reading %s: unsupported account type", filename)
+ }
+ }
+
+ scope := "openid email"
+ if c.IsSet("scope") {
+ scope = strings.Join(c.StringSlice("scope"), " ")
+ }
+
+ o, err := newOauth(opts.Provider, clientID, clientSecret, authzEp, tokenEp, scope, opts.Email)
+ if err != nil {
+ return errs.ToError(err)
+ }
+
+ var tok *token
+ if do2lo {
+ if c.Bool("jwt") {
+ tok, err = o.DoJWTAuthorization(issuer, scope)
+ } else {
+ tok, err = o.DoTwoLeggedAuthorization(issuer)
+ }
+ } else if opts.Console {
+ tok, err = o.DoManualAuthorization()
+ } else {
+ tok, err = o.DoLoopbackAuthorization()
+ }
+
+ if err != nil {
+ return errs.ToError(err)
+ }
+
+ if c.Bool("header") {
+ if c.Bool("oidc") {
+ fmt.Println("Authorization: Bearer", tok.IDToken)
+ } else {
+ fmt.Println("Authorization: Bearer", tok.AccessToken)
+ }
+ } else {
+ if c.Bool("bare") {
+ if c.Bool("oidc") {
+ fmt.Println(tok.IDToken)
+ } else {
+ fmt.Println(tok.AccessToken)
+ }
+ } else {
+ b, err := json.MarshalIndent(tok, "", " ")
+ if err != nil {
+ return errors.Wrapf(err, "error marshaling token data")
+ }
+ fmt.Println(string(b))
+ }
+ }
+
+ return nil
+}
+
+type options struct {
+ Provider string
+ Email string
+ Console bool
+}
+
+// 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 nil
+}
+
+type oauth struct {
+ provider string
+ clientID string
+ clientSecret string
+ scope string
+ loginHint string
+ redirectURI string
+ tokenEndpoint string
+ authzEndpoint string
+ userInfoEndpoint string // For testing
+ state string
+ codeChallenge string
+ errCh chan error
+ tokCh chan *token
+}
+
+func newOauth(provider, clientID, clientSecret, authzEp, tokenEp, scope, loginHint string) (*oauth, error) {
+ state, err := crypto.GenerateRandomRestrictedString(32)
+ if err != nil {
+ return nil, err
+ }
+
+ challenge, err := crypto.GenerateRandomRestrictedString(64)
+ if err != nil {
+ return nil, err
+ }
+
+ switch provider {
+ case "google":
+ return &oauth{
+ provider: provider,
+ clientID: clientID,
+ clientSecret: clientSecret,
+ scope: scope,
+ authzEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
+ tokenEndpoint: "https://www.googleapis.com/oauth2/v4/token",
+ userInfoEndpoint: "https://www.googleapis.com/oauth2/v3/userinfo",
+ loginHint: loginHint,
+ state: state,
+ codeChallenge: challenge,
+ errCh: make(chan error),
+ tokCh: make(chan *token),
+ }, nil
+ default:
+ userinfoEp := ""
+ if authzEp == "" && tokenEp == "" {
+ d, err := disco(provider)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, ok := d["authorization_endpoint"]; !ok {
+ return nil, errors.New("missing 'authorization_endpoint' in provider metadata")
+ }
+ if _, ok := d["token_endpoint"]; !ok {
+ return nil, errors.New("missing 'token_endpoint' in provider metadata")
+ }
+ authzEp = d["authorization_endpoint"].(string)
+ tokenEp = d["token_endpoint"].(string)
+ userinfoEp = d["token_endpoint"].(string)
+ }
+ return &oauth{
+ provider: provider,
+ clientID: clientID,
+ clientSecret: clientSecret,
+ scope: scope,
+ authzEndpoint: authzEp,
+ tokenEndpoint: tokenEp,
+ userInfoEndpoint: userinfoEp,
+ loginHint: loginHint,
+ state: state,
+ codeChallenge: challenge,
+ errCh: make(chan error),
+ tokCh: make(chan *token),
+ }, nil
+ }
+}
+
+func disco(provider string) (map[string]interface{}, error) {
+ url, err := url.Parse(provider)
+ if err != nil {
+ return nil, err
+ }
+ // 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
+ url.Path = path.Join(url.Path, "/.well-known/openid-configuration")
+ resp, err := http.Get(url.String())
+ if err != nil {
+ return nil, errors.Wrapf(err, "error retrieving %s", url.String())
+ }
+ defer resp.Body.Close()
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error retrieving %s", url.String())
+ }
+ details := make(map[string]interface{})
+ if err := json.Unmarshal(b, &details); err != nil {
+ return nil, errors.Wrapf(err, "error reading %s: unsupported format", url.String())
+ }
+ return details, err
+}
+
+// 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)
+ o.redirectURI = srv.URL
+ defer srv.Close()
+
+ // Get auth url and open it in a browser
+ authURL, err := o.Auth()
+ if err != nil {
+ return nil, err
+ }
+
+ if err := exec.OpenInBrowser(authURL); err != nil {
+ fmt.Fprintln(os.Stderr, "Cannot open a web browser on your platform.")
+ fmt.Fprintln(os.Stderr)
+ fmt.Fprintln(os.Stderr, "Open a local web browser and visit:")
+ fmt.Fprintln(os.Stderr)
+ fmt.Fprintln(os.Stderr, authURL)
+ fmt.Fprintln(os.Stderr)
+ } else {
+ fmt.Fprintln(os.Stderr, "Your default web browser has been opened to visit:")
+ fmt.Fprintln(os.Stderr)
+ fmt.Fprintln(os.Stderr, authURL)
+ fmt.Fprintln(os.Stderr)
+ }
+
+ // Wait for response and return the token
+ select {
+ case tok := <-o.tokCh:
+ return tok, nil
+ case err := <-o.errCh:
+ return nil, err
+ case <-time.After(2 * time.Minute):
+ return nil, errors.New("oauth command timed out, please try again")
+ }
+}
+
+// DoManualAuthorization performs the log in into the identity provider
+// allowing the user to open a browser on a different system and then entering
+// the authorization code on the Step CLI.
+func (o *oauth) DoManualAuthorization() (*token, error) {
+ o.redirectURI = oobCallbackUrn
+ authURL, err := o.Auth()
+ if err != nil {
+ return nil, err
+ }
+
+ fmt.Fprintln(os.Stderr, "Open a local web browser and visit:")
+ fmt.Fprintln(os.Stderr)
+ fmt.Fprintln(os.Stderr, authURL)
+ fmt.Fprintln(os.Stderr)
+
+ // Read from the command line
+ fmt.Fprint(os.Stderr, "Enter verification code: ")
+ reader := bufio.NewReader(os.Stdin)
+ code, err := reader.ReadString('\n')
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ tok, err := o.Exchange(o.tokenEndpoint, code)
+ if err != nil {
+ return nil, err
+ }
+ if tok.Err != "" || tok.ErrDesc != "" {
+ return nil, errors.Errorf("Error exchanging authorization code: %s. %s", tok.Err, tok.ErrDesc)
+ }
+ return tok, nil
+}
+
+// DoTwoLeggedAuthorization performs two-legged OAuth using the jwt-bearer
+// grant type.
+func (o *oauth) DoTwoLeggedAuthorization(issuer string) (*token, error) {
+ pemBytes := []byte(o.clientSecret)
+ block, _ := pem.Decode(pemBytes)
+ if block == nil {
+ return nil, fmt.Errorf("failed to read private key pem block")
+ }
+ priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+ if err != nil {
+ return nil, errors.Wrap(err, "error parsing private key")
+ }
+
+ // Add claims
+ now := int(time.Now().Unix())
+ c := map[string]interface{}{
+ "aud": o.tokenEndpoint,
+ "nbf": now,
+ "iat": now,
+ "exp": now + 3600,
+ "iss": issuer,
+ "scope": o.scope,
+ }
+
+ so := new(jose.SignerOptions)
+ so.WithType("JWT")
+ so.WithHeader("kid", o.clientID)
+
+ // Sign JWT
+ signer, err := jose.NewSigner(jose.SigningKey{
+ Algorithm: "RS256",
+ Key: priv,
+ }, so)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error creating JWT signer")
+ }
+
+ raw, err := jwt.Signed(signer).Claims(c).CompactSerialize()
+ if err != nil {
+ return nil, errors.Wrapf(err, "error serializing JWT")
+ }
+
+ // Construct the POST request to fetch the OAuth token.
+ params := url.Values{
+ "assertion": []string{string(raw)},
+ "grant_type": []string{jwtBearerUrn},
+ }
+
+ // Send the POST request and return token.
+ resp, err := http.PostForm(o.tokenEndpoint, params)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error from token endpoint")
+ }
+ defer resp.Body.Close()
+
+ var tok token
+ if err := json.NewDecoder(resp.Body).Decode(&tok); err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ return &tok, nil
+}
+
+// DoJWTAuthorization generates a JWT instead of an OAuth token. Only works for
+// certain APIs. See https://developers.google.com/identity/protocols/OAuth2ServiceAccount#jwt-auth.
+func (o *oauth) DoJWTAuthorization(issuer, aud string) (*token, error) {
+ pemBytes := []byte(o.clientSecret)
+ block, _ := pem.Decode(pemBytes)
+ if block == nil {
+ return nil, fmt.Errorf("failed to read private key pem block")
+ }
+ priv, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+ if err != nil {
+ return nil, errors.Wrap(err, "error parsing private key")
+ }
+
+ // Add claims
+ now := int(time.Now().Unix())
+ c := map[string]interface{}{
+ "aud": aud,
+ "nbf": now,
+ "iat": now,
+ "exp": now + 3600,
+ "iss": issuer,
+ "sub": issuer,
+ }
+
+ so := new(jose.SignerOptions)
+ so.WithType("JWT")
+ so.WithHeader("kid", o.clientID)
+
+ // Sign JWT
+ signer, err := jose.NewSigner(jose.SigningKey{
+ Algorithm: "RS256",
+ Key: priv,
+ }, so)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error creating JWT signer")
+ }
+
+ raw, err := jwt.Signed(signer).Claims(c).CompactSerialize()
+ if err != nil {
+ return nil, errors.Wrapf(err, "error serializing JWT")
+ }
+
+ tok := &token{string(raw), "", "", 3600, "Bearer", "", ""}
+ return tok, nil
+}
+
+// ServeHTTP is the handler that performs the OAuth 2.0 dance and returns the
+// tokens using channels.
+func (o *oauth) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+ q := req.URL.Query()
+ errStr := q.Get("error")
+ if errStr != "" {
+ o.badRequest(w, "Failed to authenticate: "+errStr)
+ return
+ }
+
+ code := q.Get("code")
+ if code == "" {
+ o.badRequest(w, "Failed to authenticate: missing or invalid code")
+ return
+ }
+
+ state := q.Get("state")
+ if state == "" || state != o.state {
+ o.badRequest(w, "Failed to authenticate: missing or invalid state")
+ return
+ }
+
+ tok, err := o.Exchange(o.tokenEndpoint, code)
+ if err != nil {
+ o.badRequest(w, "Failed exchanging authorization code: "+err.Error())
+ }
+ if tok.Err != "" || tok.ErrDesc != "" {
+ o.badRequest(w, fmt.Sprintf("Failed exchanging authorization code: %s. %s", tok.Err, tok.ErrDesc))
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Add("Content-Type", "text/plain; charset=utf-8")
+ w.Write([]byte("Success: look for the token on the command line"))
+ o.tokCh <- tok
+}
+
+// Auth returns the OAuth 2.0 authentication url.
+func (o *oauth) Auth() (string, error) {
+ u, err := url.Parse(o.authzEndpoint)
+ if err != nil {
+ return "", errors.WithStack(err)
+ }
+
+ q := u.Query()
+ q.Add("client_id", o.clientID)
+ q.Add("redirect_uri", o.redirectURI)
+ q.Add("response_type", "code")
+ q.Add("scope", o.scope)
+ q.Add("state", o.state)
+ q.Add("code_challenge_method", "plain")
+ q.Add("code_challenge", o.codeChallenge)
+ if o.loginHint != "" {
+ q.Add("login_hint", o.loginHint)
+ }
+ u.RawQuery = q.Encode()
+ return u.String(), nil
+}
+
+// Exchange exchanges the authorization code for refresh and access tokens.
+func (o *oauth) Exchange(tokenEndpoint, code string) (*token, error) {
+ data := url.Values{}
+ data.Set("code", code)
+ data.Set("client_id", o.clientID)
+ data.Set("client_secret", o.clientSecret)
+ data.Set("redirect_uri", o.redirectURI)
+ data.Set("grant_type", "authorization_code")
+ data.Set("code_verifier", o.codeChallenge)
+
+ resp, err := http.PostForm(tokenEndpoint, data)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+ defer resp.Body.Close()
+
+ var tok token
+ if err := json.NewDecoder(resp.Body).Decode(&tok); err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ return &tok, nil
+}
+
+func (o *oauth) badRequest(w http.ResponseWriter, msg string) {
+ w.WriteHeader(http.StatusBadRequest)
+ w.Header().Add("Content-Type", "text/plain; charset=utf-8")
+ w.Write([]byte(msg))
+ o.errCh <- errors.New(msg)
+}
diff --git a/command/version/version.go b/command/version/version.go
new file mode 100644
index 00000000..ccea3897
--- /dev/null
+++ b/command/version/version.go
@@ -0,0 +1,27 @@
+package version
+
+import (
+ "fmt"
+
+ "github.com/urfave/cli"
+
+ "github.com/smallstep/cli/command"
+ "github.com/smallstep/cli/config"
+)
+
+func init() {
+ cmd := cli.Command{
+ Name: "version",
+ Usage: "Displays the current version of the cli",
+ Action: Command,
+ }
+
+ command.Register(cmd)
+}
+
+// Command prints out the current version of the tool
+func Command(c *cli.Context) error {
+ fmt.Printf("%s\n", config.Version())
+ fmt.Printf("Release Date: %s\n", config.ReleaseDate())
+ return nil
+}
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 00000000..e9487d79
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,86 @@
+package config
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "os/user"
+ "path"
+ "runtime"
+ "time"
+)
+
+// version and buildTime are filled in during build by the Makefile
+var (
+ buildTime = "N/A"
+ commit = "N/A"
+)
+
+// StepPathEnv defines the name of the environment variable that can overwrite
+// the default configuration path.
+const StepPathEnv = "STEPPATH"
+
+// stepPath will be populated in init() with the proper STEPPATH.
+var stepPath string
+
+// StepPath returns the path for the step configuration directory, this is
+// defined by the environment variable STEPPATH or if this is not set it will
+// default to '$HOME/.step'.
+func StepPath() string {
+ return stepPath
+}
+
+func init() {
+ l := log.New(os.Stderr, "", 0)
+
+ // Get step path from environment or user's home directory
+ stepPath = os.Getenv(StepPathEnv)
+ if stepPath == "" {
+ usr, err := user.Current()
+ if err != nil || usr.HomeDir == "" {
+ l.Fatalf("Error obtaining home directory, please define environment variable %s.", StepPathEnv)
+ }
+ stepPath = path.Join(usr.HomeDir, ".step")
+ }
+
+ // Check for presence or create it if necessary
+ if fi, err := os.Stat(stepPath); err != nil {
+ if err := os.MkdirAll(stepPath, 0700); err != nil {
+ if e, ok := err.(*os.PathError); ok {
+ err = e.Err
+ }
+ l.Fatalf("Error creating '%s': %s.", stepPath, err)
+ }
+ } else if !fi.IsDir() {
+ l.Fatalf("File '%s' is not a directory.", stepPath)
+ }
+ // cleanup
+ stepPath = path.Clean(stepPath)
+}
+
+// Set updates the Version and ReleaseDate
+func Set(v, t string) {
+ buildTime = t
+ commit = v
+}
+
+// Version returns the current version of the binary
+func Version() string {
+ out := commit
+ if commit == "N/A" {
+ out = "0000000-dev"
+ }
+
+ return fmt.Sprintf("Smallstep CLI/%s (%s/%s)",
+ out, runtime.GOOS, runtime.GOARCH)
+}
+
+// ReleaseDate returns the time of when the binary was built
+func ReleaseDate() string {
+ out := buildTime
+ if buildTime == "N/A" {
+ out = time.Now().UTC().Format("2006-01-02 15:04 MST")
+ }
+
+ return out
+}
diff --git a/crypto/certificates/x509/certTemplate.go b/crypto/certificates/x509/certTemplate.go
new file mode 100644
index 00000000..bb416068
--- /dev/null
+++ b/crypto/certificates/x509/certTemplate.go
@@ -0,0 +1,450 @@
+package x509
+
+import (
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "math/big"
+ "net"
+ "strings"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/pkg/errors"
+)
+
+const (
+ defaultDuration = time.Hour * 24 * 365
+)
+
+var (
+ // DefaultCertValidity is the minimum validity of an end-entity (not root or intermediate) certificate.
+ DefaultCertValidity = 24 * time.Hour
+ // DefaultRootCertValidity is the default validity of a root certificate in the step PKI.
+ DefaultRootCertValidity = time.Hour * 24 * 365 * 10
+ // DefaultIntermediateCertValidity is the default validity of a root certificate in the step PKI.
+ DefaultIntermediateCertValidity = time.Hour * 24 * 365 * 10
+
+ // TLS Options
+
+ // DefaultTLSMinVersion default minimum version of TLS.
+ DefaultTLSMinVersion = TLSVersion(1.2)
+ // DefaultTLSMaxVersion default maximum version of TLS.
+ DefaultTLSMaxVersion = TLSVersion(1.2)
+ // DefaultTLSRenegotiation default TLS connection renegotiation policy.
+ DefaultTLSRenegotiation = false // Never regnegotiate.
+ // DefaultTLSCipherSuites specifies default step ciphersuite(s).
+ DefaultTLSCipherSuites = CipherSuites{
+ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+ }
+ // ApprovedTLSCipherSuites smallstep approved ciphersuites.
+ ApprovedTLSCipherSuites = CipherSuites{
+ "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
+ "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
+ "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
+ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
+ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+ "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
+ }
+)
+
+// PkixName allows us to add our own methods to pkix.Name
+type PkixName pkix.Name
+
+// CertTemplate allows us to add our own methods to x509.Certificate
+type CertTemplate x509.Certificate
+
+// PkixNameBuilder for organizing pkix fields.
+type PkixNameBuilder struct {
+ Country, Organization, OrganizationalUnit *string
+ Locality, Province, StreetAddress, PostalCode *string
+ SerialNumber, CommonName *string
+}
+
+// Now is a helper function that returns the current time with the location
+// set to UTC.
+func Now() time.Time {
+ return time.Now().UTC()
+}
+
+// Country generates a function that modifies the Country value
+// of a certificate name struct.
+// Takes a pointer to a comma separated string of countries
+// (e.g. " ecuador,italy,brazil")
+// Returns a function that will modify, in-place, a CertTemplate.
+func Country(countries string) func(*PkixName) error {
+ return func(pn *PkixName) error {
+ if countries == "" {
+ return errors.Errorf("countries cannot be empty")
+ }
+ // appends countries to existing list
+ for _, c := range strings.Split(countries, ",") {
+ pn.Country = append(pn.Country, c)
+ }
+ return nil
+ }
+}
+
+// Locality generates a function that modifies the Country value
+// of a certificate name struct.
+// Takes a pointer to a comma separated string of localities
+// (e.g. " ecuador,italy,brazil")
+// Returns a function that will modify, in-place, a CertTemplate.
+func Locality(localities string) func(*PkixName) error {
+ return func(pn *PkixName) error {
+ if localities == "" {
+ return errors.Errorf("localities cannot be empty")
+ }
+ // appends localities to existing list
+ for _, l := range strings.Split(localities, ",") {
+ pn.Locality = append(pn.Locality, l)
+ }
+ return nil
+ }
+}
+
+// CommonName generates a function that modifies the CommonName value
+// of a certificate name struct.
+// Takes a pointer to a common name string.
+// Returns a function that will modify, in-place, a CertTemplate.
+func CommonName(common string) func(*PkixName) error {
+ return func(pn *PkixName) error {
+ if common == "" {
+ return errors.Errorf("common cannot be empty")
+ }
+ pn.CommonName = common
+ return nil
+ }
+}
+
+// Organization generates a function that modifies the Organization value
+// of a certificate name struct.
+// Takes a pointer to a comma separated string of organizations
+// (e.g. " ecuador,italy,brazil")
+// Returns a function that will modify, in-place, a CertTemplate.
+func Organization(orgs string) func(*PkixName) error {
+ return func(pn *PkixName) error {
+ if orgs == "" {
+ return errors.Errorf("orgs cannot be empty")
+ }
+ // appends organizations to existing list
+ for _, o := range strings.Split(orgs, ",") {
+ pn.Organization = append(pn.Organization, o)
+ }
+ return nil
+ }
+}
+
+// NewPkixName generates a new PkixName struct.
+// Takes an arbitrary number of augmenting functions each of which modifies
+// a PkixName. A default PkixName is created and then the optional
+// augmenter functions are applied one after another in the order in which they
+// appear as parameters.
+// Returns the address of a new PkixName and an error object that will be
+// nil on success or contain error data on failure.
+func NewPkixName(options ...func(*PkixName) error) (*PkixName, error) {
+ pn := &PkixName{}
+
+ for _, op := range options {
+ err := op(pn)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return pn, nil
+}
+
+// Hosts generates a function that modifies the IPAddresses and DNSNames values
+// of a certificate.
+// Takes a pointer to a comma separated string of hostnames
+// (e.g. "127.0.0.1,smallstep.com,blog.smallstep.com")
+// Returns a function that will modify, in-place, a CertTemplate.
+func Hosts(hosts string) func(*CertTemplate) error {
+ return func(ct *CertTemplate) error {
+ if hosts == "" {
+ return errors.New("hosts cannot be empty")
+ }
+ hostsL := strings.Split(hosts, ",")
+ for _, h := range hostsL {
+ if h == "" {
+ continue
+ } else if ip := net.ParseIP(h); ip != nil {
+ ct.IPAddresses = append(ct.IPAddresses, ip)
+ } else {
+ ct.DNSNames = append(ct.DNSNames, h)
+ }
+ }
+ return nil
+ }
+}
+
+// NotBeforeAfter generates a function that modifies the NotBefore and NotAfter
+// values of a certificate.
+// Takes a pair of arguments used to compute the window of time during which
+// the certificate should be valid.
+// Returns a function that will modify, in-place, a CertTemplate.
+func NotBeforeAfter(from time.Time, duration time.Duration) func(*CertTemplate) error {
+ return func(ct *CertTemplate) error {
+ if from.IsZero() {
+ ct.NotBefore = Now()
+ } else {
+ ct.NotBefore = from
+ }
+
+ switch {
+ case duration < 0:
+ return errors.New("Duration must be greater than 0")
+ case duration == 0:
+ ct.NotAfter = ct.NotBefore.Add(defaultDuration)
+ default:
+ ct.NotAfter = ct.NotBefore.Add(duration)
+ }
+
+ return nil
+ }
+}
+
+// SerialNumber generates a function that modifies the SerialNumber value of
+// a CertTemplate.
+// Takes an argument that will be used to set the SerialNumber.
+// Returns a function that will modify, in-place, a CertTemplate.
+func SerialNumber(sn *string) func(*CertTemplate) error {
+ return func(ct *CertTemplate) error {
+ // If sn is empty then generate a random serial number.
+ if sn == nil || len(*sn) == 0 {
+ return errors.Errorf("SerialNumber cannot be nil or empty")
+ }
+
+ ct.SerialNumber = new(big.Int)
+ ct.SerialNumber.SetString(*sn, 10)
+ if _, succ := ct.SerialNumber.SetString(*sn, 10); !succ {
+ return errors.Errorf("Failed to parse serial number: %s",
+ *sn)
+ }
+ return nil
+ }
+}
+
+// Issuer generates a function that modifies the Issuer value of
+// a CertTemplate.
+// Takes an argument that will be used to populate the Issuer pkix.Name.
+// Returns a function that will modify, in-place, a CertTemplate.
+func Issuer(pn PkixName) func(*CertTemplate) error {
+ return func(ct *CertTemplate) error {
+ ct.Issuer = pkix.Name(pn)
+ return nil
+ }
+}
+
+// Subject generates a function that modifies the Subject value of
+// a CertTemplate.
+// Takes an argument that will be used to populate the Subject pkix.Name.
+// Returns a function that will modify, in-place, a CertTemplate.
+func Subject(pn PkixName) func(*CertTemplate) error {
+ return func(ct *CertTemplate) error {
+ ct.Subject = pkix.Name(pn)
+ return nil
+ }
+}
+
+// CRLSign generates a function that modifies the KeyUsage bitmap value of a
+// CertTemplate.
+func CRLSign(c bool) func(*CertTemplate) error {
+ return func(ct *CertTemplate) error {
+ if c {
+ ct.KeyUsage |= x509.KeyUsageCRLSign
+ } else {
+ ct.KeyUsage &= ^(x509.KeyUsageCRLSign)
+ }
+ return nil
+ }
+}
+
+// BasicConstraints generates a function that modifies the BasicConstraintsValid,
+// IsCA, MaxPathLen, and MaxPathLenZero fields of a CertTemplate.
+//
+// If BasicConstraintsValid==true then the next two fields are valid.
+// MaxPathLenZero indicates that BasicConstraintsValid==true and
+// MaxPathLen==0 should be interpreted as an actual maximum path length
+// of zero. Otherwise, that combination is interpreted as MaxPathLen
+// not being set.
+func BasicConstraints(bcv bool, isCA bool, maxPathLen int) func(*CertTemplate) error {
+ return func(ct *CertTemplate) error {
+ if maxPathLen < 0 {
+ return errors.Errorf("MaxPathLen must be >= 0")
+ }
+ if bcv {
+ ct.BasicConstraintsValid = true
+ ct.isCAHelper(isCA)
+ ct.MaxPathLen = maxPathLen
+ if maxPathLen == 0 {
+ ct.MaxPathLenZero = true
+ }
+ } else {
+ if isCA {
+ return errors.Errorf("isCA must be `false` if `BasicConstraintsValid==false`")
+ }
+ if maxPathLen != 0 {
+ return errors.Errorf("maxPathLen should be set to 0 if `BasicConstraintsValid==false`")
+ }
+ ct.BasicConstraintsValid = false
+ ct.isCAHelper(false)
+ ct.MaxPathLen = 0
+ ct.MaxPathLenZero = false
+ }
+ return nil
+ }
+}
+
+func (ct *CertTemplate) isCAHelper(isCA bool) {
+ if isCA {
+ ct.IsCA = true
+ ct.KeyUsage |= x509.KeyUsageCertSign
+ } else {
+ ct.IsCA = false
+ ct.KeyUsage &= ^(x509.KeyUsageCertSign)
+ }
+}
+
+// ExtKeyUsage overwrites the extended key usage slice of a CertTemplate
+func ExtKeyUsage(eku []x509.ExtKeyUsage) func(*CertTemplate) error {
+ return func(ct *CertTemplate) error {
+ ct.ExtKeyUsage = eku
+ return nil
+ }
+}
+
+// NewCertTemplate generates and returns a new CertTemplate struct.
+// Takes an arbitrary number of augmenting functions each of which modifies
+// a CertTemplate. A default CertTemplate is created and then the optional
+// augmenter functions are applied one after another in the order in which they
+// were submitted.
+// Returns the address of a new CertTemplate and an error object which will
+// the nil on success and contain the reason and location of the failure.
+func NewCertTemplate(options ...func(*CertTemplate) error) (*CertTemplate, error) {
+ var err error
+ notBefore := Now()
+
+ ct := &CertTemplate{
+ IsCA: false,
+ NotBefore: notBefore,
+ NotAfter: notBefore.Add(defaultDuration),
+
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+ BasicConstraintsValid: false,
+ MaxPathLen: 0,
+ MaxPathLenZero: false,
+ }
+
+ for _, op := range options {
+ err = op(ct)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if ct.SerialNumber == nil {
+ // TODO figure out how to test rand w/out threading as another arg
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ ct.SerialNumber, err = rand.Int(rand.Reader, serialNumberLimit)
+ // TODO error condition untested -- hard to test w/o mocking rand
+ if err != nil {
+ return nil, errors.Wrap(err, "Failed to generate serial number")
+ }
+ }
+
+ return ct, nil
+}
+
+// Compare compares the calling CertTemplate to the one provided as an argument.
+// Returns nil if the two are equal, otherwise returns an error describing the diff.
+// NOTE: this method avoids comparing a number of fields that are inconvenient or difficult
+// to compare for equality. Check the `IgnoreFields` call below to check if the
+// field you would like to check is being ignored.
+func (ct CertTemplate) Compare(other CertTemplate) error {
+ var diff string
+
+ if diff = cmp.Diff(CertTemplate(ct), other,
+ cmpopts.IgnoreFields(ct, "Extensions", "Issuer.Names",
+ "NotBefore", "NotAfter", "PublicKey", "Raw", "RawIssuer",
+ "RawSubject", "RawSubjectPublicKeyInfo", "RawTBSCertificate",
+ "SerialNumber", "Signature", "Subject.Names")); len(diff) != 0 {
+ return errors.Errorf("data mismatch -- %s", diff)
+ }
+
+ if other.NotBefore.Before(ct.NotBefore.Add(-time.Second*10)) ||
+ ct.NotBefore.After(ct.NotBefore.Add(time.Second*10)) {
+ return errors.Errorf("NotBefore mismatch -- expected: `%s`, but got: `%s`",
+ ct.NotBefore, other.NotBefore)
+ }
+ if ct.NotAfter.Before(other.NotAfter.Add(-time.Second*10)) ||
+ ct.NotAfter.After(other.NotAfter.Add(time.Second*10)) {
+ return errors.Errorf("NotAfter mismatch -- expected: `%s`, but got: `%s`",
+ ct.NotAfter, other.NotAfter)
+ }
+ return nil
+}
+
+// MergeASN1DN fills empty fields of a pkix.Name with default ASN1DN settings.
+// If the field is already set (with non-empty value) then do not overwrite
+// with default value, otherwise overwrite.
+// TODO: test
+func MergeASN1DN(n *pkix.Name, asn1dn *ASN1DN) error {
+ if n == nil || asn1dn == nil {
+ return errors.New("both arguments to mergeASN1DN must be non-nil")
+ }
+ if len(n.Country) == 0 && asn1dn.Country != "" {
+ n.Country = append(n.Country, asn1dn.Country)
+ }
+ if len(n.Organization) == 0 && asn1dn.Organization != "" {
+ n.Organization = append(n.Organization, asn1dn.Organization)
+ }
+ if len(n.OrganizationalUnit) == 0 && asn1dn.OrganizationalUnit != "" {
+ n.OrganizationalUnit = append(n.OrganizationalUnit, asn1dn.OrganizationalUnit)
+ }
+ if len(n.Locality) == 0 && asn1dn.Locality != "" {
+ n.Locality = append(n.Locality, asn1dn.Locality)
+ }
+ if len(n.Province) == 0 && asn1dn.Province != "" {
+ n.Province = append(n.Province, asn1dn.Province)
+ }
+ if len(n.StreetAddress) == 0 && asn1dn.StreetAddress != "" {
+ n.StreetAddress = append(n.StreetAddress, asn1dn.StreetAddress)
+ }
+ return nil
+}
+
+// FromCSR generates a CertTemplate from a x509 certificate signing request.
+func FromCSR(csr *x509.CertificateRequest, options ...func(*CertTemplate) error) (*CertTemplate, error) {
+ ct, err := NewCertTemplate(Hosts(csr.Subject.CommonName),
+ NotBeforeAfter(Now(), DefaultCertValidity),
+ ExtKeyUsage([]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}),
+ Subject(PkixName(csr.Subject)))
+ if err != nil {
+ return nil, err
+ }
+ for _, op := range options {
+ err = op(ct)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return ct, nil
+}
+
+// FromCert generates a CertTemplate from a x509 certificate.
+func FromCert(cert *x509.Certificate, issuer pkix.Name) (*CertTemplate, error) {
+ return NewCertTemplate(Hosts(cert.Subject.CommonName),
+ NotBeforeAfter(Now(), DefaultCertValidity),
+ ExtKeyUsage([]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}),
+ Issuer(PkixName(issuer)), Subject(PkixName(cert.Subject)))
+}
diff --git a/crypto/certificates/x509/certTemplate_test.go b/crypto/certificates/x509/certTemplate_test.go
new file mode 100644
index 00000000..74ea8fff
--- /dev/null
+++ b/crypto/certificates/x509/certTemplate_test.go
@@ -0,0 +1,886 @@
+package x509
+
+import (
+ "crypto/x509"
+ "math/big"
+ "net"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ cmp "github.com/google/go-cmp/cmp"
+)
+
+func Test_NotBeforeAfter(t *testing.T) {
+ var expected string
+ var ctv *CertTemplate
+
+ // NotBefore zero defaults to Now
+ duration := time.Hour * 24 * 365 * 5
+ ct, err := NewCertTemplate(NotBeforeAfter(time.Time{}, duration))
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.NotBefore = time.Now().UTC()
+ ctv.NotAfter = ctv.NotBefore.Add(duration)
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // Duration 0 uses default
+ ct, err = NewCertTemplate(NotBeforeAfter(time.Time{}, 0))
+ if err != nil {
+ t.Errorf("Unexpected error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.NotBefore = time.Now().UTC()
+ ctv.NotAfter = ctv.NotBefore.Add(defaultDuration)
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // Duration < 0 returns error
+ _, err = NewCertTemplate(NotBeforeAfter(time.Time{}, -1))
+ if err == nil {
+ t.Errorf("expected: , but got `nil`")
+ } else {
+ expected = "Duration must be greater than 0"
+ if strings.Compare(err.Error(), expected) != 0 {
+ t.Errorf("error mismatch -- expected: `%s ...`, but got: `%s`",
+ expected, err.Error())
+ }
+ }
+
+ // Duration negative returns error
+ _, err = NewCertTemplate(NotBeforeAfter(time.Time{}, -1))
+ if err == nil {
+ t.Errorf("expected: , but got `nil`")
+ } else {
+ expected = "Duration must be greater than 0"
+ if strings.Compare(err.Error(), expected) != 0 {
+ t.Errorf("error mismatch -- expected: `%s ...`, but got: `%s`",
+ expected, err.Error())
+ }
+ }
+
+ // NotBefore and Duration set
+ now := time.Now().UTC()
+ duration = time.Hour * 24 * 365 * 5
+ start := now.Add(time.Hour * 24 * 365 * 1)
+ ct, err = NewCertTemplate(NotBeforeAfter(start, duration))
+ if err != nil {
+ t.Errorf("Unexpected error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.NotBefore = start
+ ctv.NotAfter = start.Add(duration)
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // Overwritten
+ now = time.Now().UTC()
+ duration = time.Hour * 24 * 365 * 5
+ start = now.Add(time.Hour * 24 * 365 * 1)
+ ct, err = NewCertTemplate(NotBeforeAfter(start, duration),
+ NotBeforeAfter(time.Time{}, 0))
+ if err != nil {
+ t.Errorf("Unexpected error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+}
+
+func Test_CRLSign(t *testing.T) {
+ var ctv *CertTemplate
+
+ // false
+ ct, err := NewCertTemplate(CRLSign(false))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // true
+ ct, err = NewCertTemplate(CRLSign(true))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.KeyUsage |= x509.KeyUsageCRLSign
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // true -> false
+ ct, err = NewCertTemplate(CRLSign(true), CRLSign(false))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // true -> false -> true
+ ct, err = NewCertTemplate(CRLSign(true), CRLSign(false), CRLSign(true))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.KeyUsage |= x509.KeyUsageCRLSign
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+}
+
+func Test_Hosts(t *testing.T) {
+ var ctv *CertTemplate
+ var ct *CertTemplate
+
+ // empty throws error
+ hosts := ""
+ _, err := NewCertTemplate(Hosts(hosts))
+ if err != nil {
+ expected := "hosts cannot be empty"
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s`, but got: `%s`",
+ expected, err.Error())
+ }
+ } else {
+ t.Errorf("expected but not ")
+ }
+
+ hosts = "127.0.0.1"
+ ct, err = NewCertTemplate(Hosts(hosts))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ hosts = "127.0.0.1,smallstep.com,8.8.8.8,google.com"
+ ct, err = NewCertTemplate(Hosts(hosts))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.DNSNames = []string{"smallstep.com", "google.com"}
+ ctv.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("8.8.8.8")}
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ h1 := "127.0.0.1,smallstep.com,8.8.8.8,google.com"
+ h2 := "1.1.1.1,facebook.com"
+ ct, err = NewCertTemplate(Hosts(h1), Hosts(h2))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.DNSNames = []string{"smallstep.com", "google.com", "facebook.com"}
+ ctv.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("8.8.8.8"),
+ net.ParseIP("1.1.1.1")}
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+}
+func Test_SerialNumber(t *testing.T) {
+ var ctv *CertTemplate
+ var err error
+ var expected string
+
+ // nil pointer returns error
+ _, err = NewCertTemplate(SerialNumber(nil))
+ if err == nil {
+ t.Error("expected: , but got: nil")
+ } else {
+ expected = "SerialNumber cannot be nil"
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s ...`, but got: `%s`",
+ expected, err.Error())
+ }
+ }
+
+ // empty string returns errordefaults to random
+ sn := ""
+ _, err = NewCertTemplate(SerialNumber(&sn))
+ if err == nil {
+ t.Error("expected: , but got: nil")
+ } else {
+ expected = "SerialNumber cannot be nil"
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s ...`, but got: `%s`",
+ expected, err.Error())
+ }
+ }
+
+ // NaN returns error
+ sn = "shake and bake"
+ _, err = NewCertTemplate(SerialNumber(&sn))
+ if err == nil {
+ t.Error("expected: , but got: nil")
+ } else {
+ expected = "Failed to parse serial number: "
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s ...`, but got: `%s`",
+ expected, err.Error())
+ }
+ }
+
+ // valid value modifies SerialNumber
+ var success bool
+ sn = "51"
+ ct, err := NewCertTemplate(SerialNumber(&sn))
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.SerialNumber, success = new(big.Int).SetString(sn, 10)
+ if !success {
+ t.Errorf("New big.Int failure")
+ } else {
+ if !reflect.DeepEqual(ctv.SerialNumber, ct.SerialNumber) {
+ t.Errorf("SerialNumber mismatch -- expected: `%s`, but got: `%s`",
+ ctv.SerialNumber.String(), ct.SerialNumber.String())
+ }
+ }
+ }
+ }
+}
+
+func Test_Subject(t *testing.T) {
+ var ctv *CertTemplate
+ var pn PkixName
+
+ // Empty
+ ct, err := NewCertTemplate(Subject(PkixName{}))
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // with values
+ pn = PkixName{
+ Country: []string{"usa"},
+ Organization: []string{"smallstep"},
+ Locality: []string{"san francisco"},
+ CommonName: "internal.smallstep.com",
+ }
+ ct, err = NewCertTemplate(Subject(pn))
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.Subject.Country = []string{"usa"}
+ ctv.Subject.Organization = []string{"smallstep"}
+ ctv.Subject.Locality = []string{"san francisco"}
+ ctv.Subject.CommonName = "internal.smallstep.com"
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+}
+
+func Test_Issuer(t *testing.T) {
+ var ctv *CertTemplate
+ var pn PkixName
+
+ // Empty
+ ct, err := NewCertTemplate(Issuer(PkixName{}))
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // with values
+ pn = PkixName{
+ Country: []string{"usa"},
+ Organization: []string{"smallstep"},
+ Locality: []string{"san francisco"},
+ CommonName: "internal.smallstep.com",
+ }
+ ct, err = NewCertTemplate(Issuer(pn))
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.Issuer.Country = []string{"usa"}
+ ctv.Issuer.Organization = []string{"smallstep"}
+ ctv.Issuer.Locality = []string{"san francisco"}
+ ctv.Issuer.CommonName = "internal.smallstep.com"
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+}
+
+func Test_BasicConstraints(t *testing.T) {
+ var ctv *CertTemplate
+ var ct *CertTemplate
+ var err error
+ var expected string
+
+ // unset
+ ct, err = NewCertTemplate()
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // if BasicConstraintsValid==false and IsCA=true then error
+ ct, err = NewCertTemplate(BasicConstraints(false, true, 0))
+ if err != nil {
+ expected = "isCA must be `false` if `BasicConstraintsValid==false`"
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s`, but got: `%s`",
+ expected, err)
+ }
+ } else {
+ t.Errorf("expected: `error`, but got: `nil`")
+ }
+
+ // BCV==false and IsCA false
+ ct, err = NewCertTemplate(BasicConstraints(false, false, 0))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // BCV==false and IsCA false
+ ct, err = NewCertTemplate(BasicConstraints(true, true, 0))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.IsCA = true
+ ctv.KeyUsage |= x509.KeyUsageCertSign
+ ctv.BasicConstraintsValid = true
+ ctv.MaxPathLen = 0
+ ctv.MaxPathLenZero = true
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // overwrite
+ ct, err = NewCertTemplate(BasicConstraints(true, true, 0), BasicConstraints(true, false, 0))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.IsCA = false
+ ctv.BasicConstraintsValid = true
+ ctv.MaxPathLen = 0
+ ctv.MaxPathLenZero = true
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // BCV==true and maxPathLen < 0 returns error
+ ct, err = NewCertTemplate(BasicConstraints(true, false, -4))
+ if err == nil {
+ t.Errorf("expected: `error`, but got: `nil`")
+ } else {
+ expected = "MaxPathLen must be >= 0"
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s`, but got: `%s`",
+ expected, err)
+ }
+ }
+
+ // explicitly set MaxPathlen to 0
+ mpl := 0
+ ct, err = NewCertTemplate(BasicConstraints(true, false, mpl))
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ct.MaxPathLen != mpl {
+ t.Errorf("MaxPathLen -- expected: `%d`, but got: `%d`",
+ mpl, ct.MaxPathLen)
+ }
+ if ct.MaxPathLenZero != true {
+ t.Errorf("MaxPathLenZero mismatch -- expected: `%t`, but got: `%t`",
+ true, ct.MaxPathLenZero)
+ }
+ }
+
+ // MaxPathLen non-zero
+ mpl = 3
+ ct, err = NewCertTemplate(BasicConstraints(true, false, mpl))
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.BasicConstraintsValid = true
+ ctv.MaxPathLen = mpl
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // overwrite MaxPathLen
+ ct, err = NewCertTemplate(BasicConstraints(true, false, 3), BasicConstraints(true, false, 0))
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.BasicConstraintsValid = true
+ ctv.MaxPathLen = 0
+ ctv.MaxPathLenZero = true
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // BCV==false and maxPathLen != 0 returns error
+ ct, err = NewCertTemplate(BasicConstraints(false, false, 5))
+ if err != nil {
+ expected = "maxPathLen should be set to 0 if `BasicConstraintsValid==false`"
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s`, but got: `%s`",
+ expected, err)
+ }
+ } else {
+ t.Errorf("expected: `error`, but got: `nil`")
+ }
+
+ // overwrite MaxPathLen
+ ct, err = NewCertTemplate(BasicConstraints(false, false, 0),
+ BasicConstraints(true, true, 3), BasicConstraints(false, false, 0))
+ if err != nil {
+ t.Errorf("NewCertTemplate: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+}
+
+func Test_NewCertTemplate(t *testing.T) {
+ var ctv *CertTemplate
+
+ // empty
+ ct, err := NewCertTemplate()
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ct.SerialNumber == nil {
+ t.Errorf("SerialNumber cannot be nil. A random one should have been created")
+ }
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+
+ // error
+ sn := "shake and bake"
+ hosts := "google.com,127.0.0.1"
+ ct, err = NewCertTemplate(Hosts(hosts), SerialNumber(&sn), BasicConstraints(true, true, 0))
+ if err == nil {
+ t.Error("expected: , but got: nil")
+ } else {
+ expected := "Failed to parse serial number: "
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("bad error message -- expected: `%s ...`, but got: `%s`",
+ expected, err.Error())
+ }
+ }
+
+ // with values
+ sn = "51"
+ hosts = "google.com,127.0.0.1,facebook.com,1.1.1.1"
+ now := time.Now().UTC()
+ duration := time.Hour * 24 * 365 * 5
+ start := now.Add(time.Hour * 24 * 365 * 1)
+ pn := PkixName{
+ Country: []string{"usa"},
+ Organization: []string{"smallstep"},
+ Locality: []string{"san francisco"},
+ CommonName: "internal.smallstep.com",
+ }
+ ct, err = NewCertTemplate(Hosts(hosts), BasicConstraints(true, true, 0),
+ Subject(pn), Issuer(pn), NotBeforeAfter(start, duration))
+ if err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ if ctv, err = NewCertTemplate(); err != nil {
+ t.Errorf("NewCertTemplate error: %s", err)
+ } else {
+ ctv.NotBefore = start
+ ctv.NotAfter = start.Add(duration)
+ ctv.Subject.Country = []string{"usa"}
+ ctv.Subject.Organization = []string{"smallstep"}
+ ctv.Subject.Locality = []string{"san francisco"}
+ ctv.Subject.CommonName = "internal.smallstep.com"
+ ctv.Issuer.Country = []string{"usa"}
+ ctv.Issuer.Organization = []string{"smallstep"}
+ ctv.Issuer.Locality = []string{"san francisco"}
+ ctv.Issuer.CommonName = "internal.smallstep.com"
+ ctv.DNSNames = []string{"google.com", "facebook.com"}
+ ctv.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("1.1.1.1")}
+ ctv.IsCA = true
+ ctv.KeyUsage |= x509.KeyUsageCertSign
+ ctv.BasicConstraintsValid = true
+ ctv.MaxPathLen = 0
+ ctv.MaxPathLenZero = true
+ if err = ctv.Compare(*ct); err != nil {
+ t.Errorf("%s", err)
+ }
+ }
+ }
+}
+
+func Test_Locality(t *testing.T) {
+ var err error
+ var expected string
+ var pn, pnv *PkixName
+
+ // empty throws error
+ locality := ""
+ _, err = NewPkixName(Locality(locality))
+ if err != nil {
+ expected = "localities cannot be empty"
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s`, but got: `%s`",
+ expected, err.Error())
+ }
+ } else {
+ t.Errorf("expected: `error`, but got: `nil`")
+ }
+
+ // normal single
+ locality = "boston"
+ pn, err = NewPkixName(Locality(locality))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{Locality: strings.Split(locality, ",")}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+
+ // normal list
+ locality = "boston,philadelphia,manhattan"
+ pn, err = NewPkixName(Locality(locality))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{Locality: strings.Split(locality, ",")}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+
+ // overwrite
+ l1 := "brazil,italy,sudan"
+ l2 := "ukraine,russia,china"
+ pn, err = NewPkixName(Locality(l1), Locality(l2))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{Locality: append(strings.Split(l1, ","), strings.Split(l2, ",")...)}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+}
+
+func Test_Country(t *testing.T) {
+ var err error
+ var expected string
+ var pn, pnv *PkixName
+
+ // empty throws error
+ pn, err = NewPkixName(Country(""))
+ if err != nil {
+ expected = "countries cannot be empty"
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s`, but got: `%s`",
+ expected, err.Error())
+ }
+ } else {
+ pnv = &PkixName{}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+
+ // normal single
+ country := "brazil"
+ pn, err = NewPkixName(Country(country))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{Country: strings.Split(country, ",")}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+
+ // normal list
+ country = "brazil,italy,sudan"
+ pn, err = NewPkixName(Country(country))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{Country: strings.Split(country, ",")}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+
+ // overwrite
+ c1 := "brazil,italy,sudan"
+ c2 := "ukraine,russia,china"
+ pn, err = NewPkixName(Country(c1), Country(c2))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{Country: append(strings.Split(c1, ","), strings.Split(c2, ",")...)}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+}
+
+func Test_Organization(t *testing.T) {
+ var err error
+ var expected string
+ var orgs string
+ var pn, pnv *PkixName
+
+ // empty throws error
+ orgs = ""
+ pn, err = NewPkixName(Organization(orgs))
+ if err != nil {
+ expected = "orgs cannot be empty"
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s`, but got: `%s`",
+ expected, err.Error())
+ }
+ } else {
+ pnv = &PkixName{}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+
+ // normal single
+ orgs = "smallstep"
+ pn, err = NewPkixName(Organization(orgs))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{Organization: strings.Split(orgs, ",")}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+
+ // normal list
+ orgs = "smallstep,betable,oracle"
+ pn, err = NewPkixName(Organization(orgs))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{Organization: strings.Split(orgs, ",")}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+
+ // overwrite
+ org1 := "smallstep,betable,oracle"
+ org2 := "google,facebook,apple"
+ pn, err = NewPkixName(Organization(org1), Organization(org2))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{Organization: append(strings.Split(org1, ","), strings.Split(org2, ",")...)}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+}
+
+func Test_CommonName(t *testing.T) {
+ var err error
+ var expected string
+ var pn, pnv *PkixName
+
+ // empty throws error
+ cn := ""
+ pn, err = NewPkixName(CommonName(cn))
+ if err != nil {
+ expected = "common cannot be empty"
+ if !strings.HasPrefix(err.Error(), expected) {
+ t.Errorf("error mismatch -- expected: `%s`, but got: `%s`",
+ expected, err.Error())
+ }
+ } else {
+ pnv = &PkixName{}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+
+ // normal
+ cn = "internal.smallstep.com"
+ pn, err = NewPkixName(CommonName(cn))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{CommonName: cn}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+
+ // overwrite
+ cn = "internal.smallstep.com"
+ cn2 := "smallstep.com"
+ pn, err = NewPkixName(CommonName(cn), CommonName(cn2))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv = &PkixName{CommonName: cn2}
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+}
+
+func Test_NewPkixName(t *testing.T) {
+ cn := "internal.smallstep.com"
+ country := "brazil,italy,sudan"
+ org := "smallstep,betable,oracle"
+ locality := "boston,philadelphia,manhattan"
+ pn, err := NewPkixName(CommonName(cn), Organization(org), Country(country),
+ Locality(locality))
+ if err != nil {
+ t.Errorf("NewPkixName: %s", err)
+ } else {
+ pnv := &PkixName{
+ Organization: strings.Split(org, ","),
+ Locality: strings.Split(locality, ","),
+ Country: strings.Split(country, ","),
+ CommonName: cn,
+ }
+ if !cmp.Equal(pnv, pn) {
+ t.Errorf("data mismatch")
+ }
+ }
+}
diff --git a/crypto/certificates/x509/clean_test.go b/crypto/certificates/x509/clean_test.go
new file mode 100644
index 00000000..14caa770
--- /dev/null
+++ b/crypto/certificates/x509/clean_test.go
@@ -0,0 +1,26 @@
+package x509
+
+import (
+ "io/ioutil"
+ "log"
+ "os"
+ "testing"
+)
+
+func TestMain(m *testing.M) {
+ // discard log output when testing
+ log.SetOutput(ioutil.Discard)
+
+ result := m.Run()
+
+ clean := func(files []string) {
+ for _, f := range files {
+ if _, err := os.Stat(f); !os.IsNotExist(err) {
+ os.Remove(f)
+ }
+ }
+ }
+ clean([]string{"./test.crt"})
+
+ os.Exit(result)
+}
diff --git a/crypto/certificates/x509/crt.go b/crypto/certificates/x509/crt.go
new file mode 100644
index 00000000..ef73ba75
--- /dev/null
+++ b/crypto/certificates/x509/crt.go
@@ -0,0 +1,99 @@
+package x509
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+// WriteCertificate encodes a x509 Certificate to a file on disk in PEM format.
+func WriteCertificate(crt []byte, out string) error {
+ if crt == nil {
+ return errors.Errorf("crt cannot be nil")
+ }
+ certOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
+ os.FileMode(0644))
+ if err != nil {
+ return errors.Wrapf(err,
+ "failed to open '%s' for writing", out)
+ }
+ err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: crt})
+ if err != nil {
+ return errors.Wrapf(err,
+ "pem encode '%s' failed", out)
+ }
+ certOut.Close()
+ return nil
+}
+
+// LoadCertificate load a certificate.
+func LoadCertificate(crtPath string) (*x509.Certificate, *pem.Block, error) {
+ publicBytes, err := ioutil.ReadFile(crtPath)
+ if err != nil {
+ return nil, nil, errors.Wrapf(err, "error opening certificate file %s", crtPath)
+ }
+ publicPEM, _ := pem.Decode(publicBytes)
+ if publicPEM == nil {
+ return nil, nil, errors.Errorf("error decoding certificate file %s", crtPath)
+ }
+ crt, err := x509.ParseCertificate(publicPEM.Bytes)
+ if err != nil {
+ return nil, nil, errors.Wrapf(err, "error parsing x509 certificate file %s", crtPath)
+ }
+
+ return crt, publicPEM, nil
+}
+
+// ReadCertPool loads a certificate pool from disk.
+func ReadCertPool(path string) (*x509.CertPool, error) {
+ info, err := os.Stat(path)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ var (
+ files []string
+ pool = x509.NewCertPool()
+ )
+ if info.IsDir() {
+ finfos, err := ioutil.ReadDir(path)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+ for _, finfo := range finfos {
+ files = append(files, filepath.Join(path, finfo.Name()))
+ }
+ } else {
+ files = strings.Split(path, ",")
+ }
+
+ var pems []byte
+ for _, f := range files {
+ bytes, err := ioutil.ReadFile(f)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+ for len(bytes) > 0 {
+ var block *pem.Block
+ block, bytes = pem.Decode(bytes)
+ if block == nil {
+ // TODO: at a higher log level we should log the file we could not find.
+ break
+ }
+ // Ignore PEM blocks that are not CERTIFICATEs.
+ if block.Type != "CERTIFICATE" {
+ continue
+ }
+ pems = append(pems, pem.EncodeToMemory(block)...)
+ }
+ }
+ if ok := pool.AppendCertsFromPEM(pems); !ok {
+ return nil, errors.Errorf("error loading Root certificates")
+ }
+ return pool, nil
+}
diff --git a/crypto/certificates/x509/crt_test.go b/crypto/certificates/x509/crt_test.go
new file mode 100644
index 00000000..fdc5e69e
--- /dev/null
+++ b/crypto/certificates/x509/crt_test.go
@@ -0,0 +1,157 @@
+package x509
+
+import (
+ "crypto/sha1"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/assert"
+)
+
+func Test_WriteCertificate(t *testing.T) {
+ certPath := "./test.crt"
+
+ tests := map[string]struct {
+ crt func() ([]byte, error)
+ crtOut string
+ err error
+ }{
+ "crt cannot be nil": {
+ crt: func() ([]byte, error) {
+ return nil, nil
+ },
+ err: errors.New("crt cannot be nil"),
+ },
+ "propagate open crt out file error": {
+ crt: func() ([]byte, error) {
+ return []byte{}, nil
+ },
+ crtOut: "./fakeDir/test.crt",
+ err: errors.New("failed to open './fakeDir/test.crt' for writing: open ./fakeDir/test.crt: no such file or directory"),
+ },
+ "success": {
+ crt: func() ([]byte, error) {
+ hosts := "google.com,127.0.0.1,facebook.com,1.1.1.1"
+ sub := pkix.Name{
+ Country: []string{"usa"},
+ Organization: []string{"smallstep"},
+ Locality: []string{"san francisco"},
+ CommonName: "internal.smallstep.com",
+ }
+ profile, err := NewRootProfile("overwrite", WithSubject(sub),
+ WithIssuer(sub), WithHosts(hosts))
+ if err != nil {
+ return nil, err
+ }
+ return profile.CreateCertificate()
+ },
+ crtOut: certPath,
+ },
+ }
+
+ for name, test := range tests {
+ t.Logf("Running test case: %s", name)
+
+ crtBytes, err := test.crt()
+ assert.FatalError(t, err)
+
+ err = WriteCertificate(crtBytes, test.crtOut)
+ if err != nil {
+ if assert.NotNil(t, test.err) {
+ assert.HasPrefix(t, err.Error(), test.err.Error())
+ }
+ } else {
+ ctv, err := NewCertTemplate()
+ assert.FatalError(t, err)
+ ctv.Subject.Country = []string{"usa"}
+ ctv.Subject.Organization = []string{"smallstep"}
+ ctv.Subject.Locality = []string{"san francisco"}
+ ctv.Subject.CommonName = "internal.smallstep.com"
+ ctv.Issuer.Country = []string{"usa"}
+ ctv.Issuer.Organization = []string{"smallstep"}
+ ctv.Issuer.Locality = []string{"san francisco"}
+ ctv.Issuer.CommonName = "internal.smallstep.com"
+ ctv.DNSNames = []string{"google.com", "facebook.com"}
+ ctv.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("1.1.1.1")}
+ ctv.IsCA = true
+ ctv.KeyUsage |= x509.KeyUsageKeyEncipherment |
+ x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign |
+ x509.KeyUsageCRLSign
+ ctv.BasicConstraintsValid = true
+ ctv.MaxPathLenZero = false
+ ctv.MaxPathLen = 1
+ ctv.SignatureAlgorithm = x509.ECDSAWithSHA256
+ ctv.PublicKeyAlgorithm = x509.ECDSA
+ ctv.Version = 3
+
+ crt, err := x509.ParseCertificate(crtBytes)
+ assert.FatalError(t, err)
+ pubBytes, err := x509.MarshalPKIXPublicKey(crt.PublicKey)
+ assert.FatalError(t, err)
+ _hash := sha1.Sum(pubBytes)
+ hash := _hash[:]
+ ctv.SubjectKeyId = hash[:] // takes slice over the whole array
+
+ // Verify that cert written to file is correct
+ certFileBytes, err := ioutil.ReadFile(certPath)
+ assert.FatalError(t, err)
+ pemCert, _ := pem.Decode(certFileBytes)
+ fileCert, err := x509.ParseCertificate(pemCert.Bytes)
+ assert.FatalError(t, err)
+
+ // Check `NotBefore` and `NotAfter`.
+ now := time.Now().UTC()
+ assert.True(t, fileCert.NotBefore.Before(now))
+ assert.True(t, fileCert.NotBefore.After(now.Add(-time.Minute)))
+ expiry := now.Add(time.Hour * 24 * 365 * 10)
+ assert.True(t, fileCert.NotAfter.Before(expiry))
+ assert.True(t, fileCert.NotAfter.After(expiry.Add(-time.Minute)))
+ // Now we set these to correct values since we've already checked them.
+ ctv.NotBefore = fileCert.NotBefore
+ ctv.NotAfter = fileCert.NotAfter
+
+ assert.NoError(t, ctv.Compare(CertTemplate(*fileCert)))
+ }
+ }
+}
+
+func Test_LoadCertificate(t *testing.T) {
+ var (
+ testBadCert = "./test_files/badca.crt"
+ testBadPEMCert = "./test_files/badpem.crt"
+ testCert = "./test_files/ca.crt"
+ )
+
+ e1 := fmt.Sprintf("error decoding certificate file %s", testBadPEMCert)
+ e2 := fmt.Sprintf("error parsing x509 certificate file %s", testBadCert)
+
+ tests := []struct {
+ crtPath string
+ expectedError string
+ }{
+ {"", "error opening certificate file "},
+ {"", "error opening certificate file "},
+ {testBadPEMCert, e1},
+ {testBadCert, e2},
+ }
+
+ for i, tc := range tests {
+ _, _, err := LoadCertificate(tc.crtPath)
+ if assert.Error(t, err) {
+ assert.True(t, strings.HasPrefix(err.Error(), tc.expectedError), err.Error(), i)
+ }
+ }
+
+ crt, pemBlock, err := LoadCertificate(testCert)
+ assert.FatalError(t, err)
+ assert.Equals(t, crt.Subject.CommonName, "internal.smallstep.com")
+ assert.NotNil(t, pemBlock)
+}
diff --git a/crypto/certificates/x509/csr.go b/crypto/certificates/x509/csr.go
new file mode 100644
index 00000000..1d4e8317
--- /dev/null
+++ b/crypto/certificates/x509/csr.go
@@ -0,0 +1,16 @@
+package x509
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+)
+
+// LoadCSRFromBytes loads a CSR given the ASN.1 DER format.
+func LoadCSRFromBytes(der []byte) (*x509.CertificateRequest, error) {
+ block, _ := pem.Decode(der)
+ if block == nil {
+ return nil, errors.New("failed to decode PEM block containing CSR")
+ }
+ return x509.ParseCertificateRequest(block.Bytes)
+}
diff --git a/crypto/certificates/x509/csr_test.go b/crypto/certificates/x509/csr_test.go
new file mode 100644
index 00000000..a07524be
--- /dev/null
+++ b/crypto/certificates/x509/csr_test.go
@@ -0,0 +1,78 @@
+package x509
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "testing"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/assert"
+)
+
+func Test_CSR_LoadCSRFromBytes(t *testing.T) {
+ tests := map[string]struct {
+ der func() ([]byte, error)
+ err error
+ }{
+ "propagate pem decode error": {
+ der: func() ([]byte, error) {
+ return nil, nil
+ },
+ err: errors.New("failed to decode PEM block containing CSR"),
+ },
+ "propagate parse error": {
+ der: func() ([]byte, error) {
+ return []byte("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEU5ZhIhFn7v4bpMKlkz\ndmLCj9KfmqFWig29c6OzYoMUnbdodOmZ6RId/Gw5fnluH12eFxsItlXKDT4RPSm7\nm4D1sYgFmk88oo6z4XDuItDncoIg89jGK38OZ8A0gwEoy5JqukONGmAldzgzQyiq\nuzSNMeT1WO9zXCwOljcUio697M1kP/YN1Lp7n7YILVwdV8wQ2vyNKQK1M/5OZOFl\nlOqww4wsqLTDK0rfxp6LAVtczp1XdxbsnpdixrK38O+dHWe4IS5HhKLmmmdTfpFQ\nD3PIAXs/Naap/0+t0lsOplNPiF4BYyNBIqyyfm1o5ZQpGfmITKvDFZMBkQ2i2cou\nnQIDAQAB\n-----END PUBLIC KEY-----"), nil
+ },
+ err: errors.New("asn1: structure error: tags don't match"),
+ },
+ "success": {
+ der: func() ([]byte, error) {
+ keypair, err := rsa.GenerateKey(rand.Reader, 4096)
+ if err != nil {
+ return nil, err
+ }
+ template := &x509.CertificateRequest{
+ Subject: pkix.Name{
+ Country: []string{"Foo"},
+ Organization: []string{"Smallstep"},
+ CommonName: "Bar",
+ },
+ SignatureAlgorithm: x509.SHA256WithRSA,
+ }
+
+ bytes, err := x509.CreateCertificateRequest(rand.Reader, template, keypair)
+ if err != nil {
+ return nil, err
+ }
+
+ return pem.EncodeToMemory(&pem.Block{
+ Type: "CSR",
+ Headers: map[string]string{},
+ Bytes: bytes,
+ }), nil
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Logf("Running test case: %s", name)
+
+ bytes, err := test.der()
+ assert.FatalError(t, err)
+ csr, err := LoadCSRFromBytes(bytes)
+
+ if err != nil {
+ if assert.NotNil(t, test.err) {
+ assert.HasPrefix(t, err.Error(), test.err.Error())
+ }
+ } else {
+ assert.Equals(t, csr.Subject.Country, []string{"Foo"})
+ assert.Equals(t, csr.Subject.Organization, []string{"Smallstep"})
+ assert.Equals(t, csr.Subject.CommonName, "Bar")
+ }
+ }
+}
diff --git a/crypto/certificates/x509/identity.go b/crypto/certificates/x509/identity.go
new file mode 100644
index 00000000..e6330ed2
--- /dev/null
+++ b/crypto/certificates/x509/identity.go
@@ -0,0 +1,52 @@
+package x509
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "io/ioutil"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/crypto/keys"
+)
+
+// Identity contains a public/private x509 certificate/key pair.
+type Identity struct {
+ Crt *x509.Certificate
+ CrtPem *pem.Block
+ Key interface{}
+}
+
+// NewIdentity returns a new Identity.
+func NewIdentity(c *x509.Certificate, b *pem.Block, k interface{}) *Identity {
+ return &Identity{
+ Crt: c,
+ CrtPem: b,
+ Key: k,
+ }
+}
+
+// LoadIdentityFromDisk load a public certificate and private key from disk.
+func LoadIdentityFromDisk(crtPath, keyPath string, getPass func() (string, error)) (*Identity, error) {
+ var (
+ err error
+ caCert *x509.Certificate
+ caPrivateKey interface{}
+ publicPem *pem.Block
+ )
+
+ // load crt
+ if caCert, publicPem, err = LoadCertificate(crtPath); err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ // load private key
+ keyBytes, err := ioutil.ReadFile(keyPath)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+ if caPrivateKey, err = keys.LoadPrivateKey(keyBytes, getPass); err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ return NewIdentity(caCert, publicPem, caPrivateKey), nil
+}
diff --git a/crypto/certificates/x509/identity_test.go b/crypto/certificates/x509/identity_test.go
new file mode 100644
index 00000000..7850e88b
--- /dev/null
+++ b/crypto/certificates/x509/identity_test.go
@@ -0,0 +1,61 @@
+package x509
+
+import (
+ "testing"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/assert"
+)
+
+func Test_LoadIdentityFromDisk(t *testing.T) {
+ var (
+ testBadCert = "./test_files/badca.crt"
+ testCert = "./test_files/ca.crt"
+ testNoPasscodeBadKey = "./test_files/noPasscodeBadCa.key"
+ noPasscodeKey = "./test_files/noPasscodeCa.key"
+ )
+
+ tests := map[string]struct {
+ crtPath string
+ keyPath string
+ pass string
+ err error
+ }{
+ "error parsing x509 certificate": {
+ crtPath: testBadCert,
+ keyPath: "",
+ pass: "",
+ err: errors.Errorf("error parsing x509 certificate file %s",
+ testBadCert),
+ },
+ "error parsing rsa key": {
+ crtPath: testCert,
+ keyPath: testNoPasscodeBadKey,
+ pass: "",
+ err: errors.Errorf("error parsing RSA key"),
+ },
+ "success": {
+ crtPath: testCert,
+ keyPath: noPasscodeKey,
+ pass: "",
+ },
+ }
+
+ for name, test := range tests {
+ t.Logf("Running test case: %s", name)
+
+ kp, err := LoadIdentityFromDisk(test.crtPath, test.keyPath, func() (string, error) {
+ return test.pass, nil
+ })
+ if err != nil {
+ if assert.NotNil(t, test.err) {
+ assert.HasPrefix(t, err.Error(), test.err.Error())
+ }
+ } else {
+ assert.FatalError(t, err)
+ assert.NotNil(t, kp.Crt)
+ assert.NotNil(t, kp.CrtPem)
+ assert.NotNil(t, kp.Key)
+ }
+ }
+}
diff --git a/crypto/certificates/x509/intermediateProfile.go b/crypto/certificates/x509/intermediateProfile.go
new file mode 100644
index 00000000..0a2897b9
--- /dev/null
+++ b/crypto/certificates/x509/intermediateProfile.go
@@ -0,0 +1,63 @@
+package x509
+
+import (
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "math/big"
+ "time"
+
+ "github.com/pkg/errors"
+)
+
+// Intermediate implements the Profile for a intermediate certificate.
+type Intermediate struct {
+ base
+}
+
+// NewIntermediateProfile returns a new intermediate x509 Certificate profile.
+func NewIntermediateProfile(name string, iss *x509.Certificate, issPriv interface{}, withOps ...WithOption) (*Intermediate, error) {
+ var (
+ err error
+ notBefore = time.Now()
+ )
+
+ sub := &x509.Certificate{
+ IsCA: true,
+ NotBefore: notBefore,
+ // 10 year intermediate certificate validity.
+ NotAfter: notBefore.Add(time.Hour * 24 * 365 * 10),
+ KeyUsage: x509.KeyUsageKeyEncipherment |
+ x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+ ExtKeyUsage: []x509.ExtKeyUsage{
+ x509.ExtKeyUsageServerAuth,
+ x509.ExtKeyUsageClientAuth,
+ },
+ BasicConstraintsValid: true,
+ MaxPathLen: 0,
+ MaxPathLenZero: true,
+ Issuer: pkix.Name{CommonName: name},
+ Subject: pkix.Name{CommonName: name},
+ }
+
+ b, err := newBase(sub, iss, withOps...)
+ if err != nil {
+ return nil, err
+ }
+
+ if sub.SerialNumber == nil {
+ // TODO figure out how to test rand w/out threading as another arg
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ sub.SerialNumber, err = rand.Int(rand.Reader, serialNumberLimit)
+ // TODO error condition untested -- hard to test w/o mocking rand
+ if err != nil {
+ return nil, errors.Wrapf(err, "Failed to generate serial number for "+
+ "certificate with common name '%s'", name)
+ }
+ }
+
+ i := &Intermediate{}
+ fromBase(i, *b)
+ i.SetIssuerPrivateKey(issPriv)
+ return i, nil
+}
diff --git a/crypto/certificates/x509/leafProfile.go b/crypto/certificates/x509/leafProfile.go
new file mode 100644
index 00000000..94339690
--- /dev/null
+++ b/crypto/certificates/x509/leafProfile.go
@@ -0,0 +1,106 @@
+package x509
+
+import (
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "time"
+
+ "github.com/pkg/errors"
+)
+
+// Leaf implements the Profile for a leaf certificate.
+type Leaf struct {
+ base
+}
+
+// NewLeafProfileWithTemplate returns a new leaf x509 Certificate Profile with
+// Subject Certificate set to the value of the template argument.
+// A public/private keypair **WILL NOT** be generated for this profile because
+// the public key will be populated from the Subject Certificate parameter.
+func NewLeafProfileWithTemplate(sub *x509.Certificate, iss *x509.Certificate, issPriv interface{}, withOps ...WithOption) (*Leaf, error) {
+ withOps = append(withOps, WithPublicKey(sub.PublicKey))
+ b, err := newBase(sub, iss, withOps...)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ l := &Leaf{}
+ fromBase(l, *b)
+ l.SetIssuerPrivateKey(issPriv)
+ return l, nil
+}
+
+// NewLeafProfile 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 NewLeafProfile(cn string, iss *x509.Certificate, issPriv interface{}, withOps ...WithOption) (*Leaf, error) {
+ sub, err := defaultLeafTemplate(pkix.Name{CommonName: cn}, iss.Subject)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ b, err := newBase(sub, iss, withOps...)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ l := &Leaf{}
+ fromBase(l, *b)
+ l.SetIssuerPrivateKey(issPriv)
+ return l, 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
+// the public key will be populated from the CSR.
+func NewLeafProfileWithCSR(csr *x509.CertificateRequest, iss *x509.Certificate, issPriv interface{}, withOps ...WithOption) (*Leaf, error) {
+ if csr.PublicKey == nil {
+ return nil, errors.Errorf("CSR must have PublicKey")
+ }
+ withOps = append(withOps, WithPublicKey(csr.PublicKey))
+
+ sub, err := defaultLeafTemplate(csr.Subject, iss.Subject)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+ sub.Extensions = csr.Extensions
+ sub.ExtraExtensions = csr.ExtraExtensions
+ sub.DNSNames = csr.DNSNames
+ sub.EmailAddresses = csr.EmailAddresses
+ sub.IPAddresses = csr.IPAddresses
+ sub.URIs = csr.URIs
+
+ b, err := newBase(sub, iss, withOps...)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ l := &Leaf{}
+ fromBase(l, *b)
+ l.SetIssuerPrivateKey(issPriv)
+ return l, nil
+}
+
+func defaultLeafTemplate(sub pkix.Name, iss pkix.Name) (*x509.Certificate, error) {
+ notBefore := time.Now()
+
+ ct := &x509.Certificate{
+ IsCA: false,
+ NotBefore: notBefore,
+ // 1 Day Leaf Certificate validity.
+ NotAfter: notBefore.Add(time.Hour * 24),
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{
+ x509.ExtKeyUsageServerAuth,
+ x509.ExtKeyUsageClientAuth,
+ },
+ BasicConstraintsValid: false,
+ MaxPathLen: 0,
+ MaxPathLenZero: false,
+ Issuer: iss,
+ Subject: sub,
+ }
+
+ return ct, nil
+}
diff --git a/crypto/certificates/x509/profile.go b/crypto/certificates/x509/profile.go
new file mode 100644
index 00000000..833db62c
--- /dev/null
+++ b/crypto/certificates/x509/profile.go
@@ -0,0 +1,259 @@
+package x509
+
+import (
+ "crypto/rand"
+ "crypto/sha1"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "math/big"
+ "net"
+ "strings"
+ "time"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/cli/crypto/keys"
+)
+
+// Profile is an interface that certificate profiles (e.g. leaf,
+// intermediate, root) must implement.
+type Profile interface {
+ Issuer() *x509.Certificate
+ Subject() *x509.Certificate
+ SubjectPrivateKey() interface{}
+ SubjectPublicKey() interface{}
+ SetIssuer(*x509.Certificate)
+ SetSubject(*x509.Certificate)
+ SetSubjectPrivateKey(interface{})
+ SetSubjectPublicKey(interface{})
+ SetIssuerPrivateKey(interface{})
+ CreateCertificate() ([]byte, error)
+ GenerateKeyPair(string, string, int) error
+}
+
+type base struct {
+ iss *x509.Certificate
+ sub *x509.Certificate
+ subPub interface{}
+ subPriv interface{}
+ issPriv interface{}
+}
+
+// WithOption is a modifier function on base.
+type WithOption func(Profile) error
+
+// GenerateKeyPair returns a Profile modifier that generates a public/private
+// key pair for a profile.
+func GenerateKeyPair(kty, crv string, size int) WithOption {
+ return func(p Profile) error {
+ return p.GenerateKeyPair(kty, crv, size)
+ }
+}
+
+// WithPublicKey returns a Profile modifier that sets the public key for a profile.
+func WithPublicKey(pub interface{}) WithOption {
+ return func(p Profile) error {
+ p.SetSubjectPublicKey(pub)
+ return nil
+ }
+}
+
+// WithSubject returns a Profile modifier that sets the Subject for a x509
+// Certificate.
+func WithSubject(sub pkix.Name) WithOption {
+ return func(p Profile) error {
+ crt := p.Subject()
+ crt.Subject = sub
+ return nil
+ }
+}
+
+// WithIssuer returns a Profile modifier that sets the Subject for a x509
+// Certificate.
+func WithIssuer(iss pkix.Name) WithOption {
+ return func(p Profile) error {
+ crt := p.Subject()
+ crt.Issuer = iss
+ return nil
+ }
+}
+
+// WithNotBeforeAfter returns a Profile modifier that sets the `NotBefore` and
+// `NotAfter` attributes of the subject x509 Certificate.
+func WithNotBeforeAfter(nb, na time.Time) WithOption {
+ return func(p Profile) error {
+ crt := p.Subject()
+ crt.NotBefore = nb
+ crt.NotAfter = na
+ return nil
+ }
+}
+
+// WithHosts returns a Profile modifier which sets the DNS Names and IP Addresses
+// that will be bound to the subject Certificate.
+//
+// `hosts` should be a comma separated string of DNS Names and IP Addresses.
+// e.g. `127.0.0.1,internal.smallstep.com,blog.smallstep.com,1.1.1.1`.
+func WithHosts(hosts string) WithOption {
+ return func(p Profile) error {
+ hostsL := strings.Split(hosts, ",")
+ crt := p.Subject()
+ for _, h := range hostsL {
+ if h == "" {
+ continue
+ } else if ip := net.ParseIP(h); ip != nil {
+ crt.IPAddresses = append(crt.IPAddresses, ip)
+ } else {
+ crt.DNSNames = append(crt.DNSNames, h)
+ }
+ }
+
+ return nil
+ }
+}
+
+// newBase generates a new base profile.
+//
+// If the public/private key pair of the subject identity are not set by
+// the optional modifiers then a pair will be generated using sane defaults.
+func newBase(sub, iss *x509.Certificate, withOps ...WithOption) (*base, error) {
+ if sub == nil {
+ return nil, errors.Errorf("subject certificate cannot be nil")
+ }
+ if iss == nil {
+ return nil, errors.Errorf("issuing certificate cannot be nil")
+ }
+
+ var (
+ err error
+ b = &base{}
+ )
+ b.SetSubject(sub)
+ b.SetIssuer(iss)
+
+ for _, op := range withOps {
+ if err := op(b); err != nil {
+ return nil, errors.WithStack(err)
+ }
+ }
+
+ if b.SubjectPublicKey() == nil {
+ if err := b.GenerateDefaultKeyPair(); err != nil {
+ return nil, errors.WithStack(err)
+ }
+ }
+
+ if b.sub.SubjectKeyId == nil {
+ pubBytes, err := x509.MarshalPKIXPublicKey(b.SubjectPublicKey())
+ if err != nil {
+ return nil, errors.Wrapf(err, "failed to marshal public key to bytes")
+ }
+ hash := sha1.Sum(pubBytes)
+ b.sub.SubjectKeyId = hash[:] // takes slice over the whole array
+ }
+
+ if b.sub.SerialNumber == nil {
+ // TODO figure out how to test rand w/out threading as another arg
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ b.sub.SerialNumber, err = rand.Int(rand.Reader, serialNumberLimit)
+ // TODO error condition untested -- hard to test w/o mocking rand
+ if err != nil {
+ return nil, errors.Wrapf(err, "Failed to generate serial number for "+
+ "certificate with common name '%s'", sub.Subject.CommonName)
+ }
+ }
+
+ return b, nil
+}
+
+func fromBase(profile Profile, b base) {
+ profile.SetSubject(b.Subject())
+ profile.SetIssuer(b.Issuer())
+ profile.SetSubjectPublicKey(b.SubjectPublicKey())
+ profile.SetSubjectPrivateKey(b.SubjectPrivateKey())
+ profile.SetIssuerPrivateKey(b.issPriv)
+}
+
+func (b *base) Issuer() *x509.Certificate {
+ return b.iss
+}
+
+func (b *base) Subject() *x509.Certificate {
+ return b.sub
+}
+
+func (b *base) SubjectPrivateKey() interface{} {
+ return b.subPriv
+}
+
+func (b *base) SubjectPublicKey() interface{} {
+ return b.subPub
+}
+
+func (b *base) SetIssuer(iss *x509.Certificate) {
+ b.iss = iss
+}
+
+func (b *base) SetSubject(sub *x509.Certificate) {
+ b.sub = sub
+}
+
+func (b *base) SetSubjectPrivateKey(priv interface{}) {
+ b.subPriv = priv
+}
+
+func (b *base) SetIssuerPrivateKey(priv interface{}) {
+ b.issPriv = priv
+}
+
+func (b *base) SetSubjectPublicKey(pub interface{}) {
+ b.subPub = pub
+}
+
+func (b *base) GenerateKeyPair(kty, crv string, size int) error {
+ pub, priv, err := keys.GenerateKeyPair(kty, crv, size)
+ if err != nil {
+ return err
+ }
+ b.SetSubjectPublicKey(pub)
+ b.SetSubjectPrivateKey(priv)
+ return nil
+}
+
+func (b *base) GenerateDefaultKeyPair() error {
+ pub, priv, err := keys.GenerateDefaultKeyPair()
+ if err != nil {
+ return err
+ }
+ b.SetSubjectPublicKey(pub)
+ b.SetSubjectPrivateKey(priv)
+ return nil
+}
+
+// CreateCertificate creates an x509 Certificate using the configuration stored
+// in the profile.
+func (b *base) CreateCertificate() ([]byte, error) {
+ if b.SubjectPublicKey() == nil {
+ return nil, errors.Errorf("Profile does not have subject public key. Need to call 'profile.GenKeys(...)' or use setters to populate keys")
+ }
+ if b.issPriv == nil {
+ return nil, errors.Errorf("Profile does not have issuer private key. Use setters to populate this field.")
+ }
+ return x509.CreateCertificate(rand.Reader, b.Subject(), b.Issuer(),
+ b.SubjectPublicKey(), b.issPriv)
+}
+
+// Create Certificate from profile and write the certificate and private key
+// to disk.
+func (b *base) CreateWriteCertificate(crtOut, keyOut, pass string) ([]byte, error) {
+ crtBytes, err := b.CreateCertificate()
+ if err != nil {
+ return nil, err
+ }
+ if err := WriteCertificate(crtBytes, crtOut); err != nil {
+ return nil, err
+ }
+ if err := keys.WritePrivateKey(b.SubjectPrivateKey(), pass, keyOut); err != nil {
+ return nil, err
+ }
+ return crtBytes, nil
+}
diff --git a/crypto/certificates/x509/rootProfile.go b/crypto/certificates/x509/rootProfile.go
new file mode 100644
index 00000000..fcb37865
--- /dev/null
+++ b/crypto/certificates/x509/rootProfile.go
@@ -0,0 +1,80 @@
+package x509
+
+import (
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "math/big"
+ "time"
+
+ "github.com/pkg/errors"
+)
+
+// Root implements the Profile for a root certificate.
+type Root struct {
+ base
+}
+
+// NewRootProfile returns a new root x509 Certificate profile.
+func NewRootProfile(name string, withOps ...WithOption) (*Root, error) {
+ crt, err := defaultRootTemplate(name)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ b, err := newBase(crt, crt, withOps...)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ r := &Root{}
+ fromBase(r, *b)
+ r.SetIssuerPrivateKey(r.SubjectPrivateKey())
+ return r, nil
+}
+
+// NewRootProfileWithTemplate returns a new root x509 Certificate profile.
+func NewRootProfileWithTemplate(crt *x509.Certificate, withOps ...WithOption) (*Root, error) {
+ b, err := newBase(crt, crt, withOps...)
+ if err != nil {
+ return nil, err
+ }
+
+ r := &Root{}
+ fromBase(r, *b)
+ r.SetIssuerPrivateKey(r.SubjectPrivateKey())
+ return r, nil
+}
+
+func defaultRootTemplate(cn string) (*x509.Certificate, error) {
+ var (
+ err error
+ notBefore = time.Now()
+ )
+
+ ct := &x509.Certificate{
+ IsCA: true,
+ NotBefore: notBefore,
+ // 10 year root certificate validity.
+ NotAfter: notBefore.Add(time.Hour * 24 * 365 * 10),
+ KeyUsage: x509.KeyUsageKeyEncipherment |
+ x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+ BasicConstraintsValid: true,
+ MaxPathLen: 1,
+ MaxPathLenZero: false,
+ Issuer: pkix.Name{CommonName: cn},
+ Subject: pkix.Name{CommonName: cn},
+ }
+
+ // TODO figure out how to test rand w/out threading as another arg
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ ct.SerialNumber, err = rand.Int(rand.Reader, serialNumberLimit)
+ // TODO error condition untested -- hard to test w/o mocking rand
+ if err != nil {
+ return nil, errors.Wrapf(err, "Failed to generate serial number for "+
+ "certificate with common name '%s'", cn)
+ }
+
+ return ct, nil
+
+}
diff --git a/crypto/certificates/x509/test_files/badca.crt b/crypto/certificates/x509/test_files/badca.crt
new file mode 100644
index 00000000..df88045a
--- /dev/null
+++ b/crypto/certificates/x509/test_files/badca.crt
@@ -0,0 +1,37 @@
+-----BEGIN CERTIFICATE-----
+MIIGcDCCBFigAwIBAgIBATANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoMCXNt
+YWxsc3RlcDEfMB0GA1UEAwwWaW50ZXJuYWwuc21hbGxzdGVwLmNvbTAeFw0xNzA2
+MTQyMTU3MTRaFw0yNzA2MTIyMTU3MTRaMGcxHzAdBgNVBAMMFmludGVybmFsLnNt
+YWxsc3RlcC5jb20xCzAJBgNVBAYTAlVTMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2Nv
+MRIwEAYDVQQKDAlzbWFsbHN0ZXAxCzAJBgNVBAgMAkNBMIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAt0M+Eu4i6hdERJ8yTrVwZz7Hds2WQrPd89NuVYP4
+ouJbuG+udcGeWpJc8nIlv0w6F/8CRMxuY9FaHY2epiGb4Xw9P1a+XXIm70OpQFEQ
+VPJycbHyphLuGjSq3Mc3+AVKDyQZSYuW+C6XJYsI06p2Y0xm8ST2BWc7b4CuNVte
+e38eVaxiISs5P7GIcD6nMhTbEtC6H6j+EjEcZOboMFNPQNRXlPdtyrLT4QhDSLGi
+d/vTs33ttkv0ebUuCFGc/965KmP1NawKhka3x0kJ20/vXzM8XjwLSOG2041+MdnJ
+rgCllQkirbTBfIg0u64yDV81k853RF9ZvkuNI/FPQ5ueo1HeB2ExumQl3S6vT+nv
+TgoDLkwq6kgoMYXPjO4kDnklf3xiCVfmsMUmGgOYbFcRD5e4fTOHeBRGC27aklOI
+k5CODMcFQRvHliQZpnORWmNgzvaNLEOf5d3wk613+rj+iru//EeuMNVP+PO6k95/
+g+D6nEvc9WUjiO/2vDR4Dw+LiAeWH+bXx+AVCugin7snkz0xmNUtvzne2LLqUTie
+s3mRxw87pjgDvTDstF7il3D83dxzJI7xp/00WlMdzwaxPT182Y45JH6mS0BNtUEo
+oPtfDtyE54SXa56R+tZREzw+CizAbS4FzlwQmYY5pZHuSQKAxQMFOJPbFTrEt8tk
+Tg0CAwEAAaOCASUwggEhMIGZBgNVHSMEgZEwgY6AFIuhuVEao1ML4RaiJbaSFlHs
+fDgloWukaTBnMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNh
+biBGcmFuY2lzY28xEjAQBgNVBAoMCXNtYWxsc3RlcDEfMB0GA1UEAwwWaW50ZXJu
+YWwuc21hbGxzdGVwLmNvbYIJANHnRMPsmOa0MBIGA1UdEwEB/wQIMAYBAf8CAQAw
+HQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIBRjAh
+BgNVHREEGjAYghZpbnRlcm5hbC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQWBBRFga2r
+OHyWFPOHK2hrFPP7PKx28DANBgkqhkiG9w0BAQsFAAOCAgEAay1ZCyfCJNwmGawA
+pWO8pUsmv39dUu6lfeG0yHBGQMUUrmuUrn1JbAQQ5rxXSIMWQHNskACO37l5Ol0+
+UZNjo+2vnDS6+5XQJ2k68e9Ivb/dm7nrJfDrs+WcLcQ/fCn01pKA02gIESQzIHci
+zuSAtj8iVG7IkfWkGAK7DLsrktAcB+2+8SqzN1XwLlb2jkPOuseM+BidesvP+cQR
+ik614DwNAHkD5v+n6Ee2LrDFu7Qsl2fNe3OPIMe9cWFC68yL/bhXpeTYzfArHVbR
+imZvEB6e6wvW0J4o+e25LIFIavLuoLTt0Js+3Q2XD/HNqxBJsZe6y/LX1JMGLEe1
+pWOcGt97xXL4hdqWT+M4kUgpbxmuEEretlVVydpKncxCa/ovzWyhp4TJ4RZ0J67p
+vUbVV+BiW3nfIbz+3pS9a0O4huvp+iNfIKo74xLYWGcT/37Le/X7nJbwLLg+fzCj
+5CKgEj6YWJ5om9fD5aV3CLczsFsSkzg0R292DPYuWiWAcgPrrVa2mSaNSWacajmx
+5CIBu5G/VBmGjYSHa+dnagPFExsDpksB6AzX82pthDewAikVSg3DTK4sDuKXpXFS
+CMRrIxHUYRUgyoMjsNUR9KQeq7hiTwCq325KpL7XWBtRCoBOwLWX1lECScJITQA4
+XevZD8LWagqK8/3ZQsrBDmcmrAs+
+-----END CERTIFICATE-----
diff --git a/crypto/certificates/x509/test_files/badca.key b/crypto/certificates/x509/test_files/badca.key
new file mode 100644
index 00000000..74aae1d2
--- /dev/null
+++ b/crypto/certificates/x509/test_files/badca.key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,6D4455D3019FC904
+
+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
+-----END RSA PRIVATE KEY-----
diff --git a/crypto/certificates/x509/test_files/badcsr.csr b/crypto/certificates/x509/test_files/badcsr.csr
new file mode 100644
index 00000000..6a2820b2
--- /dev/null
+++ b/crypto/certificates/x509/test_files/badcsr.csr
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE REQUEST-----
+LIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH
+DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl
+c3Quc21hbGxzdGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ANPahliigZ38QpBLmQMS3MVKKZ5gapNjqR7LIEYoYWa4lTFiUnbwg8tSfIFcgLZr
+jNIxn7/98+JOJHKgS03NhFJoS5hej0LyypleOGJ0nk2qawYVKnn1ftoKjkfxkfZI
+a/5rsDF1jhNBspB/KPHWE0eimKQJbUiVG1zA1sExnXDecF3vJfBj+DPDWngx4yxR
+/jYEKjt4tQ6Ei752TbosrCHYeYXzkr6iAwiNz6vT/ewLb6b8JmuN8X6Y1I9ogDGx
+hntBJ1jAK8x3IGTjYbkm+mqVuCyhNcHtGfEHcBnUEzLAPrVFn8kGiAnU17FJ0uQ7
+1C9CtUzgBRZCxSBm6Qs+Zs8CAwEAAaCBjTCBigYJKoZIhvcNAQkOMX0wezAMBgNV
+HRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNVHQ8B
+Af8EBAMCBaAwHQYDVR0RBBYwFIISdGVzdC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQW
+BBQj6N4RTAAjhV3UBYXH72mkdOGpqzANBgkqhkiG9w0BAQsFAAOCAQEAN0/ivCBk
+FD53SqtRmqqc7C9saoRNvV+wDi4Sg6YGLFQLjbZPJrqQURWdHtV9O3sb3p8O5erX
+9Kgq3C7fqd//0mro4GZ1GTpjsPKIMocZFfH7zEhAZlvQLRKWICjoBaOwxQum2qY/
+B3+ltAXb4uqGdbI0jPkkyWGN5CQhK+ZHoYe/zGtTEmHBcPxRtJJkukQQjUgZhjU2
+Z7K+w3AjOxj47XLNHHlW83QYUJ2mN+mEZF9DhrZb2ydYOlpy0V2NJwv7QrmnFaDj
+R0v3BFLTblIp100li3oV2QaM/yESrgo9XIjEEGzCGz5cNs5ovNadufUZDCJyyT4q
+ZEp7knvU2osWRw==
+-----END CERTIFICATE REQUEST-----
diff --git a/crypto/certificates/x509/test_files/badpem.crt b/crypto/certificates/x509/test_files/badpem.crt
new file mode 100644
index 00000000..e208eaa4
--- /dev/null
+++ b/crypto/certificates/x509/test_files/badpem.crt
@@ -0,0 +1,37 @@
+-----BEGIN CERTIFICATE-----
+MIIGcDCCBFigAwIBAgIBATANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoMCXNt
+YWxsc3RlcDEfMB0GA1UEAwwWaW50ZXJuYWwuc21hbGxzdGVwLmNvbTAeFw0xNzA2
+MTQyMTU3MTRaFw0yNzA2MTIyMTU3MTRaMGcxHzAdBgNVBAMMFmludGVybmFsLnNt
+YWxsc3RlcC5jb20xCzAJBgNVBAYTAlVTMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2Nv
+MRIwEAYDVQQKDAlzbWFsbHN0ZXAxCzAJBgNVBAgMAkNBMIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAt0M+Eu4i6hdERJ8yTrVwZz7Hds2WQrPd89NuVYP4
+ouJbuG+udcGeWpJc8nIlv0w6F/8CRMxuY9FaHY2epiGb4Xw9P1a+XXIm70OpQFEQ
+VPJycbHyphLuGjSq3Mc3+AVKDyQZSYuW+C6XJYsI06p2Y0xm8ST2BWc7b4CuNVte
+e38eVaxiISs5P7GIcD6nMhTbEtC6H6j+EjEcZOboMFNPQNRXlPdtyrLT4QhDSLGi
+d/vTs33ttkv0ebUuCFGc/965KmP1NawKhka3x0kJ20/vXzM8XjwLSOG2041+MdnJ
+rgCllQkirbTBfIg0u64yDV81k853RF9ZvkuNI/FPQ5ueo1HeB2ExumQl3S6vT+nv
+TgoDLkwq6kgoMYXPjO4kDnklf3xiCVfmsMUmGgOYbFcRD5e4fTOHeBRGC27aklOI
+k5CODMcFQRvHliQZpnORWmNgzvaNLEOf5d3wk613+rj+iru//EeuMNVP+PO6k95/
+g+D6nEvc9WUjiO/2vDR4Dw+LiAeWH+bXx+AVCugin7snkz0xmNUtvzne2LLqUTie
+s3mRxw87pjgDvTDstF7il3D83dxzJI7xp/00WlMdzwaxPT182Y45JH6mS0BNtUEo
+oPtfDtyE54SXa56R+tZREzw+CizAbS4FzlwQmYY5pZHuSQKAxQMFOJPbFTrEt8tk
+Tg0CAwEAAaOCASUwggEhMIGZBgNVHSMEgZEwgY6AFIuhuVEao1ML4RaiJbaSFlHs
+fDgloWukaTBnMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNh
+biBGcmFuY2lzY28xEjAQBgNVBAoMCXNtYWxsc3RlcDEfMB0GA1UEAwwWaW50ZXJu
+YWwuc21hbGxzdGVwLmNvbYIJANHnRMPsmOa0MBIGA1UdEwEB/wQIMAYBAf8CAQAw
+HQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIBRjAh
+BgNVHREEGjAYghZpbnRlcm5hbC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQWBBRFga2r
+OHyWFPOHK2hrFPP7PKx28DANBgkqhkiG9w0BAQsFAAOCAgEAay1ZCyfCJNwmGawA
+pWO8pUsmv39dUu6lfeG0yHBGQMUUrmuUrn1JbAQQ5rxXSIMWQHNskACO37l5Ol0+
+UZNjo+2vnDS6+5XQJ2k68e9Ivb/dm7nrJfDrs+WcLcQ/fCn01pKA02gIESQzIHci
+zuSAtj8iVG7IkfWkGAK7DLsrktAcB+2+8SqzN1XwLlb2jkPOuseM+BidesvP+cQR
+ik614DwNAHkD5v+n6Ee2LrDFu7Qsl2fNe3OPIMe9cWFC68yL/bhXpeTYzfArHVbR
+imZvEB6e6wvW0J4o+e25LIFIavLuoLTt0Js+3Q2XD/HNqxBJsZe6y/LX1JMGLEe1
+pWOcGt97xXL4hdqWT+M4kUgpbxmuEEretlVVydpKncxCa/ovzWyhp4TJ4RZ0J67p
+vUbVV+BiW3nfIbz+3pS9a0O4huvp+iNfIKo74xLYWGcT/37Le/X7nJbwLLg+fzCj
+5CKgEj6YWJ5om9fD5aV3CLczsFsSkzg0R292DPYuWiWAcgPrrVa2mSaNSWacajmx
+5CIBu5G/VBmGjYSHa+dnagPFExsDpksB6AzX82pthDewAikVSg3DTK4sDuKXpXFS
+CMRrIxHUYRUgyoMjsNUR9KQeq7hiTwCq325KpL7XWBtRCoBOwLWX1lECScJITQA4
+XevZD8LWagqK8/3ZQsrBDmcmr
+-----END CERTIFICATE-----
diff --git a/crypto/certificates/x509/test_files/badpem.csr b/crypto/certificates/x509/test_files/badpem.csr
new file mode 100644
index 00000000..24940b82
--- /dev/null
+++ b/crypto/certificates/x509/test_files/badpem.csr
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH
+DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl
+c3Quc21hbGxzdGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ANPahliigZ38QpBLmQMS3MVKKZ5gapNjqR7LIEYoYWa4lTFiUnbwg8tSfIFcgLZr
+jNIxn7/98+JOJHKgS03NhFJoS5hej0LyypleOGJ0nk2qawYVKnn1ftoKjkfxkfZI
+a/5rsDF1jhNBspB/KPHWE0eimKQJbUiVG1zA1sExnXDecF3vJfBj+DPDWngx4yxR
+/jYEKjt4tQ6Ei752TbosrCHYeYXzkr6iAwiNz6vT/ewLb6b8JmuN8X6Y1I9ogDGx
+hntBJ1jAK8x3IGTjYbkm+mqVuCyhNcHtGfEHcBnUEzLAPrVFn8kGiAnU17FJ0uQ7
+1C9CtUzgBRZCxSBm6Qs+Zs8CAwEAAaCBjTCBigYJKoZIhvcNAQkOMX0wezAMBgNV
+HRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNVHQ8B
+Af8EBAMCBaAwHQYDVR0RBBYwFIISdGVzdC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQW
+BBQj6N4RTAAjhV3UBYXH72mkdOGpqzANBgkqhkiG9w0BAQsFAAOCAQEAN0/ivCBk
+FD53SqtRmqqc7C9saoRNvV+wDi4Sg6YGLFQLjbZPJrqQURWdHtV9O3sb3p8O5erX
+9Kgq3C7fqd//0mro4GZ1GTpjsPKIMocZFfH7zEhAZlvQLRKWICjoBaOwxQum2qY/
+B3+ltAXb4uqGdbI0jPkkyWGN5CQhK+ZHoYe/zGtTEmHBcPxRtJJkukQQjUgZhjU2
+Z7K+w3AjOxj47XLNHHlW83QYUJ2mN+mEZF9DhrZb2ydYOlpy0V2NJwv7QrmnFaDj
+R0v3BFLTblIp100li3oV2QaM/yESrgo9XIjEEGzCGz5cNs5ovNadufUZDCJyyT4q
+ZEp7knvU2osWR
+-----END CERTIFICATE REQUEST-----
diff --git a/crypto/certificates/x509/test_files/badpem.key b/crypto/certificates/x509/test_files/badpem.key
new file mode 100644
index 00000000..a31402a3
--- /dev/null
+++ b/crypto/certificates/x509/test_files/badpem.key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,6D4455D3019FC904
+
+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
+-----END RSA PRIVATE KEY-----
diff --git a/crypto/certificates/x509/test_files/badsig.csr b/crypto/certificates/x509/test_files/badsig.csr
new file mode 100644
index 00000000..bb733121
--- /dev/null
+++ b/crypto/certificates/x509/test_files/badsig.csr
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH
+DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl
+c3Quc21hbGxzdGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ANPahliigZ38QpBLmQMS3MVKKZ5gapNjqR7LIEYoYWa4lTFiUnbwg8tSfIFcgLZr
+jNIxn7/98+JOJHKgS03NhFJoS5hej0LyypleOGJ0nk2qawYVKnn1ftoKjkfxkfZI
+a/5rsDF1jhNBspB/KPHWE0eimKQJbUiVG1zA1sExnXDecF3vJfBj+DPDWngx4yxR
+/jYEKjt4tQ6Ei752TbosrCHYeYXzkr6iAwiNz6vT/ewLb6b8JmuN8X6Y1I9ogDGx
+hntBJ1jAK8x3IGTjYbkm+mqVuCyhNcHtGfEHcBnUEzLAPrVFn8kGiAnU17FJ0uQ7
+1C9CtUzgBRZCxSBm6Qs+Zs8CAwEAAaCBjTCBigYJKoZIhvcNAQkOMX0wezAMBgNV
+HRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNVHQ8B
+Af8EBAMCBaAwHQYDVR0RBBYwFIISdGVzdC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQW
+BBQj6N4RTAAjhV3UBYXH72mkdOGpqzANBgkqhkiG9w0BAQsFAAOCAQEAN0/ivCBk
+FD53SqtRmqqc7C9saoRNvV+wDi4Sg6YGLFQLjbZPJrqQURWdHtV9O3sb3p8O5erX
+9Kgq3C7fqd//0mro4GZ1GTpjsPKIMocZFfH7zEhAZlvQLRKWICjoBaOwxQum2qY/
+B3+ltAXb4uqGdbI0jPkkyWGN5CQhK+ZHoYe/zGtTEmHBcPxRtJJkukQQjUgZhjU2
+Z7K+w3AjOxj47XLNHHlW83QYUJ2mN+mEZF9DhrZb2ydYOlpy0V2NJwv7QrmnFaDj
+R0v3BFLTblIp100li3oV2QaM/yESrgo9XIjEEGzCGz5cNs5ovNadufUZDCJyyT4q
+ZEp7knvU2psWRw==
+-----END CERTIFICATE REQUEST-----
diff --git a/crypto/certificates/x509/test_files/ca.crt b/crypto/certificates/x509/test_files/ca.crt
new file mode 100644
index 00000000..1fa1e1ce
--- /dev/null
+++ b/crypto/certificates/x509/test_files/ca.crt
@@ -0,0 +1,34 @@
+-----BEGIN CERTIFICATE-----
+MIIF6zCCA9OgAwIBAgIRAL4t3Jo++cwAle8DdXchv/owDQYJKoZIhvcNAQELBQAw
+WzEMMAoGA1UEBhMDVVNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRIwEAYDVQQK
+EwlzbWFsbHN0ZXAxHzAdBgNVBAMTFmludGVybmFsLnNtYWxsc3RlcC5jb20wHhcN
+MTcwOTIzMDczNTA3WhcNMTgwOTIzMDczNTA3WjBbMQwwCgYDVQQGEwNVU0ExFjAU
+BgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCXNtYWxsc3RlcDEfMB0GA1UE
+AxMWaW50ZXJuYWwuc21hbGxzdGVwLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ADCCAgoCggIBAKA+760g0MbZpFCgG6NpzRh0B8ElgQUteMjynL8ge+r8QsFCm2XY
+P7BYzjyyD9FdNTRw2toUB8G/t3E5jhjrE6qvG0PWsluzFEtfh0uS59BPS6YTgurY
+LE3PAc/+fCxEI3SfA4TCYVnzUcSkhcHNT0PtMWG8tR7S+0GFc1O22wUn2e/dKK1d
+fCGhEu9gzuA3TjJgpzfmXTBFUijiIRSaXHiYcUWR0FE3CKVULlM2jJ/uxXZr6kSZ
+STxQ/kisaIzOe7Y/uA9F4fyfCHdaCsvkv3d11d1SkOdBCY+jx+PG5uLDWxGCgZYZ
+dWDjOX43gquSaC3bFMi+cglF4Wx+n173elcOuoF77bVNBOOtWIbWNLYVujkvbzec
+Dn0NLySl79OKMuSuF995iR7Or29gcbaZz5j1NHeqbhb24HWZ+9xi3ws4ike7GZ5Q
+akZ3AwEcwVwbMhQ5KCoWKroSWpYUvQ58PGgy+ml5f42Cjg/e1nH1/hpnqwzzItbs
+6qb9I0RV12Y6KCEqmKIrs1EdHc351aknhiZ1Zgdankhym3TiAo08mkDIqbJUKjR+
+0De7ynBKBDq79NWfb5DLdXH95z1DDZvI4FJ9X0eAlo3DbkZXFIfoeF1gL577pEES
+NXZKXqmY2hPcsZUKAhXIEK3zmNXJVGeqb9sNnYtBTY6zBo5sA+40WDHNAgMBAAGj
+gakwgaYwDgYDVR0PAQH/BAQDAgGmMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
+BQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRVBozFxzNJ9pSzW4lW
+MZF/q+6SPTAfBgNVHSMEGDAWgBTd1DpE9Y4R5OoP7f5WT91mPCU2gDAhBgNVHREE
+GjAYghZpbnRlcm5hbC5zbWFsbHN0ZXAuY29tMA0GCSqGSIb3DQEBCwUAA4ICAQCj
+3CT2xk9xLFTX0Ki30PWB6/0OxN5L0sKk7pJAWIzgdsKYrBbh93sA/oYGsnr0iW6F
+VLWkvqMmGLlp5yLg6LIQaed5C26u2fc0udzXTVfEx7QjOtLtetLt7LQ6Kzb7FOri
+iiUfvilLttXyESQ3WzlCTh4OlrIhNWg1w56jc0/7GAJY0LTrsCoYOSwR2qlBQiTI
+41+fApAlRZKI0eNP9X4GxVkgh+wIVuF4zXr/460VkaWT9RquvS2MIaotdZ3IBTTk
+tCmFHvbI/eZOWo1KbjPdSByOZVI1gBfpU/eufsdysRebZwBsYRDF391QJ3aKt2cZ
+WnAjtYl3lfcXA/iFj2HL04vmdTweBVvl7Wa2EsM/iiEhPOWlIXdQx81FURE8nc2H
+DCQJgQIbwqZ4LQJrmF6tmzhmJUH2/9Vxc/rYMSx6NgT6sSoz+gXt0yDd20tF7SU6
+smiL/uCGfSXAbqsI+MO8Nc7gOPhKtHeW4r2Kx/OzuFkYAFBez0GqxmnJ/xKgwfGO
+v+pzRC09KUgpncGZuB6S9PUWPhC15LO5bFBF1tiUy8hyzzbopfyPtLnhY8tq4u+j
+lGTAz5g+7chL+j6UqZZYGD5DqIiOYO/YK0wi92ov1wu6Pkvb33dy1LVWJaGjcAQv
+7cuffXbN9jXxAtrFxrmY3qfXnUR4K2lCESWPjCCmaw==
+-----END CERTIFICATE-----
diff --git a/crypto/certificates/x509/test_files/ca.key b/crypto/certificates/x509/test_files/ca.key
new file mode 100644
index 00000000..85e782cb
--- /dev/null
+++ b/crypto/certificates/x509/test_files/ca.key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,36945dfbb2645820d163fad834a3d290
+
+D0CiVeW3XhmauadBLQRr6+mEeFE4jJHrGwhIprvovzlnok3kWBSFNAyooX14x80s
+ULwJ06FLxwrQuZjh64D/UgUtyFKauVkXsZjqBEh05jtU7lUH8pUICbCFPUmqR45U
+ZO1euIYozHfV4wCHfhuHqTXVrn/7bqdiXgM5P4mpQIKgu4eOJ5EaN4WRJw9pbPny
+TCo+7uY+kdEdrCIqKmsdCOf4fN9gYjxxAgKkPM9RrBaQLnX9YwnH1tc1NtduYpEA
+wtVZUiKzrNKLCAER1mthIsqwspakY7drzBvXpy6P/kO79UAhgyKuKpqlD54H3csF
+VRDxtAtRwcqklq/RbJ+B+BrXQAuQc2naQmttF87F3CUWEwqs15Nzzhha8qkAiUOY
+DY8zg2olB0Rf5DHq+3CRoLd/pFZNMV+jEyKrG3jJAr4H6TfQ3XYOZ0r3uFGSI6kM
+zoHPJNNSETLrMPBjOn/HeuHSAvQpDKRuQXJwlvv8eHOS65r1jMJZn+efphbYvuZa
+x9NlR1piwoWcu2q1CZDpqR4AcGDMjVAqXD2v0f9ApMkAXKYBI5DbtM+OJEaiCQcJ
+ne1fRinytWLiRbFY3fKuf/KCWzJiGanrZVASHHTUuVuEFGVg0W0yVu2+0KwNmdhE
+CppYvXnCNsgd/lgXPPqievuCW3hlkPT9NxTKZRq6a67VqXR093o0aRlSV8n5lW0g
+YJFNcbdYs7Gvh3IkeHIfL7MfnKc2CJPyBSMpI23Qv9W5EU7uq3/IysU4Zv1A4V9T
+ajJ75zMYbMtVW9I8uYvoOixbDs3DeIoCuHoy5+7GLTQvUalpC90gTWWVaDWq+UNn
+APRAa2LdRVtWDwjqTLviygsBtcapLyRMGvEJgZLDnW48SUjpfPhH1rvtmWEMUz/c
+Q621rmixSyhurV23LwHLiVTU7pTXD10yJfIeJw85nWxdhVGkqx3ZB1HyCvaxHaLw
+XIcMUUkAwn68QgBwI4g8U2X1EXApQwxN3pd4IZBRG368OMj9LC/kiAySnGpEkH7P
+Sj37mU1OxTcWhCLx59tTZ0plwpuIOvO50UXyNoWlFL0PNqO7dits6ZzeV4KQKhcO
+nY5+b05Y7qT9ObsjtlO6i3awkrTHoe/Nfjh10NyIFrdfAsQtVrBn5Ua6UxATRFh1
+KCNG4CyMuTJc8T5bYEajghM9yoWLavu+LR2REHYbBRDkdlR10oxpKMMG2pKO8Spk
+Eon2qAwqd99ryriUSX5oc/Xmr/RyIWr2hWnbqOPq9z/n4frOPM7YM0A9KzOo3Cxz
+EAxjQ6WEUV7rTU/h+vkXBNCil09KS1IiIMWasch/dV4ei9n0UJZK1JNonJXsTBbZ
+XvNUlafiGLQq7ltiA5dOtpN4D6zYzR/4NHo5CBJSpK1STGlSo9lCo9SNf8QFdz8o
+0+79Xzg3XR6iTiP5iWfo+0nkbaEU7xL7YMYba1ODky6KtA2p4NEW9UyCfmZm34SI
+4StDFoGX+Jn9lW5KdRMSyXb0/ieNIh8fn+nqliHlcp/X3i7aqKAtLq5SNAyFUVMp
+m5nLPbjjVc7ivbNqgNsSyB00SPf/u7O/7qorYUI/S6NXsFIY8SJByS2/zE7W4lJp
+2RhX1/BbsnQ094Bcrexn91LYNKuJVDSLeU/IEyhz4OblDAiRBC2vr+NwAik9WS0v
+7MwfyqLzOoWbw3BrpvARV3/hPzGL3aN42zMnoqf83S3NdJ0Ir8XW8IUzUm7Hpbmd
+xaOoZnJcRk6giDAQ2j396p0IE0bV1pk98U9qHiee26bUKfzs5NF6alaAFBMd/uaK
+UupCgasawAYwEWgoNC1dwNhDtjyfj5zUVpbFIieS/1PxPIJBOyQZIRYmMAniFLCr
+cg/6ZiUF8lmuncMxD2VF3Ckdn9VdXXIC211q9rhSo7+iySAddxZRBhdDFGwtw2hS
+ujXiG0qD3uE3SzdArpm3KUNhl3+64e6o9N8dle0A8q4lJVg3Tl1F6NjlAkYrn4Oz
+Klys4TrRcAw4YiYd0/QG6jCE74n45NDqcgl4U4uk9jc+xE4l/lxjBTNKyhnscK+U
+rXeR/n3f/i+bDyhqW5Wktm8Ta7Nzs7a4J5ZFfnH0F7WDFyoNvuW0bsBSuGUShCf0
+QYe+Od86CX3zKlJEvC2jGIZeAQct6+JaZF5xIgP6s0OPlHPYH7B5VgetReksHbU4
+aLp394UtWo98mPaXdESTLWn0bp5MIBJL3vVr+LMcoBaJoHOPjuN6TacimJ9nnBbs
+wuLpH3/hVjnb6N4qhu6cF2moVv/+cW51uYIQLmp6eXRCFK+L0htp1mJ/mV6gg3Lj
+z7NmwqUzpuRF92HwvOJLymWNF5sAFzDTnu9iTmMw6GiUUr3dPyBC4acAgaXw44hc
+tUKei4g9j6uBpbxbNiwzqd51ycanulvsUqPIeelgdMGdA+rUOtvM4qyPXHxadDX0
+BciENL0t5VkJCWsSJL5dvZRy2njmlrR4T6Fv0LRaFo0QdWn4Q5l6mfggddVyS8kg
+9kLnvpIg6jVSBF3MGIJceM5mPFEn5WHBiMAYfpA4ScrCzuahOTJzQKIl3mjXXw1G
+XMLrCaWpJL4hyiEVDlvbs/zl34couw6GPDf2x3bf3RxenWZ2r9mLJuH/pF5TvnNY
+LCxvsmq6BOnmrt10Z8CVu4ZJ3e923nQLHT2zJPtM9T/MFnSTl2hishGcPEwjZsx8
+YKQvcFTLtOydG68BCvGghMrXQJc/uvPKngfLKK603CG7un1LaSqTEYTT2EN/xWwd
+cWz6c85K6DUL81AFvtdMjStahc/JlhkQoaTY8/qcksva64Hj/aBpq63w9n+gUqCh
+6W20HWso9pLRnbRa+sTJZj1qr8YFyBzqpZSSNxiAF00X04aaOFwN5oA+rqZdL6+J
+wQCRinA0e+WD237tm0dgzoFp5lrtbYyay4wqLTAQPQuCDUf3c6D/XtIzBBPDvu1/
+XmRLC3+FxP5NQx0z6cCk20aD9Bh1GTppdU5UF4RXGicMMCaEmhCjFzGl43TrWZzX
+7mhI6r4uJc6H2IVmlUoClGVM+c7sDjjtfXTWWbnLaFOs5NfP7f6yRa2rCCfCtcWX
+uLzLgDfE3ekOBrg9zVdmduABrUmGTx30RyHsPmW21lNCwbWXv7GkPPf4zIS/UctQ
+-----END RSA PRIVATE KEY-----
diff --git a/crypto/certificates/x509/test_files/noPasscodeBadCa.key b/crypto/certificates/x509/test_files/noPasscodeBadCa.key
new file mode 100644
index 00000000..90caebe6
--- /dev/null
+++ b/crypto/certificates/x509/test_files/noPasscodeBadCa.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/crypto/certificates/x509/test_files/noPasscodeCa.crt b/crypto/certificates/x509/test_files/noPasscodeCa.crt
new file mode 100644
index 00000000..f83c9bf5
--- /dev/null
+++ b/crypto/certificates/x509/test_files/noPasscodeCa.crt
@@ -0,0 +1,36 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
diff --git a/crypto/certificates/x509/test_files/noPasscodeCa.key b/crypto/certificates/x509/test_files/noPasscodeCa.key
new file mode 100644
index 00000000..056875b5
--- /dev/null
+++ b/crypto/certificates/x509/test_files/noPasscodeCa.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/crypto/certificates/x509/test_files/test.smallstep.com.csr b/crypto/certificates/x509/test_files/test.smallstep.com.csr
new file mode 100644
index 00000000..5e85b727
--- /dev/null
+++ b/crypto/certificates/x509/test_files/test.smallstep.com.csr
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH
+DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl
+c3Quc21hbGxzdGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ANPahliigZ38QpBLmQMS3MVKKZ5gapNjqR7LIEYoYWa4lTFiUnbwg8tSfIFcgLZr
+jNIxn7/98+JOJHKgS03NhFJoS5hej0LyypleOGJ0nk2qawYVKnn1ftoKjkfxkfZI
+a/5rsDF1jhNBspB/KPHWE0eimKQJbUiVG1zA1sExnXDecF3vJfBj+DPDWngx4yxR
+/jYEKjt4tQ6Ei752TbosrCHYeYXzkr6iAwiNz6vT/ewLb6b8JmuN8X6Y1I9ogDGx
+hntBJ1jAK8x3IGTjYbkm+mqVuCyhNcHtGfEHcBnUEzLAPrVFn8kGiAnU17FJ0uQ7
+1C9CtUzgBRZCxSBm6Qs+Zs8CAwEAAaCBjTCBigYJKoZIhvcNAQkOMX0wezAMBgNV
+HRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNVHQ8B
+Af8EBAMCBaAwHQYDVR0RBBYwFIISdGVzdC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQW
+BBQj6N4RTAAjhV3UBYXH72mkdOGpqzANBgkqhkiG9w0BAQsFAAOCAQEAN0/ivCBk
+FD53SqtRmqqc7C9saoRNvV+wDi4Sg6YGLFQLjbZPJrqQURWdHtV9O3sb3p8O5erX
+9Kgq3C7fqd//0mro4GZ1GTpjsPKIMocZFfH7zEhAZlvQLRKWICjoBaOwxQum2qY/
+B3+ltAXb4uqGdbI0jPkkyWGN5CQhK+ZHoYe/zGtTEmHBcPxRtJJkukQQjUgZhjU2
+Z7K+w3AjOxj47XLNHHlW83QYUJ2mN+mEZF9DhrZb2ydYOlpy0V2NJwv7QrmnFaDj
+R0v3BFLTblIp100li3oV2QaM/yESrgo9XIjEEGzCGz5cNs5ovNadufUZDCJyyT4q
+ZEp7knvU2osWRw==
+-----END CERTIFICATE REQUEST-----
diff --git a/crypto/certificates/x509/types.go b/crypto/certificates/x509/types.go
new file mode 100644
index 00000000..8201bf7d
--- /dev/null
+++ b/crypto/certificates/x509/types.go
@@ -0,0 +1,111 @@
+package x509
+
+import (
+ "crypto/tls"
+ "fmt"
+
+ "github.com/pkg/errors"
+)
+
+// ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer
+// x509 Certificate blocks.
+type ASN1DN struct {
+ Country string `step:"country"`
+ Organization string `step:"organization"`
+ OrganizationalUnit string `step:"organizationalUnit"`
+ Locality string `step:"locality"`
+ Province string `step:"province"`
+ StreetAddress string `step:"streetAddress"`
+ CommonName string `step:"commonName"`
+}
+
+// TLSVersion represents a TLS version number.
+type TLSVersion float64
+
+// Validate implements models.Validator and checks that a cipher suite is
+// valid.
+func (v TLSVersion) Validate() error {
+ if _, ok := tlsVersions[v]; ok {
+ return nil
+ }
+ return errors.Errorf("%f is not a valid tls version", v)
+}
+
+// Value returns the Go constant for the TLSVersion.
+func (v TLSVersion) Value() uint16 {
+ return tlsVersions[v]
+}
+
+// String returns the Go constant for the TLSVersion.
+func (v TLSVersion) String() string {
+ k := v.Value()
+ switch k {
+ case tls.VersionTLS10:
+ return "1.0"
+ case tls.VersionTLS11:
+ return "1.1"
+ case tls.VersionTLS12:
+ return "1.2"
+ default:
+ return fmt.Sprintf("unexpected value: %d", k)
+ }
+}
+
+// tlsVersions has the list of supported tls version.
+var tlsVersions = map[TLSVersion]uint16{
+ // Defaults to TLS 1.2
+ 0: tls.VersionTLS12,
+ // Options
+ 1.0: tls.VersionTLS10,
+ 1.1: tls.VersionTLS11,
+ 1.2: tls.VersionTLS12,
+}
+
+// CipherSuites represents an array of string codes representing the cipher
+// suites.
+type CipherSuites []string
+
+// Validate implements models.Validator and checks that a cipher suite is
+// valid.
+func (c CipherSuites) Validate() error {
+ for _, s := range c {
+ if _, ok := cipherSuites[s]; !ok {
+ return errors.Errorf("%s is not a valid cipher suite", s)
+ }
+ }
+ return nil
+}
+
+// Value returns an []uint16 for the cipher suites.
+func (c CipherSuites) Value() []uint16 {
+ values := make([]uint16, len(c))
+ for i, s := range c {
+ values[i] = cipherSuites[s]
+ }
+ return values
+}
+
+// cipherSuites has the list of supported cipher suites.
+var cipherSuites = map[string]uint16{
+ "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
+ "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+ "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
+ "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+ "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
+ "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
+ "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+ "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+ "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+ "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
+ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+ "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+ "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
+ "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+ "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+}
diff --git a/crypto/keys/clean_test.go b/crypto/keys/clean_test.go
new file mode 100644
index 00000000..219bc2dd
--- /dev/null
+++ b/crypto/keys/clean_test.go
@@ -0,0 +1,26 @@
+package keys
+
+import (
+ "io/ioutil"
+ "log"
+ "os"
+ "testing"
+)
+
+func TestMain(m *testing.M) {
+ // discard log output when testing
+ log.SetOutput(ioutil.Discard)
+
+ result := m.Run()
+
+ clean := func(files []string) {
+ for _, f := range files {
+ if _, err := os.Stat(f); !os.IsNotExist(err) {
+ os.Remove(f)
+ }
+ }
+ }
+ clean([]string{"./test.key"})
+
+ os.Exit(result)
+}
diff --git a/crypto/keys/key.go b/crypto/keys/key.go
new file mode 100644
index 00000000..0869f12d
--- /dev/null
+++ b/crypto/keys/key.go
@@ -0,0 +1,311 @@
+package keys
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "math/big"
+ "os"
+
+ "github.com/pkg/errors"
+ "golang.org/x/crypto/ed25519"
+)
+
+// DefaultPEMCipher is the default algorithm used when encrypting PEM blocks
+// by the CA.
+var (
+ DefaultPEMCipher = x509.PEMCipherAES128
+ // DefaultKeyType is the default type of a private key.
+ DefaultKeyType = "EC"
+ // DefaultKeySize is the default size (in # of bits) of a private key.
+ DefaultKeySize = 2048
+ // DefaultKeyCurve is the default curve of a private key.
+ DefaultKeyCurve = "P-256"
+
+ x509EncryptPEMBlock = x509.EncryptPEMBlock
+ x509MarshalECPrivateKey = x509.MarshalECPrivateKey
+)
+
+// PublicKey extracts a public key from a private key.
+func PublicKey(priv interface{}) (interface{}, error) {
+ switch k := priv.(type) {
+ case *rsa.PrivateKey:
+ return &k.PublicKey, nil
+ case *ecdsa.PrivateKey:
+ return &k.PublicKey, nil
+ default:
+ return nil, errors.Errorf("unrecognized key type: %T", priv)
+ }
+}
+
+// PrivatePEM will convert a private key and encryption password
+// to an encrypted pem block.
+// Nil encryption options is identical to InsecureEncOpts() -- so no encryption.
+func PrivatePEM(priv interface{}, e *EncOpts) (*pem.Block, error) {
+ var insecure bool
+ if e == nil {
+ e = InsecureEncOpts()
+ }
+ if e.pass == "" {
+ insecure = true
+ }
+ switch k := priv.(type) {
+ case *rsa.PrivateKey:
+ if insecure {
+ return &pem.Block{
+ Bytes: x509.MarshalPKCS1PrivateKey(k),
+ Type: "RSA PRIVATE KEY",
+ }, nil
+ }
+ return x509EncryptPEMBlock(rand.Reader, "RSA PRIVATE KEY",
+ x509.MarshalPKCS1PrivateKey(k), []byte(e.pass), e.cipher)
+ case *ecdsa.PrivateKey:
+ b, err := x509MarshalECPrivateKey(k)
+ if err != nil {
+ return nil, errors.Wrap(err, "Unable to marshal EC private key")
+ }
+ if insecure {
+ return &pem.Block{
+ Bytes: b,
+ Type: "EC PRIVATE KEY",
+ }, nil
+ }
+ return x509EncryptPEMBlock(rand.Reader, "EC PRIVATE KEY",
+ b, []byte(e.pass), e.cipher)
+ default:
+ return nil, errors.Errorf("Unrecognized key - type: %T, value: %v", priv, priv)
+ }
+}
+
+// EncOpts is a type containing options for encrypting secrets to disk.
+// The fields are a password and cipher used for encryption.
+type EncOpts struct {
+ pass string
+ cipher x509.PEMCipher
+}
+
+// InsecureEncOpts returns an empty EncOpts. Unspecified password indicates insecure.
+func InsecureEncOpts() *EncOpts {
+ return &EncOpts{}
+}
+
+// DefaultEncOpts populates the password field and sets a sane default
+// for the encryption cipher.
+func DefaultEncOpts(pass string) *EncOpts {
+ return &EncOpts{pass, x509.PEMCipherAES128}
+}
+
+// PublicPEM returns the public key in PEM block format.
+func PublicPEM(pub interface{}) (*pem.Block, error) {
+ pubBytes, err := x509.MarshalPKIXPublicKey(pub)
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+
+ return &pem.Block{
+ Bytes: pubBytes,
+ Type: "PUBLIC KEY",
+ }, nil
+}
+
+// GenerateDefaultKey generates a public/private key pair using sane defaults
+// for key type, curve, and size.
+func GenerateDefaultKey() (interface{}, error) {
+ return GenerateKey(DefaultKeyType, DefaultKeyCurve, DefaultKeySize)
+}
+
+// GenerateKey generates a key of the given type (kty).
+func GenerateKey(kty, crv string, size int) (interface{}, error) {
+ switch kty {
+ case "EC":
+ return generateECKey(crv)
+ case "RSA":
+ return generateRSAKey(size)
+ case "OKP":
+ return generateOKPKey(crv)
+ case "oct":
+ return generateOctKey(size)
+ default:
+ return nil, errors.Errorf("unrecognized key type: %s", kty)
+ }
+}
+
+func generateECKey(crv string) (interface{}, error) {
+ var c elliptic.Curve
+ switch crv {
+ case "P-256":
+ c = elliptic.P256()
+ case "P-384":
+ c = elliptic.P384()
+ case "P-521":
+ c = elliptic.P521()
+ default:
+ return nil, errors.Errorf("invalid value for argument crv (crv: '%s')", crv)
+ }
+
+ key, err := ecdsa.GenerateKey(c, rand.Reader)
+ if err != nil {
+ return nil, errors.Wrap(err, "error generating EC key")
+ }
+
+ return key, nil
+}
+
+func generateRSAKey(bits int) (interface{}, error) {
+ key, err := rsa.GenerateKey(rand.Reader, bits)
+ if err != nil {
+ return nil, errors.Wrap(err, "error generating RSA key")
+ }
+
+ return key, nil
+}
+
+func generateOKPKey(crv string) (interface{}, error) {
+ switch crv {
+ case "Ed25519":
+ _, key, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ return nil, errors.Wrap(err, "error generating Ed25519 key")
+ }
+
+ return key, nil
+ default:
+ return nil, errors.Errorf("missing or invalid value for argument 'crv'. "+
+ "expected 'Ed25519', but got '%s'", crv)
+ }
+}
+
+func generateOctKey(size int) (interface{}, error) {
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ result := make([]byte, size)
+ for i := range result {
+ num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
+ if err != nil {
+ return nil, err
+ }
+ result[i] = chars[num.Int64()]
+ }
+ return result, nil
+}
+
+// LoadPrivateKey loads a private key from a file.
+// The first argument is the ASN.1 DER formatted private key.
+// The second argument is a function that returns an encryption passphrase. If
+// the private key is not encrypted the second arg can be nil or simply return
+// an empty string. If the private key is encrypted then `getPass` should return
+// the decryptor.
+func LoadPrivateKey(bytes []byte, getPass func() (string, error)) (interface{}, error) {
+ p, _ := pem.Decode(bytes)
+ if p == nil {
+ return nil, errors.Errorf("invalid key - key is not PEM formatted")
+ }
+
+ // The following block is focused on getting the decrypted key bytes
+ // from the PEM block.
+ // 1. Check if the key bytes were encrypted.
+ // a) encrypted: go to 2.
+ // b) not encrypted: hakuna mata (we don't have to do much).
+ // 2. The key bytes are encrypted so we need a key to decrypt them.
+ // a) `pass` is empty therefore we request a decryption passphrase from
+ // stdin.
+ // 3. Decrypt the key bytes using either a password from stdin or one
+ // passed in as an agument.
+ var der []byte
+ if x509.IsEncryptedPEMBlock(p) {
+ if getPass == nil {
+ return nil, errors.Errorf("private key needs a decryption passphrase")
+ }
+ pass, err := getPass()
+ if err != nil {
+ return nil, err
+ }
+ der, err = x509.DecryptPEMBlock(p, []byte(pass))
+ if err != nil {
+ return nil, errors.WithStack(err)
+ }
+ } else {
+ der = p.Bytes
+ }
+
+ var (
+ err error
+ key interface{}
+ )
+ switch p.Type {
+ case "RSA PRIVATE KEY":
+ key, err = x509.ParsePKCS1PrivateKey(der)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error parsing RSA key")
+ }
+ case "EC PRIVATE KEY":
+ key, err = x509.ParseECPrivateKey(der)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error parsing EC key")
+ }
+ default:
+ return nil, errors.Errorf("unexpected key type: %s", p.Type)
+ }
+ return key, nil
+}
+
+// WritePrivateKey encodes a crypto private key to a file on disk in PEM format.
+// Any file with the same name will be overwritten.
+func WritePrivateKey(key interface{}, pass, out string) error {
+ // Remove any file with same name, if it exists.
+ // Permissions on private key files may be such that overwriting them is impossible.
+ if _, err := os.Stat(out); err == nil {
+ if err = os.Remove(out); err != nil {
+ return errors.WithStack(err)
+ }
+ }
+ keyOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
+ os.FileMode(0600))
+ if err != nil {
+ return errors.Wrapf(err,
+ "failed to open '%s' for writing", out)
+ }
+ privPem, err := PrivatePEM(key, DefaultEncOpts(pass))
+ if err != nil {
+ return errors.Wrap(err,
+ "failed to convert private key to PEM block")
+ }
+ err = pem.Encode(keyOut, privPem)
+ if err != nil {
+ return errors.Wrapf(err,
+ "pem encode '%s' failed", out)
+ }
+ keyOut.Close()
+ return nil
+}
+
+// WritePublicKey encodes a crypto public key to a file on disk in PEM format.
+// Any file with the same name will be overwritten.
+func WritePublicKey(key interface{}, out string) error {
+ // Remove any file with same name, if it exists.
+ if _, err := os.Stat(out); err == nil {
+ if err = os.Remove(out); err != nil {
+ return errors.WithStack(err)
+ }
+ }
+ keyOut, err := os.OpenFile(out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
+ os.FileMode(0600))
+ if err != nil {
+ return errors.Wrapf(err,
+ "failed to open '%s' for writing", out)
+ }
+ pubPEM, err := PublicPEM(key)
+ if err != nil {
+ return errors.Wrap(err,
+ "failed to convert public key to PEM block")
+ }
+ err = pem.Encode(keyOut, pubPEM)
+ if err != nil {
+ return errors.Wrapf(err,
+ "pem encode '%s' failed", out)
+ }
+ keyOut.Close()
+ return nil
+}
diff --git a/crypto/keys/keyPair.go b/crypto/keys/keyPair.go
new file mode 100644
index 00000000..c224df00
--- /dev/null
+++ b/crypto/keys/keyPair.go
@@ -0,0 +1,24 @@
+package keys
+
+import (
+ "github.com/pkg/errors"
+)
+
+// GenerateDefaultKeyPair generates a public/private key pair using configured
+// default values for key type, curve, and size.
+func GenerateDefaultKeyPair() (interface{}, interface{}, error) {
+ return GenerateKeyPair(DefaultKeyType, DefaultKeyCurve, DefaultKeySize)
+}
+
+// GenerateKeyPair creates an asymmetric crypto keypair using input configuration.
+func GenerateKeyPair(kty, crv string, size int) (interface{}, interface{}, error) {
+ priv, err := GenerateKey(kty, crv, size)
+ if err != nil {
+ return nil, nil, errors.WithStack(err)
+ }
+ pub, err := PublicKey(priv)
+ if err != nil {
+ return nil, nil, errors.WithStack(err)
+ }
+ return pub, priv, err
+}
diff --git a/crypto/keys/key_test.go b/crypto/keys/key_test.go
new file mode 100644
index 00000000..f2a6c9f4
--- /dev/null
+++ b/crypto/keys/key_test.go
@@ -0,0 +1,414 @@
+package keys
+
+import (
+ "crypto/ecdsa"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "io"
+ "io/ioutil"
+ "os"
+ "testing"
+
+ "github.com/pkg/errors"
+ "github.com/smallstep/assert"
+)
+
+func Test_GenerateKey_unrecognizedkt(t *testing.T) {
+ var failTests = []struct {
+ kt string
+ crv string
+ bits int
+ expected string
+ }{
+ {"shake and bake", "", 2048, "unrecognized key type: shake and bake"},
+ {"EC", "P-12", 0, "invalid value for argument crv (crv: 'P-12')"},
+ }
+
+ for i, tc := range failTests {
+ k, err := GenerateKey(tc.kt, tc.crv, tc.bits)
+ if assert.Error(t, err, i) {
+ assert.HasPrefix(t, err.Error(), tc.expected)
+ assert.Nil(t, k)
+ }
+ }
+
+ var ecdsaTests = []struct {
+ kt string
+ crv string
+ }{
+ {"EC", "P-256"},
+ {"EC", "P-384"},
+ {"EC", "P-521"},
+ }
+
+ for i, tc := range ecdsaTests {
+ k, err := GenerateKey(tc.kt, tc.crv, 0)
+ if assert.NoError(t, err, i) {
+ _, ok := k.(*ecdsa.PrivateKey)
+ assert.True(t, ok, i)
+ }
+ }
+
+ k, err := GenerateKey("RSA", "", 2048)
+ if assert.NoError(t, err) {
+ _, ok := k.(*rsa.PrivateKey)
+ assert.True(t, ok)
+ }
+}
+
+func Test_PrivatePEM(t *testing.T) {
+ oldX509EncryptPEMBlock := x509EncryptPEMBlock
+ oldX509MarshalECPrivateKey := x509MarshalECPrivateKey
+
+ var clean = func() {
+ x509EncryptPEMBlock = oldX509EncryptPEMBlock
+ x509MarshalECPrivateKey = oldX509MarshalECPrivateKey
+ }
+ defer clean()
+
+ tests := map[string]struct {
+ key func() (interface{}, error)
+ pass string
+ cipher x509.PEMCipher
+ setup func()
+ clean func()
+ err error
+ }{
+ "unrecognized key type": {
+ key: func() (interface{}, error) {
+ return "shake and bake", nil
+ },
+ pass: "pass",
+ cipher: x509.PEMCipherAES256,
+ err: errors.New("Unrecognized key - type: string, value: shake and bake"),
+ },
+ "RSA: encrypt PEM block error": {
+ key: func() (interface{}, error) {
+ return GenerateKey("RSA", "", 1024)
+ },
+ pass: "pass",
+ cipher: x509.PEMCipherAES256,
+ setup: func() {
+ x509EncryptPEMBlock = func(r io.Reader, s string, d, p []byte, alg x509.PEMCipher) (*pem.Block, error) {
+ return nil, errors.Errorf("force EncryptPEMBlock error")
+ }
+ },
+ clean: clean,
+ err: errors.New("force EncryptPEMBlock error"),
+ },
+ "EC: marshal key error": {
+ key: func() (interface{}, error) {
+ return GenerateKey("EC", "P-256", 0)
+ },
+ pass: "pass",
+ cipher: x509.PEMCipherAES256,
+ setup: func() {
+ x509MarshalECPrivateKey = func(k *ecdsa.PrivateKey) ([]byte, error) {
+ return nil, errors.Errorf("force MarshalECPrivateKey error")
+ }
+ },
+ clean: clean,
+ err: errors.New("Unable to marshal EC private key: force MarshalECPrivateKey error"),
+ },
+ "EC: encrypt PEM block error": {
+ key: func() (interface{}, error) {
+ return GenerateKey("EC", "P-256", 0)
+ },
+ pass: "pass",
+ cipher: x509.PEMCipherAES256,
+ setup: func() {
+ x509EncryptPEMBlock = func(r io.Reader, s string, d, p []byte, alg x509.PEMCipher) (*pem.Block, error) {
+ return nil, errors.Errorf("force EncryptPEMBlock error")
+ }
+ },
+ clean: clean,
+ err: errors.New("force EncryptPEMBlock error"),
+ },
+ "RSA: empty password generates unencrypted PEM block - success": {
+ key: func() (interface{}, error) {
+ return GenerateKey("RSA", "", 1024)
+ },
+ },
+ "EC: empty password generates unencrypted PEM block - success": {
+ key: func() (interface{}, error) {
+ return GenerateKey("EC", "P-256", 0)
+ },
+ },
+ "EC: encrypt - success": {
+ key: func() (interface{}, error) {
+ return GenerateKey("EC", "P-256", 0)
+ },
+ pass: "pass",
+ cipher: x509.PEMCipherAES256,
+ },
+ "RSA: encrypt - success": {
+ key: func() (interface{}, error) {
+ return GenerateKey("RSA", "", 256)
+ },
+ pass: "pass",
+ cipher: x509.PEMCipherAES256,
+ },
+ }
+
+ for name, test := range tests {
+ t.Logf("Running test case: %s", name)
+
+ priv, err := test.key()
+ assert.FatalError(t, err)
+
+ if test.setup != nil {
+ test.setup()
+ }
+ p, err := PrivatePEM(priv, &EncOpts{test.pass, test.cipher})
+ if test.clean != nil {
+ test.clean()
+ }
+ if err != nil {
+ if assert.NotNil(t, test.err) {
+ assert.HasPrefix(t, err.Error(), test.err.Error())
+ }
+ } else {
+ if assert.Nil(t, test.err) {
+ switch k := priv.(type) {
+ case *rsa.PrivateKey:
+ if test.pass == "" {
+ assert.False(t, x509.IsEncryptedPEMBlock(p))
+ assert.Equals(t, p.Type, "RSA PRIVATE KEY")
+ assert.Equals(t, p.Bytes, x509.MarshalPKCS1PrivateKey(k))
+ } else {
+ assert.True(t, x509.IsEncryptedPEMBlock(p))
+ 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))
+ assert.FatalError(t, err)
+ assert.Equals(t, der, x509.MarshalPKCS1PrivateKey(k))
+ }
+ case *ecdsa.PrivateKey:
+ if test.pass == "" {
+ assert.False(t, x509.IsEncryptedPEMBlock(p))
+ assert.Equals(t, p.Type, "EC PRIVATE KEY")
+
+ b, err := x509MarshalECPrivateKey(k)
+ assert.FatalError(t, err)
+ assert.Equals(t, p.Bytes, b)
+ } else {
+ assert.True(t, x509.IsEncryptedPEMBlock(p))
+ assert.Equals(t, p.Type, "EC PRIVATE KEY")
+ assert.Equals(t, p.Headers["Proc-Type"], "4,ENCRYPTED")
+
+ der, err := x509.DecryptPEMBlock(p, []byte(test.pass))
+ assert.FatalError(t, err)
+ plain, err := x509.MarshalECPrivateKey(k)
+ assert.FatalError(t, err)
+ assert.Equals(t, der, plain)
+ }
+ default:
+ t.Errorf("Unrecognized key - type: %T, value: %v", k, k)
+ }
+ }
+ }
+ }
+}
+
+// empty private key path throws error
+func Test_LoadPrivateKey(t *testing.T) {
+ tests := map[string]struct {
+ bytes []byte
+ getPass func() (string, error)
+ err error
+ resultBytes []byte
+ }{
+ "input bytes are not PEM formatted": {
+ bytes: nil,
+ err: errors.New("invalid key - key is not PEM formatted"),
+ },
+ "getPass is nil": {
+ bytes: []byte(`-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,544d87909c73e30d2f43fe21b6bc6caf
+
+uir6r8TrhNuXTs6ZF2avaqaLX5cpuh+oTyoS+5Wk0Cbr3NQuAg1l/ZuRI8NWKBt2
+nnAXpKSOVnfut9i95ifaOf38mzdDKf8r8vAAVlT9nduzizTcc25Bst2ljSuSuOMP
+eCye1+g47hWU2hbxKF1NH9LfJsS7W90LKEuSCKZljz0=
+-----END EC PRIVATE KEY-----`),
+ getPass: nil,
+ err: errors.New("private key needs a decryption passphrase"),
+ },
+ "propagate getPass error": {
+ bytes: []byte(`-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,544d87909c73e30d2f43fe21b6bc6caf
+
+uir6r8TrhNuXTs6ZF2avaqaLX5cpuh+oTyoS+5Wk0Cbr3NQuAg1l/ZuRI8NWKBt2
+nnAXpKSOVnfut9i95ifaOf38mzdDKf8r8vAAVlT9nduzizTcc25Bst2ljSuSuOMP
+eCye1+g47hWU2hbxKF1NH9LfJsS7W90LKEuSCKZljz0=
+-----END EC PRIVATE KEY-----`),
+ getPass: func() (string, error) {
+ return "", errors.Errorf("force getPass error")
+ },
+ err: errors.New("force getPass error"),
+ },
+ "propagate decryptPEMBlock error": {
+ bytes: []byte(`-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,544d87909c73e30d2f43fe21b6bc6caf
+
+uir6r8TrhNuXTs6ZF2avaqaLX5cpuh+oTyoS+5Wk0Cbr3NQuAg1l/ZuRI8NWKBt2
+nnAXpKSOVnfut9i95ifaOf38mzdDKf8r8vAAVlT9nduzizTcc25Bst2ljSuSuOMP
+eCye1+g47hWU2hbxKF1NH9LfJsS7W90LKEuSCKZljz0=
+-----END EC PRIVATE KEY-----`),
+ getPass: func() (string, error) {
+ return "ricky-bobby", nil
+ },
+ err: errors.New("x509: decryption password incorrect"),
+ },
+ "EC: encrypted success": {
+ bytes: []byte(`-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,544d87909c73e30d2f43fe21b6bc6caf
+
+uir6r8TrhNuXTs6ZF2avaqaLX5cpuh+oTyoS+5Wk0Cbr3NQuAg1l/ZuRI8NWKBt2
+nnAXpKSOVnfut9i95ifaOf38mzdDKf8r8vAAVlT9nduzizTcc25Bst2ljSuSuOMP
+eCye1+g47hWU2hbxKF1NH9LfJsS7W90LKEuSCKZljz0=
+-----END EC PRIVATE KEY-----`),
+ resultBytes: []byte{48, 119, 2, 1, 1, 4, 32, 143, 215, 97, 167, 20, 68, 24, 34, 8, 221, 52, 8, 69, 10, 212, 144, 108, 53, 76, 164, 150, 247, 133, 247, 71, 39, 38, 148, 250, 36, 124, 179, 160, 10, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 161, 68, 3, 66, 0, 4, 181, 104, 199, 201, 196, 120, 38, 193, 82, 208, 46, 198, 19, 153, 195, 113, 207, 143, 99, 212, 191, 242, 188, 32, 78, 244, 154, 187, 49, 128, 223, 46, 11, 21, 137, 216, 237, 138, 98, 170, 118, 224, 239, 136, 220, 61, 82, 84, 223, 146, 99, 150, 190, 165, 18, 63, 69, 21, 10, 52, 48, 16, 128, 173},
+ getPass: func() (string, error) {
+ return "pass", nil
+ },
+ },
+ "EC: un-encrypted success": {
+ bytes: []byte(`-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIJhTbozTaR/CoOj7yZIhyteXaYXxW/4RF6D/pMA2y4XeoAoGCCqGSM49
+AwEHoUQDQgAERpymPrkl64FD4vPlljJaoc5rMhTdWZsk/G1H/X7mDtlDhoHmrRp5
+aJ6EbWtKrZ0+Eb82rZW207IzoTJFnpFPdA==
+-----END EC PRIVATE KEY-----`),
+ resultBytes: []byte{48, 119, 2, 1, 1, 4, 32, 152, 83, 110, 140, 211, 105, 31, 194, 160, 232, 251, 201, 146, 33, 202, 215, 151, 105, 133, 241, 91, 254, 17, 23, 160, 255, 164, 192, 54, 203, 133, 222, 160, 10, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 161, 68, 3, 66, 0, 4, 70, 156, 166, 62, 185, 37, 235, 129, 67, 226, 243, 229, 150, 50, 90, 161, 206, 107, 50, 20, 221, 89, 155, 36, 252, 109, 71, 253, 126, 230, 14, 217, 67, 134, 129, 230, 173, 26, 121, 104, 158, 132, 109, 107, 74, 173, 157, 62, 17, 191, 54, 173, 149, 182, 211, 178, 51, 161, 50, 69, 158, 145, 79, 116},
+ },
+ "RSA: encrypted success": {
+ bytes: []byte(`-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,dfc9d24f6f09f401a4bf297098e414ed
+
+4z6W9hmXmJNj1gNhpzwZy2GWWmzFUrzRmUHZmpJkpW1zObaDWyhlC0dhoexPUHyM
+elVwEtIfk4hHW/KUHru5yx83ce8Y29I/FTvYlLjK4R/fF4hskrWyNKOy4WRFeSFy
++h8SqjM+KnTpRrhpZarGaGc32RCSCE1OK0KtFVKXNYYyTQLuD2sEPCR0hj9SkgAq
+KsYKSvCl9KJR27iilvhXQ7UvuE32OReth6uJfcyF4uY=
+-----END RSA PRIVATE KEY-----`),
+ resultBytes: []byte{48, 129, 170, 2, 1, 0, 2, 33, 0, 182, 202, 226, 207, 7, 173, 171, 119, 158, 212, 232, 208, 16, 216, 31, 13, 248, 121, 11, 253, 97, 212, 247, 249, 39, 165, 50, 207, 197, 128, 154, 41, 2, 3, 1, 0, 1, 2, 32, 123, 241, 3, 106, 231, 68, 237, 175, 181, 69, 157, 250, 126, 129, 92, 68, 1, 100, 255, 251, 15, 212, 34, 174, 1, 128, 65, 119, 19, 78, 225, 1, 2, 17, 0, 198, 241, 220, 208, 114, 45, 98, 128, 135, 231, 202, 212, 92, 40, 29, 161, 2, 17, 0, 235, 55, 40, 19, 226, 0, 0, 15, 75, 147, 144, 130, 48, 202, 95, 137, 2, 17, 0, 153, 133, 24, 157, 238, 13, 225, 190, 71, 161, 242, 30, 47, 195, 113, 33, 2, 16, 41, 254, 214, 11, 254, 188, 203, 69, 239, 211, 111, 232, 158, 183, 115, 41, 2, 16, 102, 42, 237, 52, 125, 228, 216, 129, 192, 79, 251, 183, 46, 44, 230, 140},
+ getPass: func() (string, error) {
+ return "pass", nil
+ },
+ },
+ "RSA: un-encrypted success": {
+ bytes: []byte(`-----BEGIN RSA PRIVATE KEY-----
+MIGsAgEAAiEA1Yz+DRsCJlrxp+Rka52JIPkaCCIbyj4+EUHao/dyGvMCAwEAAQIg
+BUxcOUMESKNU/49hFnJwJn+tfLwOQzu7ozdGe8aclVECEQDq33SrABt4+DOGPTJS
+1Cx9AhEA6MKKZ9Y0mLy+Gc/5gAiwLwIRAJTGcNd0nPJWfgS1NPBEl90CEQC3Sjrj
+efMBM+AfQ38eK7lRAhEA32j/3N2dXwOlytURq2yl+Q==
+-----END RSA PRIVATE KEY-----`),
+ resultBytes: []byte{48, 129, 172, 2, 1, 0, 2, 33, 0, 213, 140, 254, 13, 27, 2, 38, 90, 241, 167, 228, 100, 107, 157, 137, 32, 249, 26, 8, 34, 27, 202, 62, 62, 17, 65, 218, 163, 247, 114, 26, 243, 2, 3, 1, 0, 1, 2, 32, 5, 76, 92, 57, 67, 4, 72, 163, 84, 255, 143, 97, 22, 114, 112, 38, 127, 173, 124, 188, 14, 67, 59, 187, 163, 55, 70, 123, 198, 156, 149, 81, 2, 17, 0, 234, 223, 116, 171, 0, 27, 120, 248, 51, 134, 61, 50, 82, 212, 44, 125, 2, 17, 0, 232, 194, 138, 103, 214, 52, 152, 188, 190, 25, 207, 249, 128, 8, 176, 47, 2, 17, 0, 148, 198, 112, 215, 116, 156, 242, 86, 126, 4, 181, 52, 240, 68, 151, 221, 2, 17, 0, 183, 74, 58, 227, 121, 243, 1, 51, 224, 31, 67, 127, 30, 43, 185, 81, 2, 17, 0, 223, 104, 255, 220, 221, 157, 95, 3, 165, 202, 213, 17, 171, 108, 165, 249},
+ },
+ }
+ for name, test := range tests {
+ t.Logf("Running test case: %s", name)
+
+ key, err := LoadPrivateKey(test.bytes, test.getPass)
+ if err != nil {
+ if assert.NotNil(t, test.err) {
+ assert.HasPrefix(t, err.Error(), test.err.Error())
+ }
+ } else {
+ if assert.Nil(t, test.err) {
+ switch k := key.(type) {
+ case *ecdsa.PrivateKey:
+ b, err := x509.MarshalECPrivateKey(k)
+ assert.FatalError(t, err)
+ assert.Equals(t, b, test.resultBytes)
+ case *rsa.PrivateKey:
+ assert.Equals(t, x509.MarshalPKCS1PrivateKey(k), test.resultBytes)
+ default:
+ t.Errorf("unrecognized key type %T", k)
+ }
+ }
+ }
+ }
+}
+
+func Test_WriteKey(t *testing.T) {
+ keyOut := "./test.key"
+ pass := "pass"
+
+ tests := map[string]struct {
+ key func() (interface{}, error)
+ keyOut string
+ pass string
+ err error
+ }{
+ "propagate open key out file error": {
+ key: func() (interface{}, error) {
+ return GenerateKey(DefaultKeyType, DefaultKeyCurve, 0)
+ },
+ keyOut: "./fakeDir/test.key",
+ err: errors.New("failed to open './fakeDir/test.key' for writing: open ./fakeDir/test.key: no such file or directory"),
+ },
+ "propagate encrypt key error": {
+ key: func() (interface{}, error) {
+ return GenerateKey(DefaultKeyType, DefaultKeyCurve, 0)
+ },
+ pass: pass,
+ keyOut: keyOut,
+ err: errors.New("failed to convert private key to PEM block: encryption passphrase cannot be empty"),
+ },
+ "success": {
+ key: func() (interface{}, error) {
+ return GenerateKey(DefaultKeyType, DefaultKeyCurve, 0)
+ },
+ keyOut: keyOut,
+ pass: pass,
+ err: errors.New("failed to convert private key to PEM block: encryption passphrase cannot be empty"),
+ },
+ }
+
+ for name, test := range tests {
+ t.Logf("Running test case: %s", name)
+
+ key, err := test.key()
+ assert.FatalError(t, err)
+
+ err = WritePrivateKey(key, test.pass, test.keyOut)
+ if err != nil {
+ if assert.NotNil(t, test.err) {
+ assert.HasPrefix(t, err.Error(), test.err.Error())
+ }
+ } else {
+ // Check key permissions
+ fileInfo, err := os.Stat(test.keyOut)
+ assert.FatalError(t, err)
+ fileMode := fileInfo.Mode()
+ if fileMode != 0600 {
+ t.Errorf("FileMode mismatch for file %s -- expected: `%d`, but got: `%d`",
+ test.keyOut, fileMode, 0600)
+ }
+
+ switch k := key.(type) {
+ case *ecdsa.PrivateKey:
+ // Verify that key written to file is correct
+ plain, err := x509.MarshalECPrivateKey(k)
+ assert.FatalError(t, err)
+ keyFileBytes, err := ioutil.ReadFile(test.keyOut)
+ assert.FatalError(t, err)
+ pemKey, _ := pem.Decode(keyFileBytes)
+ assert.True(t, x509.IsEncryptedPEMBlock(pemKey))
+ assert.Equals(t, pemKey.Type, "EC PRIVATE KEY")
+ assert.Equals(t, pemKey.Headers["Proc-Type"], "4,ENCRYPTED")
+ der, err := x509.DecryptPEMBlock(pemKey, []byte(pass))
+ assert.FatalError(t, err)
+ assert.Equals(t, der, plain)
+ default:
+ t.Errorf("unexpected key type %T", k)
+ }
+ }
+ }
+}
diff --git a/crypto/random.go b/crypto/random.go
new file mode 100644
index 00000000..1076acd5
--- /dev/null
+++ b/crypto/random.go
@@ -0,0 +1,46 @@
+package crypto
+
+import (
+ "crypto/rand"
+ "math/big"
+)
+
+// GenerateRandomASCIIString returns a securely generated random ASCII string.
+// It reads random numbers from crypto/rand and searches for printable characters.
+// It will return an error if the system's secure random number generator fails to
+// function correctly, in which case the caller must not continue.
+func GenerateRandomASCIIString(length int) (string, error) {
+ result := ""
+ for {
+ if len(result) >= length {
+ return result, nil
+ }
+ num, err := rand.Int(rand.Reader, big.NewInt(int64(127)))
+ if err != nil {
+ return "", err
+ }
+ n := num.Int64()
+ // Make sure that the number/byte/letter is inside
+ // the range of printable ASCII characters (excluding space and DEL)
+ if n > 32 && n < 127 {
+ result += string(n)
+ }
+ }
+}
+
+// GenerateRandomRestrictedString returns a securely generated random ASCII string.
+// It reads random numbers from crypto/rand and searches for printable characters.
+// It will return an error if the system's secure random number generator fails to
+// function correctly, in which case the caller must not continue.
+func GenerateRandomRestrictedString(length int) (string, error) {
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ result := make([]byte, length)
+ for i := range result {
+ num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
+ if err != nil {
+ return "", err
+ }
+ result[i] = chars[num.Int64()]
+ }
+ return string(result), nil
+}
diff --git a/crypto/test_utils/key/badca_rsa_key b/crypto/test_utils/key/badca_rsa_key
new file mode 100644
index 00000000..74aae1d2
--- /dev/null
+++ b/crypto/test_utils/key/badca_rsa_key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,6D4455D3019FC904
+
+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
+-----END RSA PRIVATE KEY-----
diff --git a/crypto/test_utils/key/badpem_rsa_key b/crypto/test_utils/key/badpem_rsa_key
new file mode 100644
index 00000000..a31402a3
--- /dev/null
+++ b/crypto/test_utils/key/badpem_rsa_key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,6D4455D3019FC904
+
+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
+-----END RSA PRIVATE KEY-----
diff --git a/crypto/test_utils/key/ca_ecdsa_key b/crypto/test_utils/key/ca_ecdsa_key
new file mode 100644
index 00000000..ae532319
--- /dev/null
+++ b/crypto/test_utils/key/ca_ecdsa_key
@@ -0,0 +1,8 @@
+-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,15FAB0B97397621DF6DE7E2DAAAC0313
+
+BOicR28Sb+aRZIOjvRc6KSH08teBRFPfKnMy0nqbcom/fLqAjA/2rs5SQ3tjO4UR
+YpddJsO3g9rnlyi28xY1nDqtjs41zRK5LmhJP75V5We4G+Bm0f+mHQTF6BOmS4Dv
+CwqNz77I95A/LAPimyQV1PJW0z52Yw/NzXYMWJG4vEs=
+-----END EC PRIVATE KEY-----
diff --git a/crypto/test_utils/key/ca_rsa_key b/crypto/test_utils/key/ca_rsa_key
new file mode 100644
index 00000000..85e782cb
--- /dev/null
+++ b/crypto/test_utils/key/ca_rsa_key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-256-CBC,36945dfbb2645820d163fad834a3d290
+
+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
+-----END RSA PRIVATE KEY-----
diff --git a/crypto/test_utils/key/noPasscodeBadCa_rsa_key b/crypto/test_utils/key/noPasscodeBadCa_rsa_key
new file mode 100644
index 00000000..90caebe6
--- /dev/null
+++ b/crypto/test_utils/key/noPasscodeBadCa_rsa_key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/crypto/test_utils/key/noPasscodeCA_ecdsa_key b/crypto/test_utils/key/noPasscodeCA_ecdsa_key
new file mode 100644
index 00000000..95241d02
--- /dev/null
+++ b/crypto/test_utils/key/noPasscodeCA_ecdsa_key
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIEZVK8QMUOQK09NkTsoGXCBeniAERbR2eow5OB66qQqeoAoGCCqGSM49
+AwEHoUQDQgAETbf9/jeZxYmhPeLsfCP77x7vvfLw1NOd+eMzJTdIqUletHs3LSHU
+0D3b/fMlRJq498hFf7brnwQXfJiW9+gNng==
+-----END EC PRIVATE KEY-----
diff --git a/crypto/test_utils/key/noPasscodeCa_rsa_key b/crypto/test_utils/key/noPasscodeCa_rsa_key
new file mode 100644
index 00000000..056875b5
--- /dev/null
+++ b/crypto/test_utils/key/noPasscodeCa_rsa_key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/errs/errs.go b/errs/errs.go
new file mode 100644
index 00000000..5bf28475
--- /dev/null
+++ b/errs/errs.go
@@ -0,0 +1,256 @@
+package errs
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/pkg/errors"
+ "github.com/urfave/cli"
+)
+
+// err errExitCode is the default exit code when an error occurs.
+const errExitCode = 1
+
+// ErrTooFewArgs occurs when too few arguments were provided by the user
+var ErrTooFewArgs = NewError("Not enough arguments were provided")
+
+// ErrTooManyArgs occurs when too many arguments were provided by the user
+var ErrTooManyArgs = NewError("Too many arguments were provided")
+
+// ErrMissingArgs occurs when one or more arguments are missing
+var ErrMissingArgs = NewError("An incorrect number of arguments were provided")
+
+// ErrMissingToken occurs when a STEP_TOKEN or --token flag is not provided
+var ErrMissingToken = NewError("A one-time token must be provided to bootstrap the identity via the `--token` flag or `$STEP_TOKEN` environment variable")
+
+// ErrMissingCAURL occurs when a STEP_CA_URL or --ca-url flag is not provided
+var ErrMissingCAURL = NewError("The CA URL must be provided through the --ca-url flag or `$STEP_CA_URL` environment variable")
+
+// NewError returns a new Error for the given format and arguments
+func NewError(format string, args ...interface{}) error {
+ return errors.Errorf(format, args...)
+}
+
+// NewExitError returns an error than the urfave/cli package will handle and
+// will show the given error and exit with the given code.
+func NewExitError(err error, exitCode int) error {
+ return cli.NewExitError(err, exitCode)
+}
+
+// Wrap returns a new error wrapped by the given error with the given message.
+// If the given error implements the errors.Cause interface, the base error is
+// used. If the given error is wrapped by a package name, the error wrapped
+// will be the string after the last colon.
+func Wrap(err error, format string, args ...interface{}) error {
+ cause := errors.Cause(err)
+ if cause == err {
+ str := err.Error()
+ if i := strings.LastIndexByte(str, ':'); i >= 0 {
+ str = strings.TrimSpace(str[i:])
+ return errors.Wrapf(fmt.Errorf(str), format, args...)
+ }
+ }
+ return errors.Wrapf(cause, format, args...)
+}
+
+// UsageExitError prints out the usage error followed by the help documentation
+// for the command
+func UsageExitError(c *cli.Context, err error) error {
+ msg := fmt.Sprintf("Error: %s\n\n%s", err.Error(), usageString(c))
+ return cli.NewExitError(msg, errExitCode)
+}
+
+// UnexpectedExitError wraps the error denoting that it was unexpected
+func UnexpectedExitError(err error) error {
+ msg := fmt.Sprintf("Error: An unexpected error was encountered: %s", err.Error())
+ return cli.NewExitError(msg, errExitCode)
+}
+
+// ToError transforms the given error into our frameworks error type
+func ToError(err error) error {
+ switch err.(type) {
+ case nil:
+ return nil
+ default:
+ return cli.NewExitError(prependErrorMsg(err), errExitCode)
+ }
+}
+
+func prependErrorMsg(err error) string {
+ m := err.Error()
+ if strings.HasPrefix(m, "Error:") {
+ return m
+ }
+
+ return "Error: " + m
+}
+
+// InsecureCommand returns an error with a message saying that the current
+// command requires the insecure flag.
+func InsecureCommand(ctx *cli.Context) error {
+ return errors.Errorf("'%s %s' requires the '--insecure' flag", ctx.App.Name, ctx.Command.Name)
+}
+
+// EqualArguments returns an error saying that the given positional arguments
+// cannot be equal.
+func EqualArguments(ctx *cli.Context, arg1, arg2 string) error {
+ return errors.Errorf("positional arguments <%s> and <%s> cannot be equal in '%s'", arg1, arg2, usage(ctx))
+}
+
+// MissingArguments returns an error with a missing arguments message for the
+// given positional argument names.
+func MissingArguments(ctx *cli.Context, argNames ...string) error {
+ switch len(argNames) {
+ case 0:
+ return errors.Errorf("missing positional arguments in '%s'", usage(ctx))
+ case 1:
+ return errors.Errorf("missing positional argument <%s> in '%s'", argNames[0], usage(ctx))
+ default:
+ args := make([]string, len(argNames))
+ for i, name := range argNames {
+ args[i] = "<" + name + ">"
+ }
+ return errors.Errorf("missing positional argument %s in '%s'", strings.Join(args, " "), usage(ctx))
+ }
+}
+
+// NumberOfArguments returns nil if the number of positional arguments is
+// equal to the required one. It will return an appropriate error if they are
+// not.
+func NumberOfArguments(ctx *cli.Context, required int) error {
+ n := ctx.NArg()
+ switch {
+ case n < required:
+ return TooFewArguments(ctx)
+ case n > required:
+ return TooManyArguments(ctx)
+ default:
+ return nil
+ }
+}
+
+// TooFewArguments returns an error with a few arguments were provided message.
+func TooFewArguments(ctx *cli.Context) error {
+ return errors.Errorf("not enough positional arguments were provided in '%s'", usage(ctx))
+}
+
+// TooManyArguments returns an error with a too many arguments were provided
+// message.
+func TooManyArguments(ctx *cli.Context) error {
+ return errors.Errorf("too many positional arguments were provided in '%s'", usage(ctx))
+}
+
+// InsecureArgument returns an error with the given argument requiring the
+// --insecure flag.
+func InsecureArgument(ctx *cli.Context, name string) error {
+ return errors.Errorf("positional argument <%s> requires the '--insecure' flag", name)
+}
+
+// FlagValueInsecure returns an error with the given flag and value requiring
+// the --insecure flag.
+func FlagValueInsecure(ctx *cli.Context, flag string, value string) error {
+ return errors.Errorf("flag '--%s %s' requires the '--insecure' flag", flag, value)
+}
+
+// InvalidFlagValue returns an error with the given value being missing or
+// invalid for the given flag. Optionally it lists the given formated options
+// at the end.
+func InvalidFlagValue(ctx *cli.Context, flag string, value string, options string) error {
+ var format string
+ if len(value) == 0 {
+ format = fmt.Sprintf("missing value for flag '--%s'", flag)
+ } else {
+ format = fmt.Sprintf("invalid value '%s' for flag '--%s'", value, flag)
+ }
+
+ if len(options) == 0 {
+ return errors.New(format)
+ }
+
+ return errors.New(format + " options are " + options)
+}
+
+// IncompatibleFlag returns an error with the flag being incompatible with the
+// given value.
+func IncompatibleFlag(ctx *cli.Context, flag string, value string) error {
+ return errors.Errorf("flag '--%s' is incompatible with '%s'", flag, value)
+}
+
+// RequiredFlag returns an error with the required flag message.
+func RequiredFlag(ctx *cli.Context, flag string) error {
+ return errors.Errorf("'%s %s' requires the '--%s' flag", ctx.App.HelpName,
+ ctx.Command.Name, flag)
+}
+
+// RequiredWithFlag returns an error with the required flag message with another flag.
+func RequiredWithFlag(ctx *cli.Context, required, with string) error {
+ return errors.Errorf("flag '--%s' requires the '--%s' flag", required, with)
+}
+
+// RequiredInsecureFlag returns an error with the required flag message unless
+// the insecure flag is used.
+func RequiredInsecureFlag(ctx *cli.Context, flag string) error {
+ return errors.Errorf("flag '--%s' requires the '--insecure' flag", flag)
+}
+
+// RequiredSubtleFlag returns an error with the required flag message unless
+// the subtle flag is used.
+func RequiredSubtleFlag(ctx *cli.Context, flag string) error {
+ return errors.Errorf("flag '--%s' requires the --subtle' flag", flag)
+}
+
+// RequiredOrFlag returns an error with a list of flags being required messages.
+func RequiredOrFlag(ctx *cli.Context, flags ...string) error {
+ params := make([]string, len(flags))
+ for i, flag := range flags {
+ params[i] = "--" + flag
+ }
+ return errors.Errorf("flag %s are required", strings.Join(params, " or "))
+}
+
+// MinSizeFlag returns an error with a greater or equal message message for
+// the given flag and size.
+func MinSizeFlag(ctx *cli.Context, flag string, size string) error {
+ return errors.Errorf("flag '--%s' must be greater or equal than %s", flag, size)
+}
+
+// MinSizeInsecureFlag returns an error with a requiring --insecure flag
+// message with the given flag an size.
+func MinSizeInsecureFlag(ctx *cli.Context, flag, size string) error {
+ return errors.Errorf("flag '--%s' requires at least %s unless '--insecure' flag is provided", flag, size)
+}
+
+// MutuallyExclusiveFlags returns an error with mutually exclusive message for
+// the given flags.
+func MutuallyExclusiveFlags(ctx *cli.Context, flag1, flag2 string) error {
+ return errors.Errorf("flag '--%s' and flag '--%s' are mutually exclusive", flag1, flag2)
+}
+
+// usage returns the command usage text if set or a default usage string.
+func usage(ctx *cli.Context) string {
+ if len(ctx.Command.UsageText) == 0 {
+ return fmt.Sprintf("%s %s [command options]", ctx.App.HelpName, ctx.Command.Name)
+ }
+
+ return ctx.Command.UsageText
+}
+
+// usageString returns the command usage prepended by the string "Usage: ".
+func usageString(ctx *cli.Context) string {
+ return "Usage: " + usage(ctx)
+}
+
+// FileError is a wrapper for errors of the os package.
+func FileError(err error, filename string) error {
+ switch e := errors.Cause(err).(type) {
+ case *os.PathError:
+ return errors.Errorf("%s %s failed: %v", e.Op, e.Path, e.Err)
+ case *os.LinkError:
+ return errors.Errorf("%s %s %s failed %v:", e.Op, e.Old, e.New, e.Err)
+ case *os.SyscallError:
+ return errors.Errorf("%s failed %v:", e.Syscall, e.Err)
+ default:
+ return Wrap(err, "unexpected error on %s", filename)
+ }
+}
diff --git a/exec/exec.go b/exec/exec.go
new file mode 100644
index 00000000..87265ce7
--- /dev/null
+++ b/exec/exec.go
@@ -0,0 +1,145 @@
+package exec
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "os/signal"
+ "path"
+ "runtime"
+ "strconv"
+ "syscall"
+
+ "github.com/pkg/errors"
+)
+
+// Exec is wrapper over syscall.Exec, invokes the execve(2) system call. On
+// windows it executes Run with the same arguments.
+func Exec(name string, arg ...string) {
+ if runtime.GOOS == "windows" {
+ Run(name, arg...)
+ return
+ }
+ args := append([]string{name}, arg...)
+ if err := syscall.Exec(name, args, os.Environ()); err != nil {
+ errorAndExit(name, err)
+ }
+}
+
+// Run is a wrapper over os/exec Cmd.Run that configures Stderr/Stdin/Stdout
+// to the current ones and wait until the process finishes, exiting with the
+// same code. Run will also forward all the signals sent to step to the
+// command.
+func Run(name string, arg ...string) {
+ cmd, exitCh, err := run(name, arg...)
+ if err != nil {
+ errorAndExit(name, err)
+ }
+
+ if err = cmd.Wait(); err != nil {
+ errorf(name, err)
+ }
+
+ // exit and wait until os.Exit
+ exitCh <- getExitStatus(cmd)
+ exitCh <- 0
+}
+
+// RunWithPid calls Run and writes the process ID in pidFile.
+func RunWithPid(pidFile, name string, arg ...string) {
+ f, err := os.OpenFile(pidFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
+ if err != nil {
+ errorAndExit(name, err)
+ }
+
+ // Run process
+ cmd, exitCh, err := run(name, arg...)
+ if err != nil {
+ f.Close()
+ os.Remove(f.Name())
+ errorAndExit(name, err)
+ }
+
+ // Write pid
+ f.Write([]byte(strconv.Itoa(cmd.Process.Pid)))
+ f.Close()
+
+ // Wait until it finishes
+ if err = cmd.Wait(); err != nil {
+ errorf(name, err)
+ }
+
+ // clean, exit and wait until os.Exit
+ os.Remove(f.Name())
+ exitCh <- getExitStatus(cmd)
+ exitCh <- 0
+}
+
+// OpenInBrowser opens the given url on a web browser
+func OpenInBrowser(url string) error {
+ var cmd *exec.Cmd
+ switch runtime.GOOS {
+ case "darwin":
+ cmd = exec.Command("open", url)
+ case "linux":
+ cmd = exec.Command("xdg-open", url)
+ case "windows":
+ cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
+ default:
+ return errors.Errorf("unsupported platform '%s'", runtime.GOOS)
+ }
+
+ return errors.WithStack(cmd.Start())
+}
+
+func run(name string, arg ...string) (*exec.Cmd, chan int, error) {
+ cmd := exec.Command(name, arg...)
+ cmd.Stderr = os.Stderr
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+
+ // Start process
+ if err := cmd.Start(); err != nil {
+ return nil, nil, err
+ }
+
+ // Forward signals
+ exitCh := make(chan int)
+ go signalHandler(cmd, exitCh)
+
+ return cmd, exitCh, nil
+}
+
+func getExitStatus(cmd *exec.Cmd) int {
+ if cmd.ProcessState != nil {
+ switch sys := cmd.ProcessState.Sys().(type) {
+ case syscall.WaitStatus:
+ return sys.ExitStatus()
+ }
+ }
+ return 1
+}
+
+func errorf(name string, err error) {
+ fmt.Fprintf(os.Stderr, "%s: %s\n", path.Base(name), err.Error())
+}
+
+func errorAndExit(name string, err error) {
+ fmt.Fprintf(os.Stderr, "%s: %s\n", path.Base(name), err.Error())
+ os.Exit(-1)
+}
+
+// signalHandler forwards all the signals to the cmd.
+func signalHandler(cmd *exec.Cmd, exitCh chan int) {
+ signals := make(chan os.Signal)
+ signal.Notify(signals)
+ defer signal.Stop(signals)
+ for {
+ select {
+ case sig := <-signals:
+ cmd.Process.Signal(sig)
+ case code := <-exitCh:
+ os.Exit(code)
+ }
+ }
+}
diff --git a/flags/flags.go b/flags/flags.go
new file mode 100644
index 00000000..fa8c1351
--- /dev/null
+++ b/flags/flags.go
@@ -0,0 +1,206 @@
+package flags
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/urfave/cli"
+)
+
+// OldPasswordFile returns a flag for receiving an old password
+func OldPasswordFile(usage string) cli.Flag {
+ if usage == "" {
+ usage = "The path to the `FILE` containing the old encryption password"
+ }
+
+ return cli.StringFlag{
+ Name: "old-password-file, o",
+ Usage: usage,
+ EnvVar: "STEP_OLD_PASSWORD_FILE",
+ }
+}
+
+// NewPasswordFile returns a flag for receiving a new password
+func NewPasswordFile(usage string) cli.Flag {
+ if usage == "" {
+ usage = "The path to the `FILE` containing the new encryption password"
+ }
+
+ return cli.StringFlag{
+ Name: "new-password-file, n",
+ Usage: usage,
+ EnvVar: "STEP_NEW_PASSWORD_FILE",
+ }
+}
+
+// Bits returns a flag for receiving the number of bits in generating a key
+func Bits(usage string, value int) cli.Flag {
+ if usage == "" {
+ usage = "Number of bits used to generate the private key"
+ }
+
+ if value == 0 {
+ value = 256
+ }
+
+ return cli.IntFlag{
+ Name: "bits, b",
+ Usage: usage,
+ EnvVar: "STEP_BITS",
+ Value: value,
+ }
+}
+
+// Action returns a flag for receiving an action out of several possibilities
+func Action(usage string, possibilities []string, value string) cli.Flag {
+ usage = fmt.Sprintf("%s (Options: %s)", usage, strings.Join(possibilities, ", "))
+ return cli.StringFlag{
+ Name: "action, a",
+ Usage: usage,
+ EnvVar: "STEP_ACTION",
+ Value: value,
+ }
+}
+
+// Type returns a flag for receiving a type of thing to create out of several
+// possibilties
+func Type(usage string, possibilities []string, value string) cli.Flag {
+ usage = fmt.Sprintf("%s (Options: %s)", usage, strings.Join(possibilities, ", "))
+ return cli.StringFlag{
+ Name: "type, t",
+ Usage: usage,
+ EnvVar: "STEP_TYPE",
+ Value: value,
+ }
+}
+
+// Alg returns a flag for receiving the type of algorithm to use when performing an operation
+func Alg(usage string, possibilities []string, value string) cli.Flag {
+ usage = fmt.Sprintf("%s (Options: %s)", usage, strings.Join(possibilities, ", "))
+ return cli.StringFlag{
+ Name: "alg",
+ Usage: usage,
+ EnvVar: "STEP_ALG",
+ Value: value,
+ }
+}
+
+// RootCertificate returns a flag for specifying the path to a root certificate
+func RootCertificate(usage string) cli.Flag {
+ if usage == "" {
+ usage = "The file `PATH` to the root certificate"
+ }
+
+ return cli.StringFlag{
+ Name: "root, r",
+ Usage: usage,
+ EnvVar: "STEP_ROOT_CERTIFICATE",
+ }
+}
+
+// PasswordFile returns a flag for specifying the path to a file containing a password
+func PasswordFile(usage string) cli.Flag {
+ if usage == "" {
+ usage = "Path to file containing a password"
+ }
+
+ return cli.StringFlag{
+ Name: "password-file, p",
+ Usage: usage,
+ EnvVar: "STEP_PASSWORD_FILE",
+ }
+}
+
+// OutputFile returns a flag for specifying the path inwhich to write output too
+func OutputFile(usage string) cli.Flag {
+ if usage == "" {
+ usage = "Path to where the output should be written"
+ }
+
+ return cli.StringFlag{
+ Name: "output-file, o",
+ Usage: usage,
+ EnvVar: "STEP_OUTPUT_FILE",
+ }
+}
+
+// Number returns a flag for collecting the number of something to create
+func Number(usage string) cli.Flag {
+ if usage == "" {
+ usage = "The `NUMBER` of entities to create"
+ }
+
+ return cli.StringFlag{
+ Name: "number, n",
+ Usage: usage,
+ EnvVar: "STEP_NUMBER",
+ }
+}
+
+// Prefix returns a flag for prefixing to the name of an entity during creation
+func Prefix(usage, value string) cli.Flag {
+ if usage == "" {
+ usage = "The `PREFIX` to apply to the names of all created entities"
+ }
+
+ return cli.StringFlag{
+ Name: "prefix, p",
+ Usage: usage,
+ Value: value,
+ EnvVar: "STEP_PREFIX",
+ }
+}
+
+// OAuthProvider returns a flag for allowing the user to select an oauth provider
+func OAuthProvider(usage string, providers []string, value string) cli.Flag {
+ usage = fmt.Sprintf("%s (Options: %s)", usage, strings.Join(providers, ", "))
+ return cli.StringFlag{
+ Name: "provider, idp",
+ Usage: usage,
+ Value: value,
+ EnvVar: "STEP_PROVIDER",
+ }
+}
+
+// Email returns a flag allowing the user to specify their email
+func Email(usage string) cli.Flag {
+ if usage == "" {
+ usage = "Email to use"
+ }
+
+ return cli.StringFlag{
+ Name: "email, e",
+ Usage: usage,
+ EnvVar: "STEP_EMAIL",
+ }
+}
+
+// Console returns a flag allowing the user to specify whether or not they want
+// to remain entirely in the console
+func Console(usage string) cli.Flag {
+ if usage == "" {
+ usage = "Whether or not to remain entirely in the console to complete the action"
+ }
+
+ return cli.BoolFlag{
+ Name: "console, c",
+ Usage: usage,
+ EnvVar: "STEP_CONSOLE",
+ }
+}
+
+// Limit returns a flag for limiting the results return by a command
+func Limit(usage string, value int) cli.Flag {
+ if usage == "" {
+ usage = "The maximum `NUMBER` of results to return"
+ }
+ if value == 0 {
+ value = 10
+ }
+
+ return cli.IntFlag{
+ Name: "limit, l",
+ Usage: usage,
+ Value: value,
+ }
+}
diff --git a/integration/command.go b/integration/command.go
new file mode 100644
index 00000000..4943351c
--- /dev/null
+++ b/integration/command.go
@@ -0,0 +1,133 @@
+package integration
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "os/exec"
+ "regexp"
+ "strings"
+ "testing"
+
+ "github.com/smallstep/assert"
+)
+
+// Command executes a shell command.
+func Command(command string) *exec.Cmd {
+ return exec.Command("sh", "-c", command)
+}
+
+// ExitError converts an error to an exec.ExitError.
+func ExitError(err error) (*exec.ExitError, bool) {
+ v, ok := err.(*exec.ExitError)
+ return v, ok
+}
+
+// Output executes a shell command and returns output from stdout.
+func Output(command string) ([]byte, error) {
+ return Command(command).Output()
+}
+
+// CombinedOutput executes a shell command and returns combined output from
+// stdout and stderr.
+func CombinedOutput(command string) ([]byte, error) {
+ return Command(command).CombinedOutput()
+}
+
+// WithStdin executes a shell command with a provided reader used for stdin.
+func WithStdin(command string, r io.Reader) ([]byte, error) {
+ cmd := Command(command)
+ cmd.Stdin = r
+ return cmd.Output()
+}
+
+// CLICommand repreents a command-line command to execute.
+type CLICommand struct {
+ command string
+ arguments string
+ flags map[string]string
+ stdin io.Reader
+}
+
+// CLIOutput represents the output from executing a CLICommand.
+type CLIOutput struct {
+ stdout string
+ stderr string
+ combined string
+}
+
+// NewCLICommand generates a new CLICommand.
+func NewCLICommand() CLICommand {
+ return CLICommand{"", "", make(map[string]string), nil}
+}
+
+func (c CLICommand) setFlag(flag, value string) CLICommand {
+ flags := make(map[string]string)
+ for k, v := range c.flags {
+ flags[k] = v
+ }
+ flags[flag] = value
+ return CLICommand{c.command, c.arguments, flags, c.stdin}
+}
+
+func (c CLICommand) setCommand(command string) CLICommand {
+ return CLICommand{command, c.arguments, c.flags, c.stdin}
+}
+
+func (c CLICommand) setArguments(arguments string) CLICommand {
+ return CLICommand{c.command, arguments, c.flags, c.stdin}
+}
+
+func (c CLICommand) setStdin(stdin string) CLICommand {
+ return CLICommand{c.command, c.arguments, c.flags, strings.NewReader(stdin)}
+}
+
+func (c CLICommand) cmd() string {
+ flags := ""
+ for key, value := range c.flags {
+ if strings.Contains(value, " ") {
+ value = "\"" + value + "\""
+ }
+ flags += fmt.Sprintf("--%s %s ", key, value)
+ }
+ return fmt.Sprintf("%s %s %s", c.command, c.arguments, flags)
+}
+
+func (c CLICommand) run() (CLIOutput, error) {
+ var stdout, stderr, combined bytes.Buffer
+ cmd := Command(c.cmd())
+ cmd.Stdout = io.MultiWriter(&stdout, &combined)
+ cmd.Stderr = io.MultiWriter(&stderr, &combined)
+ cmd.Stdin = c.stdin
+ err := cmd.Run()
+ return CLIOutput{string(stdout.Bytes()), string(stderr.Bytes()), string(combined.Bytes())}, err
+}
+
+func (c CLICommand) test(t *testing.T, name string, expected string, msg ...interface{}) {
+ t.Run(name, func(t *testing.T) {
+ out, err := c.run()
+ assert.FatalError(t, err, fmt.Sprintf("`%s`: returned error '%s'\n\nOutput:\n%s", c.cmd(), err, out.combined))
+ assert.Equals(t, out.combined, expected, msg)
+ })
+}
+
+func (c CLICommand) fail(t *testing.T, name string, expected interface{}, msg ...interface{}) {
+ t.Run(name, func(t *testing.T) {
+ out, err := c.run()
+ if assert.NotNil(t, err) {
+ assert.Equals(t, err.Error(), "exit status 1")
+ }
+ switch v := expected.(type) {
+ case string:
+ assert.Equals(t, expected, out.stderr)
+ case *regexp.Regexp:
+ re := expected.(*regexp.Regexp)
+ if !re.MatchString(out.stderr) {
+ t.Errorf("Error message did not match regex:\n Regex: %s\n\n Output:\n%s", re.String(), out.stderr)
+ }
+ default:
+ t.Errorf("unexpected type %T", v)
+ }
+ assert.Equals(t, "", out.stdout)
+ })
+}
diff --git a/integration/integration_test.go b/integration/integration_test.go
new file mode 100644
index 00000000..6632e5a2
--- /dev/null
+++ b/integration/integration_test.go
@@ -0,0 +1,54 @@
+// +build integration
+
+package integration
+
+import (
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/smallstep/assert"
+)
+
+const (
+ TempDirectory = "testdata-tmp"
+)
+
+func TestMain(m *testing.M) {
+ flag.Parse()
+ os.Setenv("PATH", os.Getenv("GOPATH")+"/src/github.com/smallstep/cli/bin"+":"+os.Getenv("PATH"))
+ if err := os.Mkdir(TempDirectory, os.ModeDir|os.ModePerm); err != nil {
+ log.Fatal(err)
+ }
+ rval := m.Run()
+ if err := os.RemoveAll(TempDirectory); err != nil {
+ log.Fatal(err)
+ }
+ os.Exit(rval)
+}
+
+func TestVersion(t *testing.T) {
+ out, err := Output("step version | head -1")
+ assert.FatalError(t, err)
+ assert.True(t, strings.HasPrefix(string(out), "Smallstep CLI"))
+}
+
+func TestCryptoJWTSign(t *testing.T) {
+ out, err := Output("step crypto jwt sign -key testdata/p256.pem -iss TestIssuer -aud TestAudience -sub TestSubject -nbf 1 -iat 1 -exp 1 -subtle")
+ assert.FatalError(t, err)
+ assert.True(t, strings.HasPrefix(string(out), "eyJhbGciOiJFUzI1NiIsImtpZCI6IiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiVGVzdEF1ZGllbmNlIl0sImV4cCI6MSwiaWF0IjoxLCJpc3MiOiJUZXN0SXNzdWVyIiwibmJmIjoxLCJzdWIiOiJUZXN0U3ViamVjdCJ9."))
+}
+
+func TestCryptoJWTVerifyWithPrivate(t *testing.T) {
+ exp := time.Now().Add(1 * time.Minute)
+ out, err := CombinedOutput(fmt.Sprintf("step crypto jwt sign -key testdata/p256.pem -iss TestIssuer -aud TestAudience -sub TestSubject -exp %d | step crypto jwt verify -key testdata/p256.pem -subtle", exp.Unix()))
+ assert.FatalError(t, err)
+ m := make(map[string]interface{})
+ assert.FatalError(t, json.Unmarshal(out, &m))
+ assert.Equals(t, "TestIssuer", m["payload"].(map[string]interface{})["iss"])
+}
diff --git a/integration/jwk_test.go b/integration/jwk_test.go
new file mode 100644
index 00000000..903957bf
--- /dev/null
+++ b/integration/jwk_test.go
@@ -0,0 +1,419 @@
+// +build integration
+
+package integration
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/smallstep/assert"
+ jose "gopkg.in/square/go-jose.v2"
+)
+
+func AssertFileExists(t *testing.T, path string, a ...interface{}) bool {
+ info, err := os.Lstat(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ assert.FatalError(t, err, fmt.Sprintf("unable to find file %q", path), a)
+ }
+ assert.FatalError(t, err, fmt.Sprintf("error when running os.Lstat(%q): %s", path, err), a)
+ }
+ assert.Fatal(t, !info.IsDir(), fmt.Sprintf("%q is a directory", path), a)
+ assert.Equals(t, int(info.Mode()), 0600)
+ return true
+}
+
+type JWKTest struct {
+ name string
+ pubfile string
+ prvfile string
+ command CLICommand
+}
+
+func NewJWKTest(name string) JWKTest {
+ pubfile := fmt.Sprintf("%s/%s-pub.json", TempDirectory, name)
+ prvfile := fmt.Sprintf("%s/%s-prv.json", TempDirectory, name)
+ cmd := NewCLICommand().setCommand("step crypto jwk create").setArguments(fmt.Sprintf("%s %s", pubfile, prvfile))
+ return JWKTest{name, pubfile, prvfile, cmd}
+}
+
+func (j JWKTest) setFlag(flag, value string) JWKTest {
+ return JWKTest{j.name, j.pubfile, j.prvfile, j.command.setFlag(flag, value)}
+}
+
+func (j JWKTest) cmd() string {
+ return j.command.cmd()
+}
+
+func (j JWKTest) run() (CLIOutput, error) {
+ return j.command.run()
+}
+
+func (j JWKTest) test(t *testing.T, msg ...interface{}) (CLIOutput, string) {
+ var out CLIOutput
+ var pass string
+ t.Run(j.name, func(t *testing.T) {
+ //fmt.Printf("Running command: %s", j.cmd())
+ out, err := j.run()
+ assert.FatalError(t, err, fmt.Sprintf("`%s`: returned error '%s'\n\nOutput:\n%s", j.cmd(), err, out.combined))
+ AssertFileExists(t, j.pubfile, fmt.Sprintf("step crypto jwk create should create public JWK at %s", j.pubfile))
+ AssertFileExists(t, j.prvfile, fmt.Sprintf("step crypto jwk create should create private JWK at %s", j.prvfile))
+ j.checkPublic(t)
+ if _, ok := j.command.flags["no-password"]; ok {
+ j.checkPrivate(t, "")
+ assert.Equals(t, out.combined, "", msg)
+ } else {
+ // Output should be look like:
+ // Private JWK file '' will be encrypted with the key:
+ //
+ lines := strings.Split(out.stdout, "\n")
+ outMsg := fmt.Sprintf("Unexpected output from `%s`:\n\n%s", j.cmd(), out.stdout)
+ assert.Equals(t, 3, len(lines), outMsg)
+ if len(lines) == 3 {
+ assert.Equals(t, fmt.Sprintf("Private JWK file '%s' will be encrypted with the key:", j.prvfile), lines[0])
+ assert.Equals(t, "", out.stderr)
+ pass = lines[1]
+ j.checkPrivate(t, pass)
+ }
+ }
+ })
+ return out, pass
+}
+
+func (j JWKTest) readJson(t *testing.T, name string) map[string]interface{} {
+ dat, err := ioutil.ReadFile(name)
+ assert.FatalError(t, err)
+ m := make(map[string]interface{})
+ assert.FatalError(t, json.Unmarshal(dat, &m))
+ return m
+}
+
+func (j JWKTest) public(t *testing.T) map[string]interface{} {
+ return j.readJson(t, j.pubfile)
+}
+
+func (j JWKTest) private(t *testing.T) map[string]interface{} {
+ return j.readJson(t, j.prvfile)
+}
+
+func (j JWKTest) kty() string {
+ // Default "kty" is EC
+ kty := "EC"
+ if v, ok := j.command.flags["kty"]; ok {
+ kty = v
+ } else if v, ok = j.command.flags["type"]; ok {
+ kty = v
+ }
+ return kty
+}
+
+/*
+func (j JWKTest) crv() string {
+ if v, ok := j.command.flags["crv"]; ok {
+ return v, true
+ } else if v, ok = j.command.flags["curve"]; ok {
+ return v, true
+ }
+ return "", false
+}
+*/
+
+func (j JWKTest) checkPubPriv(t *testing.T, m map[string]interface{}) {
+
+ checkSize := func(v string, defaultSize int) {
+ bytes, err := base64.RawURLEncoding.DecodeString(v)
+ assert.FatalError(t, err)
+ if v, ok := j.command.flags["size"]; ok {
+ size, err := strconv.Atoi(v)
+ assert.FatalError(t, err)
+ assert.Equals(t, len(bytes)*8, size)
+ } else {
+ assert.Equals(t, len(bytes)*8, defaultSize)
+ }
+ }
+
+ checkSizeBytes := func(v string, defaultSize int) {
+ bytes, err := base64.RawURLEncoding.DecodeString(v)
+ assert.FatalError(t, err)
+ if v, ok := j.command.flags["size"]; ok {
+ size, err := strconv.Atoi(v)
+ assert.FatalError(t, err)
+ assert.Equals(t, len(bytes), size)
+ } else {
+ assert.Equals(t, len(bytes), defaultSize)
+ }
+ }
+
+ kty := j.kty()
+ assert.Equals(t, kty, m["kty"])
+
+ if v, ok := j.command.flags["use"]; ok {
+ assert.Equals(t, v, m["use"])
+ } else {
+ // Default "use" is "sig"
+ assert.Equals(t, "sig", m["use"])
+ }
+
+ if v, ok := j.command.flags["kid"]; ok {
+ assert.Equals(t, v, m["kid"])
+ } else {
+ assert.False(t, "" == m["kid"])
+ }
+
+ if kty == "EC" {
+ _, ok := m["size"]
+ assert.True(t, !ok, "size attribute for EC key")
+
+ if v, ok := j.command.flags["crv"]; ok {
+ assert.Equals(t, v, m["crv"])
+ } else {
+ switch j.command.flags["alg"] {
+ case "ES256":
+ assert.Equals(t, "P-256", m["crv"])
+ case "ES384":
+ assert.Equals(t, "P-384", m["crv"])
+ case "ES512":
+ assert.Equals(t, "P-521", m["crv"])
+ default:
+ assert.Equals(t, "P-256", m["crv"])
+ }
+ }
+
+ if v, ok := j.command.flags["alg"]; ok {
+ assert.Equals(t, v, m["alg"])
+ } else {
+ if m["use"] == "enc" {
+ assert.Equals(t, "ECDH-ES", m["alg"])
+ } else {
+ switch m["crv"] {
+ case "P-256":
+ assert.Equals(t, "ES256", m["alg"])
+ case "P-384":
+ assert.Equals(t, "ES384", m["alg"])
+ case "P-521":
+ assert.Equals(t, "ES512", m["alg"])
+ }
+ }
+ }
+
+ // TODO: Check EC parameters and key size
+ } else if kty == "OKP" {
+ _, ok := m["size"]
+ assert.True(t, !ok, "size attribute for OKP key")
+ assert.Equals(t, "Ed25519", m["crv"])
+ assert.Equals(t, "EdDSA", m["alg"])
+ _, ok = m["x"]
+ assert.True(t, ok, "JWK with \"kty\" of \"OKP\" should have \"x\" parameter (public key)")
+ } else if kty == "RSA" {
+ _, ok := m["crv"]
+ assert.True(t, !ok, "crv attribute for non-EC key")
+
+ if v, ok := j.command.flags["alg"]; ok {
+ assert.Equals(t, v, m["alg"])
+ } else {
+ // Default "alg" is "RS256" for "RSA" keys
+ assert.Equals(t, "RS256", m["alg"])
+ }
+
+ n, ok := m["n"]
+ assert.True(t, ok, "JWK with \"kty\" of \"RSA\" should have \"n\" parameter (modulus)")
+ _, ok = m["e"]
+ assert.True(t, ok, "JWK with \"kty\" of \"RSA\" should have \"e\" parameter (exponent)")
+
+ // Check that `n` is the correct size
+ checkSize(n.(string), 2048)
+ } else if kty == "oct" {
+ // Should be no "crv" for non-EC keys
+ _, ok := m["crv"]
+ assert.True(t, !ok, "crv attribute for non-EC key")
+
+ if v, ok := j.command.flags["alg"]; ok {
+ assert.Equals(t, v, m["alg"])
+ } else {
+ // Default "alg" is "HS256" for "oct" keys
+ assert.Equals(t, "HS256", m["alg"])
+ }
+
+ k, ok := m["k"]
+ assert.True(t, ok, "JWK with \"kty\" of \"oct\" should have \"k\" paramater (key)")
+
+ // Check `k` is correct size
+ checkSizeBytes(k.(string), 32)
+ } else {
+ assert.True(t, false, fmt.Sprintf("invalid key type: %s", kty))
+ }
+}
+
+func (j JWKTest) checkPublic(t *testing.T) {
+ j.checkPubPriv(t, j.public(t))
+}
+
+func isJWE(m map[string]interface{}) bool {
+ // `ciphertext` which MUST be present in a JWE according to RFC7516
+ _, ok := m["ciphertext"]
+ return ok
+}
+
+func (j JWKTest) decryptJWEPayload(t *testing.T, password string) map[string]interface{} {
+ dat, err := ioutil.ReadFile(j.prvfile)
+ assert.FatalError(t, err)
+ enc, err := jose.ParseEncrypted(string(dat))
+ assert.FatalError(t, err)
+ dec, err := enc.Decrypt([]byte(password))
+ assert.FatalError(t, err)
+ m := make(map[string]interface{})
+ assert.FatalError(t, json.Unmarshal(dec, &m))
+ return m
+}
+
+func (j JWKTest) checkPrivate(t *testing.T, password string) {
+ m := j.private(t)
+ _, nopass := j.command.flags["no-password"]
+ if isJWE(m) {
+ assert.False(t, nopass, "expected unencrypted JWK with --no-password flag but got JWE")
+ hdrb, ok := m["protected"]
+ assert.True(t, ok, "missing protected header attribute in JWE")
+ hdr, err := base64.RawURLEncoding.DecodeString(hdrb.(string))
+ assert.FatalError(t, err)
+ assert.Equals(t, string(hdr), `{"alg":"A128KW","enc":"A128GCM"}`)
+ m = j.decryptJWEPayload(t, password)
+ } else {
+ assert.True(t, nopass, "JWKs should be encrypted in JWE unless --no-password flag is passed")
+ }
+ j.checkPubPriv(t, m)
+ if j.kty() == "EC" {
+ // TODO: Check EC parameters and key size
+ } else if j.kty() == "OKP" {
+ _, ok := m["d"]
+ assert.True(t, ok, "JWK with \"kty\" of \"OKP\" should have \"d\" parameter (private key)")
+ } else if j.kty() == "RSA" {
+ d, ok := m["d"]
+ assert.True(t, ok, "JWK with \"kty\" of \"RSA\" should have \"d\" parameter (private exponent)")
+ // Check that size of `d` is the correct size
+ bytes, err := base64.RawURLEncoding.DecodeString(d.(string))
+ assert.FatalError(t, err)
+ if v, ok := j.command.flags["size"]; ok {
+ size, err := strconv.Atoi(v)
+ assert.FatalError(t, err)
+ assert.Equals(t, len(bytes)*8, size)
+ } else {
+ assert.Equals(t, len(bytes)*8, 2048)
+ }
+
+ _, ok = m["p"]
+ assert.True(t, ok, "JWK with \"kty\" of \"RSA\" should have \"p\" parameter (first prime factor)")
+ _, ok = m["q"]
+ assert.True(t, ok, "JWK with \"kty\" of \"RSA\" should have \"p\" parameter (second prime factor)")
+ }
+}
+
+// TODO: Calling this on a successful test appears to cause a SEGFAULT?
+func (j JWKTest) fail(t *testing.T, expected string, msg ...interface{}) {
+ j.command.fail(t, j.name, expected, msg)
+}
+
+func TestCryptoJWK(t *testing.T) {
+ t.Run("jwk", func(t *testing.T) {
+ NewJWKTest("default").test(t)
+ t.Run("kty=RSA", func(t *testing.T) {
+ NewJWKTest("RSA-2048-RS256").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "RS256").test(t)
+ NewJWKTest("RSA-2048-RS384").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "RS384").test(t)
+ NewJWKTest("RSA-2048-RS512").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "RS512").test(t)
+ NewJWKTest("RSA-4096-RS256").setFlag("kty", "RSA").setFlag("size", "4096").setFlag("alg", "RS256").test(t)
+ NewJWKTest("RSA-2048-PS256").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "PS256").test(t)
+ NewJWKTest("RSA-2048-PS384").setFlag("type", "RSA").setFlag("size", "2048").setFlag("alg", "PS384").test(t)
+ NewJWKTest("RSA-2048-PS512").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "PS512").test(t)
+ NewJWKTest("RSA-1024-PS256-fail").setFlag("kty", "RSA").setFlag("size", "1024").setFlag("alg", "PS512").fail(t, "minimum '--size' for RSA keys is 2048 bits without '--insecure' flag\n")
+ NewJWKTest("RSA-1024-PS256").setFlag("type", "RSA").setFlag("size", "1024").setFlag("alg", "PS512").setFlag("insecure", "").test(t)
+ NewJWKTest("RSA-16-PS256").setFlag("kty", "RSA").setFlag("size", "16").setFlag("alg", "PS512").setFlag("insecure", "").test(t)
+ // Broken - actual size is 16. Needs to be multiple of 8?
+ //NewJWKTest("RSA-12-PS256").setFlag("kty", "RSA").setFlag("size", "12").setFlag("alg", "PS512").setFlag("insecure", "").test(t)
+ // Broken - fails in crypto/rsa
+ //NewJWKTest("RSA-11-PS256").setFlag("kty", "RSA").setFlag("size", "11").setFlag("alg", "PS512").setFlag("insecure", "").test(t)
+ //NewJWKTest("RSA-0-PS256").setFlag("kty", "RSA").setFlag("size", "0").setFlag("alg", "PS512").setFlag("insecure", "").test(t)
+ NewJWKTest("RSA-0-PS256").setFlag("kty", "RSA").setFlag("size", "-1").setFlag("alg", "PS512").setFlag("insecure", "").fail(t, "flag '--size' must be >= 0\n")
+ NewJWKTest("RSA-2048-PS256-enc-bad-alg").setFlag("type", "RSA").setFlag("size", "2048").setFlag("alg", "PS256").setFlag("use", "enc").fail(t, "alg 'PS256' is not compatible with kty 'RSA'\n")
+ NewJWKTest("RSA-2048-A128KW-enc-bad-alg").setFlag("type", "RSA").setFlag("size", "2048").setFlag("alg", "A128KW").setFlag("use", "enc").fail(t, "alg 'A128KW' is not compatible with kty 'RSA'\n")
+ NewJWKTest("RSA-2048-RSAOAEP-enc").setFlag("type", "RSA").setFlag("size", "2048").setFlag("alg", "RSA-OAEP").setFlag("use", "enc").test(t)
+ NewJWKTest("RSA-2048-RSAOAEP256-enc").setFlag("kty", "RSA").setFlag("size", "2056").setFlag("alg", "RSA-OAEP-256").setFlag("use", "enc").test(t)
+ NewJWKTest("RSA-2048-RSA1_5-enc").setFlag("type", "RSA").setFlag("size", "2064").setFlag("alg", "RSA1_5").setFlag("use", "enc").test(t)
+ NewJWKTest("RSA-2048-PS512-kid-snarf").setFlag("kty", "RSA").setFlag("size", "2064").setFlag("alg", "PS512").setFlag("kid", "snarf").test(t)
+ NewJWKTest("RSA-default").setFlag("kty", "RSA").test(t)
+ NewJWKTest("alg=ES256").setFlag("kty", "RSA").setFlag("alg", "ES256").setFlag("size", "2048").fail(t, "alg 'ES256' is not compatible with kty 'RSA'\n")
+ NewJWKTest("alg=HS384").setFlag("type", "RSA").setFlag("alg", "HS384").setFlag("size", "2048").fail(t, "alg 'HS384' is not compatible with kty 'RSA'\n")
+ NewJWKTest("RSA-2048-PS256-crv").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "PS256").setFlag("crv", "P-521").fail(t, "flag '--crv' is incompatible with '--kty RSA'\n")
+ NewJWKTest("RSA-128-PS256").setFlag("kty", "RSA").setFlag("size", "128").setFlag("alg", "PS256").fail(t, "minimum '--size' for RSA keys is 2048 bits without '--insecure' flag\n")
+ NewJWKTest("rsa").setFlag("kty", "rsa").fail(t, "missing or invalid value for flag '--kty'\n")
+ NewJWKTest("RSA-nopass-fail").setFlag("kty", "RSA").setFlag("no-password", "").fail(t, "flag '--no-password' requires the '--insecure' flag\n")
+ NewJWKTest("RSA-nopass").setFlag("kty", "RSA").setFlag("no-password", "").setFlag("insecure", "").test(t)
+ })
+ t.Run("kty=oct", func(t *testing.T) {
+ NewJWKTest("oct-default").setFlag("kty", "oct").test(t)
+ NewJWKTest("oct-32-fail").setFlag("type", "oct").setFlag("size", "4").fail(t, "minimum '--size' for oct keys is 16 bytes without '--insecure' flag\n")
+ NewJWKTest("oct-32").setFlag("kty", "oct").setFlag("size", "4").setFlag("insecure", "").test(t)
+ NewJWKTest("oct-16").setFlag("kty", "oct").setFlag("size", "2").setFlag("insecure", "").test(t)
+ NewJWKTest("oct-0").setFlag("kty", "oct").setFlag("size", "0").setFlag("insecure", "").fail(t, "flag '--size' must be >= 0\n")
+ NewJWKTest("oct-512-HS256").setFlag("type", "oct").setFlag("alg", "HS256").setFlag("size", "64").test(t)
+ NewJWKTest("oct-512-HS384").setFlag("kty", "oct").setFlag("alg", "HS384").setFlag("size", "64").test(t)
+ NewJWKTest("oct-512-HS512").setFlag("kty", "oct").setFlag("alg", "HS512").setFlag("size", "64").test(t)
+ NewJWKTest("oct-256-HS256-enc").setFlag("kty", "oct").setFlag("alg", "HS256").setFlag("size", "32").setFlag("use", "enc").fail(t, "alg 'HS256' is not compatible with kty 'oct'\n")
+ NewJWKTest("oct-256-dir-enc").setFlag("kty", "oct").setFlag("alg", "dir").setFlag("size", "32").setFlag("use", "enc").test(t)
+ NewJWKTest("oct-256-A128KW-enc").setFlag("kty", "oct").setFlag("alg", "A128KW").setFlag("size", "32").setFlag("use", "enc").test(t)
+ NewJWKTest("oct-256-A192KW-enc").setFlag("kty", "oct").setFlag("alg", "A192KW").setFlag("size", "32").setFlag("use", "enc").test(t)
+ NewJWKTest("oct-256-A256KW-enc").setFlag("kty", "oct").setFlag("alg", "A256KW").setFlag("size", "32").setFlag("use", "enc").test(t)
+ NewJWKTest("oct-256-A128GCMKW-enc").setFlag("kty", "oct").setFlag("alg", "A128GCMKW").setFlag("size", "32").setFlag("use", "enc").test(t)
+ NewJWKTest("oct-256-A192GCMKW-enc").setFlag("kty", "oct").setFlag("alg", "A192GCMKW").setFlag("size", "32").setFlag("use", "enc").test(t)
+ NewJWKTest("oct-256-A256GCMKW-enc").setFlag("kty", "oct").setFlag("alg", "A256GCMKW").setFlag("size", "32").setFlag("use", "enc").test(t)
+ NewJWKTest("oct-256-HS256-kid-foo").setFlag("kty", "oct").setFlag("alg", "HS256").setFlag("size", "32").setFlag("kid", "foo").test(t)
+ NewJWKTest("alg=RS256").setFlag("kty", "oct").setFlag("alg", "RS256").setFlag("size", "64").fail(t, "alg 'RS256' is not compatible with kty 'oct'\n")
+ NewJWKTest("oct-512-HS256-crv").setFlag("kty", "oct").setFlag("alg", "HS256").setFlag("size", "64").setFlag("crv", "P-256").fail(t, "flag '--crv' is incompatible with '--kty oct'\n")
+ NewJWKTest("OCT").setFlag("kty", "OCT").fail(t, "missing or invalid value for flag '--kty'\n")
+ })
+ t.Run("kty=EC", func(t *testing.T) {
+ NewJWKTest("EC-default").setFlag("kty", "EC").test(t)
+ NewJWKTest("EC-kid-w00t").setFlag("kty", "EC").setFlag("kid", "w00t").test(t)
+ NewJWKTest("EC-P256-ES256").setFlag("kty", "EC").setFlag("crv", "P-256").setFlag("alg", "ES256").test(t)
+ NewJWKTest("EC-P384-ES384").setFlag("type", "EC").setFlag("crv", "P-384").setFlag("alg", "ES384").test(t)
+ NewJWKTest("EC-P521-ES512").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "ES512").test(t)
+ NewJWKTest("EC-P521-RSA1_5-enc").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "RSA1_5").setFlag("use", "enc").fail(t, "alg 'RSA1_5' is not compatible with kty 'EC'\n")
+ NewJWKTest("EC-P521-ECDHES-enc").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "ECDH-ES").setFlag("use", "enc").test(t)
+ NewJWKTest("EC-P521-ECDHESA128KW-enc").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "ECDH-ES+A128KW").setFlag("use", "enc").test(t)
+ NewJWKTest("EC-P521-ECDHESA192KW-enc").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "ECDH-ES+A192KW").setFlag("use", "enc").test(t)
+ NewJWKTest("EC-P521-ECDHESA256KW-enc").setFlag("kty", "EC").setFlag("crv", "P-521").setFlag("alg", "ECDH-ES+A256KW").setFlag("use", "enc").test(t)
+ NewJWKTest("EC-P256-ES384").setFlag("type", "EC").setFlag("crv", "P-256").setFlag("alg", "ES384").fail(t, "alg 'ES384' is not compatible with kty 'EC' and crv 'P-256'\n")
+ NewJWKTest("EC-P256-ES256-size").setFlag("kty", "EC").setFlag("crv", "P-256").setFlag("alg", "ES256").setFlag("size", "2048").fail(t, "flag '--size' is incompatible with '--kty EC'\n")
+ NewJWKTest("EC-P256").setFlag("kty", "EC").setFlag("crv", "P-256").test(t)
+ NewJWKTest("EC-P384").setFlag("kty", "EC").setFlag("crv", "P-384").test(t)
+ NewJWKTest("EC-P521").setFlag("kty", "EC").setFlag("crv", "P-521").test(t)
+ NewJWKTest("ec").setFlag("kty", "ec").fail(t, "missing or invalid value for flag '--kty'\n")
+ })
+ t.Run("kty=OKP", func(t *testing.T) {
+ NewJWKTest("OKP-Ed25519-default").setFlag("kty", "OKP").setFlag("crv", "Ed25519").test(t)
+ NewJWKTest("OKP-Ed25519-deadbeef").setFlag("kty", "OKP").setFlag("crv", "Ed25519").setFlag("kid", "deadbeef").test(t)
+ NewJWKTest("OKP-Ed25519-EdDSA").setFlag("type", "OKP").setFlag("crv", "Ed25519").setFlag("alg", "EdDSA").test(t)
+ NewJWKTest("OKP-Ed25519-EdDSA").setFlag("kty", "OKP").setFlag("crv", "Ed25519").setFlag("alg", "ES256").fail(t, "alg 'ES256' is not compatible with kty 'OKP' and crv 'Ed25519'\n")
+ NewJWKTest("OKP-Ed25519-EdDSA").setFlag("kty", "OKP").setFlag("crv", "Ed25519").setFlag("size", "256").fail(t, "flag '--size' is incompatible with '--kty OKP'\n")
+ NewJWKTest("okp").setFlag("kty", "okp").fail(t, "missing or invalid value for flag '--kty'\n")
+ })
+ NewJWKTest("kty=FOO").setFlag("kty", "FOO").fail(t, "missing or invalid value for flag '--kty'\n")
+ NewJWKTest("kty=ec").setFlag("kty", "ec").fail(t, "missing or invalid value for flag '--kty'\n", "kty flag is case-sensitive")
+ NewJWKTest("alg=rs256").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "rs256").fail(t, "alg 'rs256' is not compatible with kty 'RSA'\n", "alg flag is case-sensitive")
+ NewJWKTest("alg=snarf").setFlag("kty", "RSA").setFlag("size", "2048").setFlag("alg", "snarf").fail(t, "alg 'snarf' is not compatible with kty 'RSA'\n")
+ NewJWKTest("alg=rs256").setFlag("alg", "rs256").fail(t, "alg 'rs256' is not compatible with kty 'EC' and crv 'P-256'\n", "alg flag is case-sensitive")
+ // Broken - prints usage
+ //NewJWKTest("type-and-kty").setFlag("type", "RSA").setFlag("kty", "RSA").fail(t, "Cannot use two forms of the same flag: type kty")
+
+ NewCLICommand().setCommand("step crypto jwk create").fail(t, "missing-args#1", "missing positional arguments 'PUB_FILE' 'PRIV_FILE'\n")
+ NewCLICommand().setCommand("step crypto jwk create").setArguments("foo.json").fail(t, "missing-args#2", "missing positional argument 'PRIV_FILE'\n")
+ NewCLICommand().setCommand("step crypto jwk create").setArguments("foo.1.json foo.2.json foo.3.json").fail(t, "too-many-args", "too many positional arguments use only 'PUB_FILE' 'PRIV_FILE'\n")
+ NewCLICommand().setCommand("step crypto jwk create").setArguments("foo.json foo.json").fail(t, "pub-priv-same", "positional arguments 'PUB_FILE' 'PRIV_FILE' cannot be equal\n")
+ // Broken - prints usage
+ //NewCLICommand().setCommand("step crypto jwk create").setArguments("foo.json bar.json").setFlag("size", "blort").fail(t, "non-int-size", "invalid value \"blort\" for flag -size: strconv.ParseInt: parsing \"blort\": invalid syntax")
+ })
+}
diff --git a/integration/jwt_test.go b/integration/jwt_test.go
new file mode 100644
index 00000000..b6d6b322
--- /dev/null
+++ b/integration/jwt_test.go
@@ -0,0 +1,743 @@
+// +build integration
+
+package integration
+
+import (
+ crand "crypto/rand"
+ "encoding/base64"
+ "encoding/binary"
+ "encoding/json"
+ "encoding/pem"
+ "fmt"
+ "io/ioutil"
+ "math"
+ "math/rand"
+ "os/exec"
+ "reflect"
+ "regexp"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/ThomasRooney/gexpect"
+ "github.com/icrowley/fake"
+ "github.com/pkg/errors"
+ "github.com/smallstep/assert"
+ "github.com/smallstep/cli/crypto/keys"
+ jose "gopkg.in/square/go-jose.v2"
+)
+
+type JWK struct {
+ pubfile string
+ prvfile string
+ password string
+ ispem bool
+ iskeyset bool
+}
+
+func (j JWK) jwk() (*jose.JSONWebKey, error) {
+ jwk := new(jose.JSONWebKey)
+ b, err := ioutil.ReadFile(j.prvfile)
+ if err != nil {
+ return nil, err
+ }
+ if enc, err := jose.ParseEncrypted(string(b)); err == nil {
+ b, err = enc.Decrypt([]byte(j.password))
+ if err != nil {
+ return nil, err
+ }
+ }
+ if err := json.Unmarshal(b, jwk); err != nil {
+ return nil, err
+ }
+ return jwk, nil
+}
+
+func (j JWK) pem() (string, error) {
+ jwk, err := j.jwk()
+ if err != nil {
+ return "", err
+ }
+ b, err := keys.PrivatePEM(jwk.Key, keys.InsecureEncOpts())
+ if err != nil {
+ return "", err
+ }
+ return string(pem.EncodeToMemory(b)), err
+}
+
+func readJSON(name string) (map[string]interface{}, error) {
+ dat, err := ioutil.ReadFile(name)
+ if err != nil {
+ return nil, err
+ }
+ m := make(map[string]interface{})
+ err = json.Unmarshal(dat, &m)
+ return m, err
+}
+
+type JWTSignTest struct {
+ command CLICommand
+ jwk JWK
+}
+
+func NewJWTSignTest(jwk JWK) JWTSignTest {
+ cmd := NewCLICommand().setCommand("step crypto jwt sign").setFlag("key", jwk.prvfile)
+ return JWTSignTest{cmd, jwk}
+}
+
+func (j JWTSignTest) setFlag(key, value string) JWTSignTest {
+ return JWTSignTest{j.command.setFlag(key, value), j.jwk}
+}
+
+func (j JWTSignTest) exp(d time.Duration) JWTSignTest {
+ exp := time.Now().Add(d)
+ return j.setFlag("exp", strconv.Itoa(int(exp.Unix())))
+}
+
+func (j JWTSignTest) nbf(d time.Duration) JWTSignTest {
+ nbf := time.Now().Add(d)
+ return j.setFlag("nbf", strconv.Itoa(int(nbf.Unix())))
+}
+
+func (j JWTSignTest) iat(d time.Duration) JWTSignTest {
+ iat := time.Now().Add(d)
+ return j.setFlag("iat", strconv.Itoa(int(iat.Unix())))
+}
+
+func (j JWTSignTest) test(t *testing.T, name string) string {
+ var jwt string
+ t.Run(name, func(t *testing.T) {
+ // Beware. This is fragile as hell. Ugh. If the output or prompt for the
+ // jwt sign cubcommand changes this will need to change too.
+ if j.jwk.password != "" {
+ cmd, err := gexpect.Spawn(j.command.cmd())
+ assert.FatalError(t, err)
+ prompt := "Please enter the password to decrypt " + j.jwk.prvfile + ": "
+ assert.Nil(t, cmd.ExpectTimeout(prompt, 1*time.Second))
+ assert.Nil(t, cmd.SendLine(j.jwk.password))
+
+ var lines []string
+ for {
+ line, err := cmd.ReadLine()
+ line = strings.Trim(line, "\r")
+ if err != nil {
+ break
+ }
+ lines = append(lines, line)
+ }
+
+ jwt = strings.Trim(strings.Join(lines, "\n"), " \r\n")
+ err = cmd.Wait()
+ if assert.Nil(t, err) {
+ j.checkJwt(t, jwt)
+ }
+ if t.Failed() {
+ t.Errorf("Output did not match for command `%s`", j.command.cmd())
+ t.Errorf("Prompt:\n%s\n\n", prompt)
+ t.Errorf("Output:\n%s\n", jwt)
+ }
+ cmd.Wait()
+ cmd.Close()
+ } else {
+ out, err := j.command.run()
+ assert.FatalError(t, err)
+ jwt = out.stdout
+ j.checkJwt(t, jwt)
+ }
+ })
+ return jwt
+}
+
+func (j JWTSignTest) fail(t *testing.T, name, expected string) {
+ if j.jwk.password != "" {
+ t.Run(name, func(t *testing.T) {
+ cmd, err := gexpect.Command(j.command.cmd())
+ assert.FatalError(t, err)
+ assert.FatalError(t, cmd.Start())
+ assert.FatalError(t, cmd.ExpectTimeout("Please enter the password to decrypt "+j.jwk.prvfile+": ", 1*time.Second))
+ assert.FatalError(t, cmd.SendLine(j.jwk.password))
+ _, err = cmd.ReadLine() // Prompt prints a newline
+ assert.FatalError(t, err)
+
+ var lines []string
+ for {
+ line, err := cmd.ReadLine()
+ line = strings.Trim(line, "\r")
+ if err != nil {
+ break
+ }
+ lines = append(lines, line)
+ }
+
+ actual := strings.Join(lines, "\n") + "\n"
+ assert.Equals(t, expected, actual)
+
+ err = cmd.Wait()
+ if assert.NotNil(t, err) {
+ assert.Equals(t, err.Error(), "exit status 1")
+ }
+ if t.Failed() {
+ t.Errorf("Error message did not match for command `%s`", j.command.cmd())
+ }
+ })
+ } else {
+ j.command.fail(t, name, expected, "")
+ }
+}
+
+func decodeB64Json(s string) (map[string]interface{}, error) {
+ b, err := base64.RawURLEncoding.DecodeString(s)
+ if err != nil {
+ return nil, err
+ }
+ m := make(map[string]interface{})
+ err = json.Unmarshal(b, &m)
+ if err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func encodeB64Json(o interface{}) (string, error) {
+ b, err := json.Marshal(o)
+ if err != nil {
+ return "", err
+ }
+ return base64.RawURLEncoding.EncodeToString(b), nil
+}
+
+func decodeJWT(jwt string) (map[string]interface{}, map[string]interface{}, string, error) {
+ parts := strings.Split(jwt, ".")
+ if len(parts) != 3 {
+ return nil, nil, "", errors.Errorf("invalid jwt; found %d parts", len(parts))
+ }
+ header, err := decodeB64Json(parts[0])
+ if err != nil {
+ return nil, nil, "", err
+ }
+ payload, err := decodeB64Json(parts[1])
+ if err != nil {
+ return nil, nil, "", err
+ }
+
+ return header, payload, strings.TrimRight(parts[2], "\n"), nil
+}
+
+func inspectJWT(jwt string) (map[string]interface{}, error) {
+ out, err := NewCLICommand().setCommand(`step crypto jwt inspect --insecure`).setStdin(jwt).run()
+ if err != nil {
+ return nil, err
+ }
+ inspect := make(map[string]interface{})
+ err = json.Unmarshal([]byte(out.stderr), &inspect)
+ return inspect, err
+}
+
+func (j JWTSignTest) checkJwt(t *testing.T, jwt string) {
+ header, payload, signature, err := decodeJWT(jwt)
+ assert.FatalError(t, err)
+
+ inspect, err := inspectJWT(strings.Trim(jwt, " \r\n"))
+ assert.FatalError(t, err)
+ assert.True(t, reflect.DeepEqual(header, inspect["header"]))
+ assert.True(t, reflect.DeepEqual(payload, inspect["payload"]))
+ assert.Equals(t, signature, inspect["signature"])
+
+ assert.Equals(t, "JWT", header["typ"])
+ // TODO: Check that the correct alg is in the JWT
+ //assert.Equals(t, "ES256", header["alg"])
+
+ if sub, ok := j.command.flags["sub"]; ok {
+ assert.Equals(t, sub, payload["sub"])
+ } else {
+ _, ok = payload["sub"]
+ assert.False(t, ok)
+ }
+ if iss, ok := j.command.flags["iss"]; ok {
+ assert.Equals(t, iss, payload["iss"])
+ } else {
+ _, ok = payload["iss"]
+ assert.False(t, ok)
+ }
+ if jti, ok := j.command.flags["jti"]; ok {
+ assert.Equals(t, jti, payload["jti"])
+ } else {
+ _, ok = payload["jti"]
+ assert.False(t, ok)
+ }
+
+ // TODO: Check aud (may be an array)
+ //assert.Equals(t, j.command.flags["aud"], payload["aud"])
+
+ // TODO: Check additional payload claims from stdin
+
+ if _, ok := j.command.flags["exp"]; ok {
+ eexp, err := strconv.Atoi(j.command.flags["exp"])
+ assert.FatalError(t, err)
+ aexp := int(payload["exp"].(float64))
+ assert.Equals(t, eexp, aexp)
+ }
+
+ iat := payload["iat"].(float64)
+ nbf := payload["nbf"].(float64)
+ now := float64(time.Now().Unix())
+ assert.True(t, math.Abs(now-iat) < 10)
+ assert.True(t, math.Abs(now-nbf) < 10)
+ if _, ok := j.command.flags["iat"]; ok {
+ eiat, err := strconv.Atoi(j.command.flags["iat"])
+ assert.FatalError(t, err)
+ assert.Equals(t, eiat, int(iat))
+ }
+ if _, ok := j.command.flags["nbf"]; ok {
+ enbf, err := strconv.Atoi(j.command.flags["nbf"])
+ assert.FatalError(t, err)
+ assert.Equals(t, enbf, int(nbf))
+ }
+}
+
+type JWTVerifyTest struct {
+ command CLICommand
+ jwk JWK
+}
+
+func NewJWTVerifyTest(jwk JWK) JWTVerifyTest {
+ cmd := NewCLICommand().setCommand("step crypto jwt verify").setFlag("key", jwk.pubfile)
+ return JWTVerifyTest{cmd, jwk}
+}
+
+func (j JWTVerifyTest) setFlag(key, value string) JWTVerifyTest {
+ return JWTVerifyTest{j.command.setFlag(key, value), j.jwk}
+}
+
+func (j JWTVerifyTest) test(t *testing.T, name, jwt string) {
+ t.Run(name, func(t *testing.T) {
+ out, err := j.command.setStdin(jwt).run()
+ assert.FatalError(t, err, fmt.Sprintf("`%s`: returned error '%s'\n\nOutput:\n%s\n\nJWT:\n%s", j.command.cmd(), err, out.combined, jwt))
+ jwt := make(map[string]interface{})
+ err = json.Unmarshal([]byte(out.combined), &jwt)
+ assert.FatalError(t, err)
+
+ // TODO: Factor out some of this / combine with checkJwt above.
+ header := jwt["header"].(map[string]interface{})
+ payload := jwt["payload"].(map[string]interface{})
+ assert.Equals(t, "JWT", header["typ"])
+
+ // Alg in the header, cli flags, and JWK, respectively (might be nil if not set)
+ halg, shalg := header["alg"]
+ falg, sfalg := j.command.flags["alg"]
+ kalg, skalg := func() (string, bool) {
+ if j.jwk.ispem {
+ return "", false
+ } else if j.jwk.iskeyset {
+ jwks, err := readJSON(j.jwk.pubfile)
+ assert.FatalError(t, err)
+ for _, e := range jwks["keys"].([]interface{}) {
+ jwk := e.(map[string]interface{})
+ if jwk["kid"].(string) == j.command.flags["kid"] {
+ kalg, skalg := jwk["alg"]
+ return kalg.(string), skalg
+ }
+ }
+ return "", false
+ } else {
+ jwk, err := readJSON(j.jwk.pubfile)
+ assert.FatalError(t, err)
+ kalg, skalg := jwk["alg"]
+ return kalg.(string), skalg
+ }
+ }()
+ if sfalg {
+ if shalg {
+ assert.Equals(t, falg, halg)
+ }
+ if skalg {
+ assert.Equals(t, falg, kalg)
+ }
+ }
+ if shalg && skalg {
+ assert.Equals(t, halg, kalg)
+ }
+
+ if iss, ok := j.command.flags["iss"]; ok {
+ assert.Equals(t, iss, payload["iss"])
+ } else {
+ _, ok = j.command.flags["subtle"]
+ assert.True(t, ok)
+ }
+
+ if aud, ok := j.command.flags["aud"]; ok {
+ auds, ok := payload["aud"]
+ if ok {
+ switch auds := auds.(type) {
+ case string:
+ assert.Equals(t, aud, auds)
+ case []interface{}:
+ /*
+ TODO: This.
+ if len(auds) == 1 {
+ t.Errorf(`single "aud" in JWT should be string not array`)
+ }
+ */
+ found := false
+ for _, a := range auds {
+ found = (a.(string) == aud)
+ }
+ assert.True(t, found)
+ default:
+ t.Errorf("unexpected type for aud: %T", auds)
+ }
+ } else {
+ t.Error(`no "aud" property in JWT`)
+ }
+ }
+
+ iat, hiat := payload["iat"]
+ nbf, hnbf := payload["nbf"]
+ exp, hexp := payload["exp"]
+ now := float64(time.Now().Unix())
+ if hiat {
+ assert.True(t, math.Abs(now-iat.(float64)) < 10)
+ }
+ if hnbf {
+ assert.True(t, math.Abs(now-nbf.(float64)) < 10)
+ }
+ if hexp {
+ assert.True(t, exp.(float64) > now)
+ }
+ })
+}
+
+func (j JWTVerifyTest) fail(t *testing.T, name string, jwt string, expected interface{}) {
+ j.command.setStdin(jwt).fail(t, name, expected, "")
+}
+
+type JWTTest struct {
+ sign JWTSignTest
+ verify JWTVerifyTest
+}
+
+func NewJWTTest(jwk JWK) JWTTest {
+ return JWTTest{NewJWTSignTest(jwk), NewJWTVerifyTest(jwk)}
+}
+
+func (j JWTTest) setFlag(key, value string) JWTTest {
+ return j.setSFlag(key, value).setVFlag(key, value)
+}
+
+func (j JWTTest) setSFlag(key, value string) JWTTest {
+ return JWTTest{j.sign.setFlag(key, value), j.verify}
+}
+
+func (j JWTTest) exp(d time.Duration) JWTTest {
+ return JWTTest{j.sign.exp(d), j.verify}
+}
+
+func (j JWTTest) nbf(d time.Duration) JWTTest {
+ return JWTTest{j.sign.nbf(d), j.verify}
+}
+
+func (j JWTTest) iat(d time.Duration) JWTTest {
+ return JWTTest{j.sign.iat(d), j.verify}
+}
+
+func (j JWTTest) setVFlag(key, value string) JWTTest {
+ return JWTTest{j.sign, j.verify.setFlag(key, value)}
+}
+
+func (j JWTTest) test(t *testing.T, name string) {
+ t.Run(name, func(t *testing.T) {
+ jwt := j.sign.test(t, "sign")
+ j.verify.test(t, "verify", jwt)
+ if t.Failed() {
+ fmt.Printf("Commands:\n\t%s\n\t%s\n", j.sign.command.cmd(), j.verify.command.cmd())
+ }
+ })
+}
+
+var rsrc csrc
+var r = rand.New(rsrc)
+
+type csrc struct{}
+
+func (s csrc) Seed(seed int64) {}
+func (s csrc) Int63() int64 {
+ return int64(s.Uint64() & ^uint64(1<<63))
+}
+func (s csrc) Uint64() (v uint64) {
+ err := binary.Read(crand.Reader, binary.BigEndian, &v)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
+func FakeURL() string {
+ scheme := []string{"http", "https"}[r.Intn(2)]
+ domain := fake.DomainName()
+ n := r.Intn(5)
+ path := make([]string, n)
+ for i := 0; i < n; i++ {
+ path[i] = fake.Word()
+ }
+ return scheme + "://" + domain + "/" + strings.Join(path, "/")
+}
+
+// A principal is usually a name, URL, or email address.
+func FakePrincipal() string {
+ return []string{fake.EmailAddress(), FakeURL(), fake.FullName()}[r.Intn(3)]
+}
+
+func randid() string {
+ b := make([]byte, 10)
+ _, err := rand.Read(b)
+ if err != nil {
+ panic(err)
+ }
+ return base64.RawURLEncoding.EncodeToString(b)
+}
+
+func NewJWK(kty string, t *testing.T) JWK {
+ jwk := NewJWKTest(fmt.Sprintf("jwt-jwk-%s", kty))
+ jwk = jwk.setFlag("kty", kty).setFlag("kid", randid())
+ if kty == "OKP" {
+ jwk = jwk.setFlag("crv", "Ed25519")
+ }
+ return JWKFromTest(t, jwk)
+}
+
+func JWKFromTest(t *testing.T, jt JWKTest) JWK {
+ _, password := jt.test(t)
+ return JWK{jt.pubfile, jt.prvfile, password, false, false}
+}
+
+func TestCryptoJWT(t *testing.T) {
+ // Generate some JWKs that we can use for testing.
+ jwkec := NewJWK("EC", t)
+ jwkrsa := NewJWK("RSA", t)
+ jwkoct := NewJWK("oct", t)
+ jwkokp := NewJWK("OKP", t)
+ jwknopass := JWKFromTest(t, NewJWKTest("jwt-jwk-nopass").setFlag("kty", "EC").setFlag("no-password", "").setFlag("insecure", ""))
+
+ t.Run("jwt", func(t *testing.T) {
+ mkjwt := func(jwk JWK) JWTTest {
+ // Audience, issuer, and subject can be emails, URLs, or any other string.
+ aud := FakePrincipal()
+ iss := FakePrincipal()
+ sub := FakePrincipal()
+ return NewJWTTest(jwk).setFlag("aud", aud).setFlag("iss", iss).setSFlag("sub", sub).exp(1 * time.Minute)
+ }
+ mkjwt(jwkec).test(t, "jwt-ec")
+ mkjwt(jwkrsa).test(t, "jwt-rsa")
+ mkjwt(jwkoct).test(t, "jwt-oct")
+ mkjwt(jwkokp).test(t, "jwt-okp")
+ mkjwt(jwknopass).test(t, "jwt-nopass")
+
+ t.Run("sign", func(t *testing.T) {
+ jwtrsa := mkjwt(jwkrsa).sign
+ jwtrsa.setFlag("alg", "RS384").fail(t, "wrong-alg", "alg RS384 does not match the alg on testdata-tmp/jwt-jwk-RSA-prv.json\n")
+
+ mkjwt(JWKFromTest(t, NewJWKTest("jwt-jwk-enc").setFlag("use", "enc").setFlag("no-password", "").setFlag("insecure", ""))).sign.fail(t, "use-enc", "cannot sign using key with intended use \"enc\" (must be \"sig\")\n")
+
+ subtle := NewJWTTest(jwkrsa).sign
+ subtle.fail(t, "no-aud-iss-sub-exp", "flag '--iss' is required unless '--subtle' is used\n")
+ subtle.setFlag("sub", FakePrincipal()).setFlag("iss", FakePrincipal()).exp(1*time.Minute).fail(t, "no-aud", "flag '--aud' is required unless '--subtle' is used\n")
+ subtle.setFlag("sub", FakePrincipal()).setFlag("aud", FakePrincipal()).exp(1*time.Minute).fail(t, "no-iss", "flag '--iss' is required unless '--subtle' is used\n")
+ subtle.setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()).exp(1*time.Minute).fail(t, "no-sub", "flag '--sub' is required unless '--subtle' is used\n")
+ subtle.setFlag("sub", FakePrincipal()).setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()).fail(t, "no-exp", "flag '--exp' is required unless '--subtle' is used\n")
+ subtle = subtle.setFlag("subtle", "")
+ subtle.test(t, "no-aud-iss-sub-exp-subtle")
+ subtle.setFlag("sub", FakePrincipal()).setFlag("iss", FakePrincipal()).exp(1*time.Minute).test(t, "no-aud-subtle")
+ subtle.setFlag("sub", FakePrincipal()).setFlag("aud", FakePrincipal()).exp(1*time.Minute).test(t, "no-iss-subtle")
+ subtle.setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()).exp(1*time.Minute).test(t, "no-sub-subtle")
+ subtle.setFlag("sub", FakePrincipal()).setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()).test(t, "no-exp-subtle")
+
+ nouse := mkjwt(JWK{"testdata/jwk-no-use.pub.json", "testdata/jwk-no-use.json", "", false, false})
+ nouse.test(t, "no-use")
+ noalg := mkjwt(JWK{"testdata/jwk-no-alg.pub.json", "testdata/jwk-no-alg.json", "", false, false})
+ noalg.sign.test(t, "no-alg")
+
+ mkjwt(JWK{"foo", "foo", "", false, false}).sign.fail(t, "missing-key-file", "error reading foo: open foo: no such file or directory\n")
+ mkjwt(JWK{"testdata/bad-key.pub.json", "testdata/bad-key.json", "", false, false}).sign.fail(t, "bad-key-file", "error reading testdata/bad-key.json: unsupported format\n")
+ mkjwt(JWK{"testdata/bad-key.pub.json", "testdata/bad-key.json", "", true, false}).sign.fail(t, "bad-key-file-pem", "error reading testdata/bad-key.json: unsupported format\n")
+ mkjwt(JWK{"testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.pub.json", "testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.pub.json", "", false, false}).sign.fail(t, "sign-with-pubkey", "cannot use a public key for signing\n")
+ mkjwt(JWK{"testdata/p256.pem.pub", "testdata/p256.pem.pub", "", true, false}).sign.fail(t, "sign-with-pubkey-pem", "cannot use a public key for signing\n")
+ mkjwt(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).sign.fail(t, "pem-alg-required", "flag '--alg' is required with the given key\n")
+ mkjwt(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).sign.setFlag("alg", "RS256").test(t, "pem-alg-required")
+ mkjwt(JWK{"testdata/rsa2048.pub", "testdata/twopems.pem", "", true, false}).sign.setFlag("alg", "RS256").fail(t, "multiple-keys", "error decoding PEM: file 'testdata/twopems.pem' contains more than one key\n")
+ mkjwt(JWK{"testdata/rsa2048.pub", "testdata/badheader.pem", "", true, false}).sign.setFlag("alg", "RS256").fail(t, "multiple-keys", "error decoding PEM: file 'testdata/badheader.pem' contains an unexpected header 'FOO PRIVATE KEY'\n")
+ mkjwt(JWK{"testdata/es256-enc.pub", "testdata/es256-enc.pem", "password", true, false}).sign.setFlag("alg", "ES256").test(t, "pem-encrypted")
+ mkjwt(JWK{"testdata/es256-enc.pub", "testdata/es256-enc.pem", "password", true, false}).sign.setFlag("alg", "RS256").fail(t, "pem-bad-alg", "alg RS256 does not match the alg on testdata/es256-enc.pem\n")
+
+ mkjwt(jwkrsa).exp(-1*time.Minute).sign.fail(t, "exp-in-past", "flag '--exp' must be in the future unless '--subtle' is used\n")
+ mkjwt(jwkrsa).exp(-1*time.Minute).setFlag("subtle", "").sign.test(t, "exp-in-past-subtle")
+
+ mkjwt(jwkrsa).setSFlag("jti", "foo").test(t, "jti")
+
+ keyset := JWTSignTest{NewCLICommand().setCommand("step crypto jwt sign").setFlag("jwks", "testdata/jwks.json"), JWK{"testdata/jwks.pub.json", "testdata/jwks.json", "", false, true}}
+ keyset = keyset.setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal()).setFlag("sub", FakePrincipal()).exp(1 * time.Minute)
+ keyset.fail(t, "keyset-no-kid", "flag `--kid` is required with `--jwks`\n")
+ keyset.setFlag("kid", "1").test(t, "keyset-kid1")
+ keyset.setFlag("kid", "2").test(t, "keyset-kid2")
+ keyset.setFlag("kid", "3").fail(t, "keyset-kid3", "cannot sign using key with intended use \"enc\" (must be \"sig\")\n")
+ keyset.setFlag("kid", "4").fail(t, "keyset-kid4", "cannot find key with kid 4 on testdata/jwks.json\n")
+ keyset.setFlag("kid", "1").setFlag("key", "foo").fail(t, "keyset-and-key", "flags `--key` and `--jwks` are mutually exclusive\n")
+ keyset.setFlag("jwks", "foo").setFlag("kid", "1").fail(t, "nonexistent-keyset", "error reading foo: open foo: no such file or directory\n")
+ keyset.setFlag("jwks", "testdata/rsa2048.pem").setFlag("kid", "1").fail(t, "bad-keyset", "error reading testdata/rsa2048.pem: unsupported format\n")
+ })
+
+ t.Run("verify", func(t *testing.T) {
+ setHeader := func(jwt string, hdr string, val interface{}) string {
+ parts := strings.Split(jwt, ".")
+ if len(parts) != 3 {
+ assert.FatalError(t, errors.Errorf("invalid jwt; found %d parts", len(parts)))
+ }
+ header, err := decodeB64Json(parts[0])
+ if err != nil {
+ assert.FatalError(t, err)
+ }
+ header[hdr] = val
+ parts[0], err = encodeB64Json(header)
+ if err != nil {
+ assert.FatalError(t, err)
+ }
+ return strings.Join(parts, ".")
+ }
+
+ tst := mkjwt(jwkokp)
+ jwt := tst.sign.test(t, "sign")
+ tst.verify.fail(t, "wrong-signature", jwt[:len(jwt)-5]+"12345", "validation failed: invalid signature\n")
+ tst.verify.setFlag("iss", "wrong issuer").fail(t, "iss-mismatch", jwt, "validation failed: invalid issuer claim (iss)\n")
+ tst.verify.setFlag("aud", "wrong audience").fail(t, "aud-mismatch", jwt, "validation failed: invalid audience claim (aud)\n")
+ tst.verify.fail(t, "crit-header", setHeader(jwt, "crit", []string{"exp"}), "validation failed: unrecognized critical headers (crit)\n")
+ tst.verify.fail(t, "invalid-jwt", "asdf", "error parsing token: compact JWS format must have three parts\n")
+ tst.verify.fail(t, "invalid-jwt-parts", "foo.bar.deadbeef", "error parsing token: invalid character '~' looking for beginning of value\n")
+
+ fakejwt := func(header, payload, signature string) string {
+ header = base64.RawURLEncoding.EncodeToString([]byte(header))
+ payload = base64.RawURLEncoding.EncodeToString([]byte(payload))
+ return strings.Join([]string{header, payload, signature}, ".")
+ }
+ parts := strings.Split(jwt, ".")
+ tst.verify.fail(t, "invalid-jwt-header", fakejwt("foo", parts[1], parts[2]), "error parsing token: invalid character 'o' in literal false (expecting 'a')\n")
+ tst.verify.fail(t, "invalid-jwt-header-json", fakejwt("[42]", "bar", "deadbeef"), "error parsing token: json: cannot unmarshal array into Go value of type jose.rawHeader\n")
+ tst.verify.fail(t, "invalid-jwt-header-changed-attrib", fakejwt(`{"kty":"EC","alg":"ES256","xxx":"yyy"}`, parts[1], parts[2]), "validation failed: invalid signature\n")
+ tst.verify.fail(t, "invalid-jwt-header-bad-json", fakejwt(`{"kty":"EC","alg":"ES256","}`, parts[1], parts[2]), "error parsing token: unexpected end of JSON input\n")
+ tst.verify.fail(t, "invalid-jwt-payload", fakejwt(parts[0], "foo", parts[2]), "error parsing token: invalid character 'e' looking for beginning of value\n")
+
+ subtle := NewJWTTest(jwkokp).exp(1 * time.Minute).verify
+ subtle.fail(t, "no-aud-iss", jwt, "flag '--iss' is required unless '--subtle' is used\n")
+ subtle.setFlag("iss", tst.verify.command.flags["iss"]).fail(t, "no-aud", jwt, "flag '--aud' is required unless '--subtle' is used\n")
+ subtle.setFlag("aud", tst.verify.command.flags["aud"]).fail(t, "no-iss", jwt, "flag '--iss' is required unless '--subtle' is used\n")
+ subtle = subtle.setFlag("subtle", "")
+ subtle.test(t, "no-aud-iss-subtle", jwt)
+ subtle.setFlag("iss", tst.verify.command.flags["iss"]).test(t, "no-aud-subtle", jwt)
+ subtle.setFlag("aud", tst.verify.command.flags["aud"]).test(t, "no-iss-subtle", jwt)
+
+ t.Run("keyset", func(t *testing.T) {
+ aud := FakePrincipal()
+ sub := FakePrincipal()
+ iss := FakePrincipal()
+ jwt = JWTSignTest{NewCLICommand().setCommand("step crypto jwt sign").setFlag("jwks", "testdata/jwks.json"), JWK{"testdata/jwks.pub.json", "testdata/jwks.json", "", false, true}}.setFlag("kid", "1").setFlag("aud", aud).setFlag("iss", iss).setFlag("sub", sub).exp(1*time.Minute).test(t, "keyset-sign")
+ keyset := JWTVerifyTest{NewCLICommand().setCommand("step crypto jwt verify").setFlag("jwks", "testdata/jwks.pub.json"), JWK{"testdata/jwks.pub.json", "testdata/jwks.json", "", false, true}}.setFlag("aud", aud).setFlag("iss", iss)
+ keyset.setFlag("kid", "1").test(t, "keyset", jwt)
+ keyset.setFlag("kid", "2").fail(t, "wrong-kid", jwt, "validation failed: invalid signature\n")
+ keyset.setFlag("kid", "4").fail(t, "kid-not-found", jwt, "cannot find key with kid 4 on testdata/jwks.pub.json\n")
+ // "kid" should be optional if it's in the JWT, else required
+ keyset.test(t, "kid-in-jwt", jwt)
+ jwt = mkjwt(JWK{"testdata/jwks-key1.json", "testdata/jwks-key1.json", "", false, false}).sign.setFlag("aud", aud).setFlag("iss", iss).setFlag("sub", sub).setFlag("no-kid", "").exp(1*time.Minute).test(t, "keyset-key1")
+ keyset.fail(t, "no-kid-in-jwt", jwt, "flag `--kid` is required with `--jwks`\n")
+ })
+
+ // JWK without `alg` should require --alg flag
+ mkossljwt := func(t *testing.T, header, payload, key string) string {
+ cmd := fmt.Sprintf("./openssl-jwt.sh -a RS256 -k %s '%s' '%s'", key, header, payload)
+ jwt, err := exec.Command("bash", "-c", cmd).CombinedOutput()
+ assert.FatalError(t, err)
+ return string(jwt)
+ }
+ t.Run("pem", func(t *testing.T) {
+ exp := time.Now().Add(1 * time.Minute)
+ jwt = mkossljwt(t, `{"typ": "JWT", "alg": "RS256"}`, fmt.Sprintf(`{"iss": "foo", "aud": "bar", "exp": %d}`, exp.Unix()), "testdata/rsa2048.pem")
+ vtst := NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false})
+ vtst.setFlag("iss", "foo").setFlag("aud", "bar").fail(t, "no-alg", jwt, "flag '--alg' is required with the given key\n")
+ vtst.setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS256").test(t, "verify", jwt)
+ vtst.setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS384").fail(t, "alg-mismatch", jwt, "alg RS384 does not match the alg on JWT (RS256)\n")
+ jwt = mkossljwt(t, `{"typ": "JWT", "alg": "RS384"}`, `{"iss": "foo", "aud": "bar"}`, "testdata/rsa2048.pem")
+ vtst.setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS384").fail(t, "wrong-alg", jwt, "validation failed: invalid signature\n")
+ vtst.setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS256").fail(t, "wrong-alg-mismatch", jwt, "alg RS256 does not match the alg on JWT (RS384)\n")
+ })
+ expiredZero := mkossljwt(t, `{"typ": "JWT", "alg": "RS256"}`, `{"exp": 0}`, "testdata/rsa2048.pem")
+ NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).setFlag("subtle", "").setFlag("alg", "RS256").fail(t, "expired-zero", expiredZero, regexp.MustCompile(`^validation failed: token is expired by (\d+h\d+m\d+\.\d+s) \(exp\)\n`))
+ expired := mkossljwt(t, `{"typ": "JWT", "alg": "RS256"}`, `{"exp": 12345}`, "testdata/rsa2048.pem")
+ NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).setFlag("subtle", "").setFlag("alg", "RS256").fail(t, "expired", expired, regexp.MustCompile(`^validation failed: token is expired by (\d+h\d+m\d+\.\d+s) \(exp\)\n`))
+ noexp := mkossljwt(t, `{"typ": "JWT", "alg": "RS256"}`, `{"iss": "foo", "aud": "bar"}`, "testdata/rsa2048.pem")
+ NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).setFlag("no-exp-check", "").setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS256").setFlag("no-exp-check", "").fail(t, "no-exp-fail", noexp, "flag '--no-exp-check' requires the '--insecure' flag\n")
+ NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).setFlag("no-exp-check", "").setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS256").setFlag("no-exp-check", "").setFlag("insecure", "").test(t, "no-exp", noexp)
+ texp := NewJWTTest(jwkrsa).setSFlag("sub", FakePrincipal()).setFlag("aud", FakePrincipal()).setFlag("iss", FakePrincipal())
+ jwt = texp.sign.setFlag("subtle", "").test(t, "no-exp-sign")
+ texp.verify.fail(t, "no-exp-verify-fail", jwt, "jwt must have \"exp\" property unless '--subtle' is used\n")
+ texp.verify.setFlag("no-exp-check", "").setFlag("insecure", "").test(t, "empty-exp-in-jwt", jwt)
+ texp.verify.setFlag("subtle", "").test(t, "no-exp-verify-subtle", jwt)
+
+ // Can't serialize OKP (Ed25519) keys yet. Switch to using RSA.
+ tst = mkjwt(jwkrsa)
+ pem, err := tst.verify.jwk.pem()
+ assert.FatalError(t, err)
+ jwt = mkossljwt(t, `{"typ": "JWT", "alg": "RS384"}`, `{"iss": "foo", "sub": "bar"}`, fmt.Sprintf("<(echo -en %q)", pem))
+ tst.verify.setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS384").fail(t, "wrong-alg", jwt, "alg RS384 does not match the alg on testdata-tmp/jwt-jwk-RSA-pub.json\n")
+
+ // We don't currently support JSON Serialization, Flattened JSON Serialzation, or multiple signatures
+ // TODO: Right now these are parse failures. They should probably parse correctly and give more helpful error messages.
+ vtst := NewJWTVerifyTest(JWK{"testdata/rsa2048.pub", "testdata/rsa2048.pem", "", true, false}).setFlag("iss", "foo").setFlag("aud", "bar").setFlag("alg", "RS256")
+ jwtb, _ := ioutil.ReadFile("testdata/jwt-json-serialization.json")
+ vtst.fail(t, "json-serialization", string(jwtb), "error parsing token: unexpected end of JSON input\n")
+ jwtb, _ = ioutil.ReadFile("testdata/jwt-json-serialization-flattened.json")
+ vtst.fail(t, "json-serialization-flattened", string(jwtb), "error parsing token: unexpected end of JSON input\n")
+ jwtb, _ = ioutil.ReadFile("testdata/jwt-json-serialization-multi.json")
+ vtst.fail(t, "json-serialization-multi", string(jwtb), "error parsing token: unexpected end of JSON input\n")
+ })
+
+ // Should fail (token not yet valid)
+ t.Run("timestamps", func(t *testing.T) {
+ t.Parallel()
+ mkjwt(jwkrsa).iat(1*time.Second).test(t, "iat")
+ t.Run("nbf", func(t *testing.T) {
+ tst := mkjwt(jwkec).nbf(1 * time.Second)
+ jwt := tst.nbf(1*time.Second).sign.test(t, "sign")
+ tst.verify.fail(t, "verify-tosoon", jwt, "validation failed: token not valid yet (nbf)\n")
+ time.Sleep(1 * time.Second)
+ tst.verify.test(t, "verify-succeed", jwt)
+ if t.Failed() {
+ t.Logf("jwt: %s", jwt)
+ }
+ })
+ t.Run("exp", func(t *testing.T) {
+ tst := mkjwt(jwkec).exp(1 * time.Second)
+ jwt := tst.sign.test(t, "sign")
+ tst.verify.test(t, "verify-succeed", jwt)
+ time.Sleep(1 * time.Second)
+ tst.verify.fail(t, "verify-expired", jwt, regexp.MustCompile(`^validation failed: token is expired by (\d\d\dms|\d\.\d+s) \(exp\)\n`))
+ if t.Failed() {
+ t.Logf("jwt: %s", jwt)
+ }
+ })
+ })
+
+ t.Run("wrong-pass", func(t *testing.T) {
+ tst := mkjwt(jwkrsa).setFlag("aud", "a").setFlag("iss", "i").setSFlag("sub", "s").exp(1 * time.Minute)
+ cmd, err := gexpect.Spawn(tst.sign.command.cmd())
+ assert.FatalError(t, err)
+ prompt := "Please enter the password to decrypt " + tst.sign.jwk.prvfile + ": "
+ assert.FatalError(t, cmd.ExpectTimeout(prompt, 1*time.Second))
+ assert.FatalError(t, cmd.SendLine("foo"))
+ assert.FatalError(t, cmd.ExpectTimeout(fmt.Sprintf("failed to decrypt %s: square/go-jose: error in cryptographic primitive", tst.sign.jwk.prvfile), 1*time.Second))
+ // TODO: This should re-prompt for the password, not fail!
+ //assert.FatalError(t, cmd.SendLine(t.jwk.password))
+ })
+
+ t.Run("inspect", func(t *testing.T) {
+ NewCLICommand().setCommand(`echo "foo" | step crypto jwt inspect`).fail(t, "requires-insecure", "'step crypto jwt inspect' must include the '--insecure' flag\n", "")
+ })
+ })
+}
diff --git a/integration/keypair_test.go b/integration/keypair_test.go
new file mode 100644
index 00000000..1a594637
--- /dev/null
+++ b/integration/keypair_test.go
@@ -0,0 +1,101 @@
+// +build integration
+
+package integration
+
+import (
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/ThomasRooney/gexpect"
+ "github.com/smallstep/assert"
+)
+
+type KeypairCmd struct {
+ name string
+ command CLICommand
+ pubfile string
+ prvfile string
+ password string
+}
+
+func (k KeypairCmd) setFlag(key, value string) KeypairCmd {
+ return KeypairCmd{k.name, k.command.setFlag(key, value), k.pubfile, k.prvfile, k.password}
+}
+
+func (k KeypairCmd) setPassword(password string) KeypairCmd {
+ return KeypairCmd{k.name, k.command, k.pubfile, k.prvfile, password}
+}
+
+func (k KeypairCmd) testJwtSignVerify(t *testing.T) {
+ aud := FakePrincipal()
+ iss := FakePrincipal()
+ sub := FakePrincipal()
+ jwk := JWK{k.pubfile, k.prvfile, k.password, true, false}
+ test := NewJWTTest(jwk).setFlag("aud", aud).setFlag("iss", iss).setSFlag("sub", sub).exp(1 * time.Minute)
+ if k.command.flags["type"] == "RSA" {
+ test = test.setFlag("alg", "RS256")
+ }
+ test.test(t, fmt.Sprintf("%s-jwt-sign-verify", k.name))
+}
+
+func (k KeypairCmd) test(t *testing.T) {
+ t.Run(k.name, func(t *testing.T) {
+ cmd, err := gexpect.Spawn(k.command.cmd())
+ assert.FatalError(t, err)
+ prompt := fmt.Sprintf("Password with which to encrypt private key file `%s`: ", k.prvfile)
+ assert.FatalError(t, cmd.ExpectTimeout(prompt, 10*time.Second))
+ assert.FatalError(t, cmd.SendLine(k.password))
+ k.testJwtSignVerify(t)
+ })
+}
+
+func (k KeypairCmd) testNoPass(t *testing.T) {
+ k.command.test(t, k.name, "", "")
+ k.testJwtSignVerify(t)
+}
+
+func (k KeypairCmd) fail(t *testing.T, expected string) {
+ k.command.fail(t, k.name, expected, "")
+}
+
+func (k KeypairCmd) failNoPass(t *testing.T, expected string) {
+ k.command.fail(t, k.name, expected, "")
+}
+
+func NewKeypairCmd(name string) KeypairCmd {
+ pubfile := fmt.Sprintf("%s/%s.pub", TempDirectory, name)
+ prvfile := fmt.Sprintf("%s/%s.pem", TempDirectory, name)
+ command := NewCLICommand().setCommand(fmt.Sprintf("step crypto keypair %s %s", pubfile, prvfile))
+ return KeypairCmd{name, command, pubfile, prvfile, "password"}
+}
+
+func TestCryptoKeypair(t *testing.T) {
+ NewCLICommand().setCommand("step crypto keypair").fail(t, "no-args", "missing positional arguments 'PUB_FILE' 'PRIV_FILE'\n", "")
+ NewCLICommand().setCommand("step crypto keypair foo").fail(t, "no-args", "missing positional argument 'PRIV_FILE'\n", "")
+ NewKeypairCmd("default").test(t)
+ t.Run("RSA", func(t *testing.T) {
+ NewKeypairCmd("RSA-default").setFlag("type", "RSA").test(t)
+ NewKeypairCmd("RSA-size-0-fail").setFlag("type", "RSA").setFlag("size", "0").fail(t, "minimum '--size' for RSA keys is 2048 bits without '--insecure' flag\n")
+ NewKeypairCmd("RSA-size-16-fail").setFlag("type", "RSA").setFlag("size", "16").fail(t, "minimum '--size' for RSA keys is 2048 bits without '--insecure' flag\n")
+ NewKeypairCmd("RSA-size-neg1-fail").setFlag("type", "RSA").setFlag("size", "-1").setFlag("insecure", "").fail(t, "--size must be >= 0\n")
+ // Error when signing JWT: "error serializing JWT: crypto/rsa: message too long for RSA public key size"
+ //NewKeypairCmd("RSA-size-16").setFlag("type", "RSA").setFlag("size", "16").setFlag("insecure", "").test(t)
+ NewKeypairCmd("RSA-size-1024-fail").setFlag("type", "RSA").setFlag("size", "1024").fail(t, "minimum '--size' for RSA keys is 2048 bits without '--insecure' flag\n")
+ NewKeypairCmd("RSA-size-1024").setFlag("type", "RSA").setFlag("size", "1024").setFlag("insecure", "").test(t)
+ NewKeypairCmd("RSA-size-3072").setFlag("type", "RSA").setFlag("size", "3072").test(t)
+ NewKeypairCmd("RSA-size-4096").setFlag("type", "RSA").setFlag("size", "4096").test(t)
+ NewKeypairCmd("RSA-curve").setFlag("type", "RSA").setFlag("crv", "P-256").fail(t, "key type 'RSA' is not compatible with flag '--crv'\n")
+ })
+ t.Run("EC", func(t *testing.T) {
+ NewKeypairCmd("EC-default").setFlag("type", "EC").test(t)
+ NewKeypairCmd("P-256").setFlag("type", "EC").setFlag("crv", "P-256").test(t)
+ NewKeypairCmd("P-384").setFlag("type", "EC").setFlag("curve", "P-384").test(t)
+ NewKeypairCmd("P-521").setFlag("type", "EC").setFlag("crv", "P-521").test(t)
+ NewKeypairCmd("bad-crv").setFlag("type", "EC").setFlag("curve", "P-512").fail(t, "invalid value for argument crv (crv: 'P-512')\n")
+ NewKeypairCmd("EC-size").setFlag("type", "EC").setFlag("size", "2048").fail(t, "key type 'EC' is not compatible with flag '--size'\n")
+ })
+ NewKeypairCmd("bad-type").setFlag("type", "foo").fail(t, "unrecognized key type: foo\n")
+ NewKeypairCmd("no-pass-fail").setFlag("no-password", "").failNoPass(t, "flag '--no-password' requires the '--insecure' flag\n")
+ NewKeypairCmd("no-pass").setPassword("").setFlag("no-password", "").setFlag("insecure", "").testNoPass(t)
+}
diff --git a/integration/openssl-jwt.sh b/integration/openssl-jwt.sh
new file mode 100755
index 00000000..0957ba88
--- /dev/null
+++ b/integration/openssl-jwt.sh
@@ -0,0 +1,41 @@
+#! /usr/bin/env bash
+
+ALG="RS256"
+
+while getopts "a:k:s:" opt; do
+ case "$opt" in
+ k) KEY=$OPTARG ;;
+ s) KEY=<(echo -en "$OPTARG") ;;
+ a) ALG=$OPTARG ;;
+ esac
+done
+
+shift $((OPTIND-1))
+
+HEADER=$1
+PAYLOAD=$2
+
+function b64enc() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; }
+
+function convert_ec {
+ INPUT=$(openssl asn1parse -inform der)
+ R=$(echo "$INPUT" | head -2 | tail -1 | cut -d':' -f4)
+ S=$(echo "$INPUT" | head -3 | tail -1 | cut -d':' -f4)
+
+ echo -n $R | xxd -r -p
+ echo -n $S | xxd -r -p
+}
+
+function rs_sign() { openssl dgst -binary -sha${1} -sign "$2"; }
+function es_sign() { openssl dgst -binary -sha${1} -sign "$2" | convert_ec; }
+
+JWT_HDR_B64="$(echo -n "$HEADER" | b64enc)"
+JWT_PAY_B64="$(echo -n "$PAYLOAD" | b64enc)"
+UNSIGNED_JWT="$JWT_HDR_B64.$JWT_PAY_B64"
+
+case "$ALG" in
+ RS*) SIGNATURE=$(echo -n "$UNSIGNED_JWT" | rs_sign "${ALG#RS}" "$KEY" | b64enc) ;;
+ ES*) SIGNATURE=$(echo -n "$UNSIGNED_JWT" | es_sign "${ALG#ES}" "$KEY" | b64enc) ;;
+esac
+
+echo "$UNSIGNED_JWT.$SIGNATURE"
diff --git a/integration/otp_test.go b/integration/otp_test.go
new file mode 100644
index 00000000..e1bd3c4e
--- /dev/null
+++ b/integration/otp_test.go
@@ -0,0 +1,73 @@
+// +build integration
+
+package integration
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/pquerna/otp"
+ "github.com/pquerna/otp/totp"
+ "github.com/smallstep/assert"
+)
+
+const (
+ totpSecretFile = "testdata/totp.secret"
+ totpSecret = "UPCTJYT7MUR4RWOUJ3TGTUB43IYCBJ76"
+ totpUrlFile = "testdata/totp.url"
+ totpUrl = "otpauth://totp/example.com:foo@example.com?algorithm=SHA1&digits=6&issuer=example.com&period=30&secret=EW32D2CFTAIRTEAWTRQZZXAITVA4U6K4"
+)
+
+func mkotp(subcommand string, flags map[string]string) CLICommand {
+ return CLICommand{fmt.Sprintf("step crypto otp %s", subcommand), "", flags, nil}
+}
+
+func TestCryptoOtp(t *testing.T) {
+ c := mkotp("generate", map[string]string{"issuer": "example.com", "account": "foo@example.com"})
+ t.Run("generate", func(t *testing.T) {
+ out, err := c.run()
+ assert.Nil(t, err)
+ assert.Equals(t, len(strings.TrimSuffix(out.combined, "\n")), 32)
+ })
+
+ c = mkotp("generate", map[string]string{"issuer": "example.com", "account": "foo@example.com", "url": ""})
+ t.Run("generate-url", func(t *testing.T) {
+ out, err := c.run()
+ assert.Nil(t, err)
+ assert.True(t, strings.HasPrefix(out.combined, "otpauth://"))
+ key, err := otp.NewKeyFromURL(out.combined)
+ assert.Nil(t, err)
+ assert.Equals(t, key.Type(), "totp")
+ assert.Equals(t, key.Issuer(), "example.com")
+ assert.Equals(t, key.AccountName(), "foo@example.com")
+ assert.True(t, len(key.Secret()) == 32)
+ })
+
+ c = mkotp("verify", map[string]string{"secret": totpSecretFile})
+ t.Run("verify", func(t *testing.T) {
+ code, err := totp.GenerateCode(totpSecret, time.Now())
+ assert.Nil(t, err)
+ out, err := c.setStdin(code).run()
+ assert.Nil(t, err)
+ assert.Equals(t, "Enter Passcode: ok\n", out.combined)
+ out, err = c.setStdin("foo").run()
+ assert.NotNil(t, err)
+ assert.Equals(t, "Enter Passcode: fail\n", out.combined)
+ })
+
+ c = mkotp("verify", map[string]string{"secret": totpUrlFile})
+ t.Run("verify-url", func(t *testing.T) {
+ key, err := otp.NewKeyFromURL(totpUrl)
+ assert.FatalError(t, err)
+ code, err := totp.GenerateCode(key.Secret(), time.Now())
+ assert.Nil(t, err)
+ out, err := c.setStdin(code).run()
+ assert.Nil(t, err)
+ assert.Equals(t, "Enter Passcode: ok\n", out.combined)
+ out, err = c.setStdin("foo").run()
+ assert.NotNil(t, err)
+ assert.Equals(t, "Enter Passcode: fail\n", out.combined)
+ })
+}
diff --git a/integration/testdata/bad-key.json b/integration/testdata/bad-key.json
new file mode 100644
index 00000000..b33c1389
--- /dev/null
+++ b/integration/testdata/bad-key.json
@@ -0,0 +1 @@
+foobarbaz
diff --git a/integration/testdata/bad-key.pub.json b/integration/testdata/bad-key.pub.json
new file mode 100644
index 00000000..b33c1389
--- /dev/null
+++ b/integration/testdata/bad-key.pub.json
@@ -0,0 +1 @@
+foobarbaz
diff --git a/integration/testdata/badheader.pem b/integration/testdata/badheader.pem
new file mode 100644
index 00000000..b57db0b0
--- /dev/null
+++ b/integration/testdata/badheader.pem
@@ -0,0 +1,27 @@
+-----BEGIN FOO PRIVATE KEY-----
+MIIEpAIBAAKCAQEAn4V0GxrIhjoheOpXLbJNg8GdjxCRhOGbY6HZRRWCzKaEq0+7
+iLh9z3v5ixWH6rnftTBsZeFKiikqb0tRRZymuOtUz42ZuzAoDiAwFQg/sYgNdkQQ
+zJHYNlbd7FF2YQyJ0JAw+cmvqbLzO0E+mgZImTNzCqzBBhLoUWjQqr1al7lXUAGw
+UvSrI7JdIMOfWEvQ2wgPqeoLL3AE97zEoiYyIaTrInQlNcA5Cs1mRa396ET6hw8b
+FfPZAXsW3m8JeGuqxJ23wL4jCee/w9v+qNL6oKwtpuCBOOnS5MEYH5GhvlBQ7baN
+zoTAT7uk9yc1qVd7aLr2RJ5yW2v2v5LazmUzeQIDAQABAoIBACPe7ZHevvK4Bajc
+AUiMTLPxCM4P6rkXxkpsLaBESwpb839WSZRf8CKE/UNSTyLwMybaQbXTKGDTCvDF
+3fuqUy9H8+VMMSKPnKI4iLdiCHiSYHyUp7ZooVbux66JTvZZzG+yzOCOgsrFK77K
+WBpoiVCx6g+fczQ7cjREPo/2TnXJY6ZQEntNeDnWN4CgJWDYTruPYTgTj84WUwPi
+4aTDzPwZcps45ZRBLlBmd2tIxy87jBSKzGvwKK9c7++mtiwr956d7HsosaqSSJCa
+jDO9JRlzcqAq8r+GZF1mmyoI3P6jgYWCElq+s1zZ0RZBaVfdhEl+AkR5pI3+oGdk
+h8AdzZECgYEA07YASLNHYm99H21e1X8Yh4zZK96MfTESvAHkcIXEU7y0zw9tczPL
+KTB6eXr9mY0JjSgVw94nx8YLYPU/dr0xqBZGD57FvqYRWdwdswk0CXSyEgWRccnB
+jfV391gauBi2gQLxzRZf7eNl2c15kabpV0KUi/Pfu645PB2bY1yD0/UCgYEAwOR7
+GAog8y4TsPYxNpNAdgnlFU/XpE3sJPIy6Q7cGN4cuWBsdaPToTkAQiQYa5UnC5ql
+dKHNwkVTwUvTa/pnRCShkdgNd2ycYDErbMe+DolEt1Ffr5+jVD4MILdGJq5CqGok
+PrLhH9ZU0mfFlc/4Hkt1VkcoWPt8uT4eL0GPsvUCgYEAhriCZcDv5AveK2mFt4Yx
+LdDLQcdUzzWzHkB2BcSZsk+bH0hJ9c03svZOeY9yYYwGT/T6JLHxzoaQJxrpT74F
+I1lJLBd07mTvFaeknpF0s6+2wREaBLbGnHdf594A4rWXLXGaPU/Hq7HQ1lCS08TL
+J+QOcyC1dtDfSwnsH8Z3fSECgYAS8t78v5H5EZ+xlJ3FBLYiYlp0u4EtjNIT1w8V
+QfZxIvCjbUt6SvuxLM5PsQgNGXvacfiq+nIiEXlm1bIRO2oFkauljhnUj4DVGj9v
+0jdjaiyr7Xx+3inHTskWNarYhenabYLd/eiLnhx7BuKsEuAG6da/AQJ/q0TXVbjV
+X5VkOQKBgQC5m6Rw2gsDXliMSmlZNbTpLRqyJPZ5agDjSUzBo7q2RkD7XOawgDGl
+LmL4bq52AdgstsAHooOerkrsmfVxgzE1ep2SfP8Pl/3yPcwEUw/AUJJxusxWKiEx
+Iz8+iP0AJqiUuDuTOkTZ7V7YVCbdmTRP+Vp3R4uVZEM1I+0+oyDvYg==
+-----END FOO PRIVATE KEY-----
diff --git a/integration/testdata/es256-enc.pem b/integration/testdata/es256-enc.pem
new file mode 100644
index 00000000..2b404753
--- /dev/null
+++ b/integration/testdata/es256-enc.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,a4865e47e33d1aedfc31de42301cedbd
+
+PI3whVCk8RTo8OW8GhlyoFsI1++0izc1QyE4qOTDK9nCjnu7xy1p5JgixVVrqFB0
+FGkKWQK46UKwJTjHV3zczblwpV2VAIb6GE9V24tkfZc9SCFFNO8kOakajU61vidk
+twu0O952g31IKzSYAebLLO5dGdF6fODilyuOv0FEVkY=
+-----END EC PRIVATE KEY-----
diff --git a/integration/testdata/es256-enc.pub b/integration/testdata/es256-enc.pub
new file mode 100644
index 00000000..19c9bd20
--- /dev/null
+++ b/integration/testdata/es256-enc.pub
@@ -0,0 +1,4 @@
+-----BEGIN ECDSA Public Key-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElxUERVm5N//HdLhWXUbimi91qhQ6
+O+CHTcGmd2KkoJonBKIhd+xeKN4/Rl345kZkuMzwCApqAXBjjrTARh48Bg==
+-----END ECDSA Public Key-----
diff --git a/integration/testdata/es256.pem b/integration/testdata/es256.pem
new file mode 100644
index 00000000..44677d11
--- /dev/null
+++ b/integration/testdata/es256.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIP9i6mrfUyym7wT529j7lPTbJXxqm5eTIKT8QIL2pdhGoAoGCCqGSM49
+AwEHoUQDQgAEePFeGCwt5mdsQeYQv7ZYO8hpB//WzYwuO2hu5cr5sNHOk+FWX2fh
+wZrBnOW8uMuHj5paHMgmPXtD5zL94j+qqw==
+-----END EC PRIVATE KEY-----
diff --git a/integration/testdata/es256.pub b/integration/testdata/es256.pub
new file mode 100644
index 00000000..48e8c3c3
--- /dev/null
+++ b/integration/testdata/es256.pub
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEePFeGCwt5mdsQeYQv7ZYO8hpB//W
+zYwuO2hu5cr5sNHOk+FWX2fhwZrBnOW8uMuHj5paHMgmPXtD5zL94j+qqw==
+-----END PUBLIC KEY-----
diff --git a/integration/testdata/jwk-no-alg.json b/integration/testdata/jwk-no-alg.json
new file mode 100644
index 00000000..9ff6d1ae
--- /dev/null
+++ b/integration/testdata/jwk-no-alg.json
@@ -0,0 +1,8 @@
+{
+ "use": "sig",
+ "kty": "EC",
+ "crv": "P-256",
+ "x": "X6YL64nzq2AyDj46j9MAGkjDJSD_gqgitMhXNG-VQpw",
+ "y": "xemItlL_CTNelUGQ1WZIIvNKWP2Dv3a9nDudBIlniQ0",
+ "d": "2WzbC-0-Rq6IwMYbYOvCqd74zrHMxk1HbTsh634Vt-I"
+}
diff --git a/integration/testdata/jwk-no-alg.pub.json b/integration/testdata/jwk-no-alg.pub.json
new file mode 100644
index 00000000..ca1738dd
--- /dev/null
+++ b/integration/testdata/jwk-no-alg.pub.json
@@ -0,0 +1,7 @@
+{
+ "use": "sig",
+ "kty": "EC",
+ "crv": "P-256",
+ "x": "X6YL64nzq2AyDj46j9MAGkjDJSD_gqgitMhXNG-VQpw",
+ "y": "xemItlL_CTNelUGQ1WZIIvNKWP2Dv3a9nDudBIlniQ0"
+}
diff --git a/integration/testdata/jwk-no-use.json b/integration/testdata/jwk-no-use.json
new file mode 100644
index 00000000..cd9c37bb
--- /dev/null
+++ b/integration/testdata/jwk-no-use.json
@@ -0,0 +1,8 @@
+{
+ "kty": "EC",
+ "crv": "P-256",
+ "alg": "ES256",
+ "x": "yqDzNseI3CwBh3R456SqjVAjkiyoNgxMp7rcEewmvHc",
+ "y": "s6Br4pT_lQimrZAPMe2STgpv0wKnf6-wlAqXUui5OPs",
+ "d": "IBcTKwlgszkE0Rr_q27-D0bS2ONToUtDUVIJ_Tulwig"
+}
diff --git a/integration/testdata/jwk-no-use.pub.json b/integration/testdata/jwk-no-use.pub.json
new file mode 100644
index 00000000..2190987b
--- /dev/null
+++ b/integration/testdata/jwk-no-use.pub.json
@@ -0,0 +1,7 @@
+{
+ "kty": "EC",
+ "crv": "P-256",
+ "alg": "ES256",
+ "x": "yqDzNseI3CwBh3R456SqjVAjkiyoNgxMp7rcEewmvHc",
+ "y": "s6Br4pT_lQimrZAPMe2STgpv0wKnf6-wlAqXUui5OPs"
+}
diff --git a/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.json b/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.json
new file mode 100644
index 00000000..3de119e7
--- /dev/null
+++ b/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.json
@@ -0,0 +1,7 @@
+{
+ "protected": "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIn0",
+ "encrypted_key": "apdmKYEs70rRLyhSFahbfpu5tZS8u3Lc",
+ "iv": "5RE1AoHlM6Z4-cxf",
+ "ciphertext": "6bch64RagQmGbNAk9yrig04QcYbTLL-QTmR4U8mi2XjWIOKJ3bS1YjHgD68ef1Iljha_cCtnMkm2JKf1n5Q4wVE7gJpX3WCDJtlI8iKrpze2XFFBt8964Rtk6d1Weo6fyZkzD7ZFf3LfAbGy2gGXeaqfc7lZNc2WYEmwUwyHE8vP8qXWfXtwIA--6J45fMt2iLeMVydAh6LS4oqQLWX5e7dRhTu-x1EHW-iVXJOj0QJsetlUWSwsN3xozJyOEPM2CK0jJoEJH88gMQ",
+ "tag": "nTqC8HHoZrGtvcFSxNcakA"
+}
\ No newline at end of file
diff --git a/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.pub.json b/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.pub.json
new file mode 100644
index 00000000..30b44243
--- /dev/null
+++ b/integration/testdata/jwk-pGoLJDgF5fgTNnB47SKMnVUzVNdu6MF0.pub.json
@@ -0,0 +1,8 @@
+{
+ "use": "sig",
+ "kty": "EC",
+ "crv": "P-256",
+ "alg": "ES256",
+ "x": "588xYzTh9Qp1f06BOBWVohArMyauc4wTzQRx_Z7WinI",
+ "y": "_A_PTsoN2Y5Z3FOxXuzJNoSheGkQIie9sr_9Vl02kOo"
+}
\ No newline at end of file
diff --git a/integration/testdata/jwks-key1.json b/integration/testdata/jwks-key1.json
new file mode 100644
index 00000000..9a90d7e8
--- /dev/null
+++ b/integration/testdata/jwks-key1.json
@@ -0,0 +1,10 @@
+{
+ "use": "sig",
+ "kty": "EC",
+ "kid": "1",
+ "crv": "P-256",
+ "alg": "ES256",
+ "x": "CYxl_HfIf80PTI7z3WR9JlZy_Z2isFmXHraXhgHAos0",
+ "y": "xsGV1k-WVTI2LeF2tU88aIkQiR4zAVuRkas5lb5xPTw",
+ "d": "Ko2frTowTbG-VUowa3wqqOAfhJtvByZ08EyYXerGkLc"
+}
diff --git a/integration/testdata/jwks.json b/integration/testdata/jwks.json
new file mode 100644
index 00000000..7bffe68c
--- /dev/null
+++ b/integration/testdata/jwks.json
@@ -0,0 +1,36 @@
+{
+ "keys": [
+ {
+ "use": "sig",
+ "kty": "EC",
+ "kid": "1",
+ "crv": "P-256",
+ "alg": "ES256",
+ "x": "CYxl_HfIf80PTI7z3WR9JlZy_Z2isFmXHraXhgHAos0",
+ "y": "xsGV1k-WVTI2LeF2tU88aIkQiR4zAVuRkas5lb5xPTw",
+ "d": "Ko2frTowTbG-VUowa3wqqOAfhJtvByZ08EyYXerGkLc"
+ },
+ {
+ "use": "sig",
+ "kty": "RSA",
+ "kid": "2",
+ "alg": "RS256",
+ "n": "zqY_VnA7ba9rp5E-jpjBRwAr8i0XKDyCnfAhI9fi66hV1pir4gZF6j11fwGSMi-Bqp2Ky_9_w23-dO4BOSPlL5JJ7AjGWmEpI_YzTGIA_GkT7s6xXLe016T6a5K4Nw8jh1xO073kxPkRpNBiQIIGHM-oFpnJZOZu0MzHOMzG2rjdRm1qTc-d6WpsEs2vDixAaGLkq5Arz-AAL2TTKY3-SZDYrv862HiFIDZRDrd3IFdAas_S-sPQDi2F3LZfMgblF8RkYLtQstJkaMUoAv5JgTg4pq3jpOuvb0axHloNdCKYO0wmxcJLyqsE9BryX4QF4edjev-swQQ_UrWALsp3fw",
+ "e": "AQAB",
+ "d": "xqS8gcTE-70CyUMvVTe1oyChHd0GQ6FXFac81A20jj90tCJyJ4VMM8z8YygJdsB_7pgeUda65AuZ7KDVAC5nIwGGmaQdt8dqGq2Rxjz3IauIg6igibX12s02A6438oVU68tp4tTJUibyNPIzYDsc0Rk0RPVzyNBRedk_MHR4Osp7PkDQ6ERCB-WFSTYZoTyFcx9YaNZikIsoOF2mfJjV86haUevsrX1npW17LqWNLG7RDo2fO3c_I6L1121FmK5xXsntYYRDBzmzWgCQo1h6Vp8lqSwcYsvz07Ms664ZvzZg1FrJyihOIMGVhwCc1pzLBWYBqTP4GEeOjoLWnTLmkQ",
+ "p": "3dYQ4eqJiLW9_ifnMU14bHQGonSZb80F89joncv1f59UyM25NBBaB7ceiNwbUarPFXsYwdycJ6VggGllXPcaamkJ65rPGeDDxyZQ66gAKnu3dkOnh3N906EsjNzHxlGUH3gt58HV4lq5Y98tZTUO0X2U1jO24goFWqFqXY-KsT0",
+ "q": "7nlweOi8DoEer3ohFdDuk7XLLEt3Mi-DF3tyyyyPBCURilcTsSnhoOQDSm-s0OdBplOlGTEdI4RqtUo2Y0kv2IGsgBcEjZKjIyYjmlCZ_Jkg1yijwQxoffo8sjYRNA4VYCqJ8qG0k5jbiclkjRiJ7Cf7TTbxJHRvWs40XcOMH2s"
+ },
+ {
+ "use": "enc",
+ "kty": "RSA",
+ "kid": "3",
+ "alg": "RS256",
+ "n": "szcBbFtJHcz6qve6zi9vBB08kP2s-CjI2nls05rbij7oyN5G7St7XayQFAkyr78u25IKB3oav43-wjHraIoD8IYQyoKztEfabPu8vPyHVUsjBULRPWU1aeMKOctFsD-nOsQTqMGXdXWF1LDC3gcy8VJ6-eH5oJGgDiY8BLyRi2meNjt5sJyC_r5KkRM_rE2EG8JpPVafDTC84nTt_AqpVt7qKlnkskICX7rL2QBc91nDrQM2RoLyLIoC0cZCoJXXm8KGhmqa-vcf0oPVWi6ypMBIiSW33gm5NfaKkZOFmLZb6WTfahvFgAvOGAM2860qStaEUuyvVsNqruXCVmhu6Q",
+ "e": "AQAB",
+ "d": "T2lgSBNrIrlhmcCMFjEOkFQkMls1-gCYf7auclV8UpXtsJRN-Wn7EFcWwMoSm62rpb_gkc-ZaqgQ6xwTpA8ED-BYMGQaHRh5wTELQPLlRPY2Xm5tKTdfo7vnHBTmnGKYR4H69BxUcMfStZxdvOSTvjs-ItvvMSdWNO7cSX0FQTCd8fUSZzHbcX0r9Moyu4SjHK78VruBaBCfpeiVjOHvc6R2jmc4oXizZDBvHCCZNks3jgZvSnOUx-CapHV9BMJY6Pd64zOcyyEH3jbma4Y9bh9p_WEZVbtex9Xgo9wR51J8TlgnnY1HmnzsT3Loh_Q0ey9IJzpxFyMqCEp2x0_vAQ",
+ "p": "z_AMFhSk0EfNjci_OkBt-qrvQCWA6QKnY0pxEnm8L6N2nNr3TFwwddeRcYtROR6bfnR-VrI6KbzBMSpLlRVXXY7TQU7T7cADU-alh7D4g9F7jWfTpPBNoKu_kuL5RuqkgPEtGYbCqWTa1DOExjLSdYad6kP1mjSrn5Tn4sPqbF0",
+ "q": "3KNj9gx8QkVSXUbK_5aLoEdWrXxVATAJfll1cLA4yVfVYeCu1uJl_j44IjJ29NE6jTbYHVmVYM6pV-Lb2J0pn1QE0QqbnQVfq8TzR0rhedLfh2Ts4lkrw-Iv1yfKbpshesSySkffDw-6A4QGVtiUNtEqC2cYuCtdUbNFHDTrQ_0"
+ }
+ ]
+}
diff --git a/integration/testdata/jwks.pub.json b/integration/testdata/jwks.pub.json
new file mode 100644
index 00000000..70392da2
--- /dev/null
+++ b/integration/testdata/jwks.pub.json
@@ -0,0 +1,29 @@
+{
+ "keys": [
+ {
+ "use": "sig",
+ "kty": "RSA",
+ "kid": "2",
+ "alg": "RS256",
+ "n": "zqY_VnA7ba9rp5E-jpjBRwAr8i0XKDyCnfAhI9fi66hV1pir4gZF6j11fwGSMi-Bqp2Ky_9_w23-dO4BOSPlL5JJ7AjGWmEpI_YzTGIA_GkT7s6xXLe016T6a5K4Nw8jh1xO073kxPkRpNBiQIIGHM-oFpnJZOZu0MzHOMzG2rjdRm1qTc-d6WpsEs2vDixAaGLkq5Arz-AAL2TTKY3-SZDYrv862HiFIDZRDrd3IFdAas_S-sPQDi2F3LZfMgblF8RkYLtQstJkaMUoAv5JgTg4pq3jpOuvb0axHloNdCKYO0wmxcJLyqsE9BryX4QF4edjev-swQQ_UrWALsp3fw",
+ "e": "AQAB"
+ },
+ {
+ "use": "sig",
+ "kty": "EC",
+ "kid": "1",
+ "crv": "P-256",
+ "alg": "ES256",
+ "x": "CYxl_HfIf80PTI7z3WR9JlZy_Z2isFmXHraXhgHAos0",
+ "y": "xsGV1k-WVTI2LeF2tU88aIkQiR4zAVuRkas5lb5xPTw"
+ },
+ {
+ "use": "enc",
+ "kty": "RSA",
+ "kid": "3",
+ "alg": "RS256",
+ "n": "szcBbFtJHcz6qve6zi9vBB08kP2s-CjI2nls05rbij7oyN5G7St7XayQFAkyr78u25IKB3oav43-wjHraIoD8IYQyoKztEfabPu8vPyHVUsjBULRPWU1aeMKOctFsD-nOsQTqMGXdXWF1LDC3gcy8VJ6-eH5oJGgDiY8BLyRi2meNjt5sJyC_r5KkRM_rE2EG8JpPVafDTC84nTt_AqpVt7qKlnkskICX7rL2QBc91nDrQM2RoLyLIoC0cZCoJXXm8KGhmqa-vcf0oPVWi6ypMBIiSW33gm5NfaKkZOFmLZb6WTfahvFgAvOGAM2860qStaEUuyvVsNqruXCVmhu6Q",
+ "e": "AQAB"
+ }
+ ]
+}
diff --git a/integration/testdata/jwt-json-serialization-flattened.json b/integration/testdata/jwt-json-serialization-flattened.json
new file mode 100644
index 00000000..62b2b923
--- /dev/null
+++ b/integration/testdata/jwt-json-serialization-flattened.json
@@ -0,0 +1,6 @@
+{
+ "payload": "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiJ9",
+ "protected": "eyJleHAiOiA0Njg2MzA2MTI5LCAiYXVkIjogImZvbyIsICJpc3MiOiAiYmFyIiwgInN1YiI6ICJiYXoifQ",
+ "header": {"kid": "rsa2048.pem"},
+ "signature": "IUYTIU6gUDCFc5Ou7MDogK1LWP4qbo9esx6rCpBlDzQFV1TN83J1g7L9EwRnYZ8zI1FyHIHhLMwNgyQgvYOONUJQVs0ia7aXV_Z6Uno4cQPVoB5FDWmfY4GTi5GsADS2kyT0BRVCFp1QAT0uVAC03Iqh0NtUZs51C_Bj8tGY2NFvUuRZ3sHuVUhC7tn2Aw86dm2z7YLRH6lir2tjFnNwEimqb_i5BBjkxNUIBpRBNWf7FjH80q5RKk2sGnzFDqR2XqGNxkpHZ5iEjq5MOl97tLEfwcqU4FkNbEn5qG31-Xq39BBg5uaX-YeV4P7duCiA0DY-Yxf6aRNOkCmprrmPBA"
+}
diff --git a/integration/testdata/jwt-json-serialization-multi.json b/integration/testdata/jwt-json-serialization-multi.json
new file mode 100644
index 00000000..eecb60ca
--- /dev/null
+++ b/integration/testdata/jwt-json-serialization-multi.json
@@ -0,0 +1,15 @@
+{
+ "payload": "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiJ9",
+ "signatures": [
+ {
+ "protected": "eyJleHAiOiA0Njg2MzA2MTI5LCAiYXVkIjogImZvbyIsICJpc3MiOiAiYmFyIiwgInN1YiI6ICJiYXoifQ",
+ "header": {"kid": "rsa2048.pem"},
+ "signature": "IUYTIU6gUDCFc5Ou7MDogK1LWP4qbo9esx6rCpBlDzQFV1TN83J1g7L9EwRnYZ8zI1FyHIHhLMwNgyQgvYOONUJQVs0ia7aXV_Z6Uno4cQPVoB5FDWmfY4GTi5GsADS2kyT0BRVCFp1QAT0uVAC03Iqh0NtUZs51C_Bj8tGY2NFvUuRZ3sHuVUhC7tn2Aw86dm2z7YLRH6lir2tjFnNwEimqb_i5BBjkxNUIBpRBNWf7FjH80q5RKk2sGnzFDqR2XqGNxkpHZ5iEjq5MOl97tLEfwcqU4FkNbEn5qG31-Xq39BBg5uaX-YeV4P7duCiA0DY-Yxf6aRNOkCmprrmPBA"
+ },
+ {
+ "protected": "eyJleHAiOiA0Njg2MzA2MTI5LCAiYXVkIjogImZvbyIsICJpc3MiOiAiYmFyIiwgInN1YiI6ICJiYXoifQ",
+ "header": {"kid": "es256.pem"},
+ "signature": "YVyl6DjiAdh7yBmcN2W0GpbR9Ti2S8WDE-JRDSlrKGl0ibxjJUpbm8Qs5G1PNVws8MKm1Tnk7GIXsZd1ZVLrnA"
+ }
+ ]
+}
diff --git a/integration/testdata/jwt-json-serialization.json b/integration/testdata/jwt-json-serialization.json
new file mode 100644
index 00000000..c93de939
--- /dev/null
+++ b/integration/testdata/jwt-json-serialization.json
@@ -0,0 +1,10 @@
+{
+ "payload": "eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJSUzI1NiJ9",
+ "signatures": [
+ {
+ "protected": "eyJleHAiOiA0Njg2MzA2MTI5LCAiYXVkIjogImZvbyIsICJpc3MiOiAiYmFyIiwgInN1YiI6ICJiYXoifQ",
+ "header": {"kid": "rsa2048.pem"},
+ "signature": "IUYTIU6gUDCFc5Ou7MDogK1LWP4qbo9esx6rCpBlDzQFV1TN83J1g7L9EwRnYZ8zI1FyHIHhLMwNgyQgvYOONUJQVs0ia7aXV_Z6Uno4cQPVoB5FDWmfY4GTi5GsADS2kyT0BRVCFp1QAT0uVAC03Iqh0NtUZs51C_Bj8tGY2NFvUuRZ3sHuVUhC7tn2Aw86dm2z7YLRH6lir2tjFnNwEimqb_i5BBjkxNUIBpRBNWf7FjH80q5RKk2sGnzFDqR2XqGNxkpHZ5iEjq5MOl97tLEfwcqU4FkNbEn5qG31-Xq39BBg5uaX-YeV4P7duCiA0DY-Yxf6aRNOkCmprrmPBA"
+ }
+ ]
+}
diff --git a/integration/testdata/p256.pem b/integration/testdata/p256.pem
new file mode 100644
index 00000000..613ce024
--- /dev/null
+++ b/integration/testdata/p256.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEICyzqgCTvJmpbaAbZuJCHMK4Sgbiz12qUG4owl6/EIJnoAoGCCqGSM49
+AwEHoUQDQgAE5Eo8nuVDXtH1uErxhlOBQIllDfLWY8vgnxpDKsEm4DR1poxKgJAd
+liu4JZ8+KIaVOuLggm1dkrtVCAMMRqc5mA==
+-----END EC PRIVATE KEY-----
diff --git a/integration/testdata/p256.pem.pub b/integration/testdata/p256.pem.pub
new file mode 100644
index 00000000..2182e729
--- /dev/null
+++ b/integration/testdata/p256.pem.pub
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5Eo8nuVDXtH1uErxhlOBQIllDfLW
+Y8vgnxpDKsEm4DR1poxKgJAdliu4JZ8+KIaVOuLggm1dkrtVCAMMRqc5mA==
+-----END PUBLIC KEY-----
\ No newline at end of file
diff --git a/integration/testdata/rsa2048.pem b/integration/testdata/rsa2048.pem
new file mode 100644
index 00000000..7ef6bacf
--- /dev/null
+++ b/integration/testdata/rsa2048.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAn4V0GxrIhjoheOpXLbJNg8GdjxCRhOGbY6HZRRWCzKaEq0+7
+iLh9z3v5ixWH6rnftTBsZeFKiikqb0tRRZymuOtUz42ZuzAoDiAwFQg/sYgNdkQQ
+zJHYNlbd7FF2YQyJ0JAw+cmvqbLzO0E+mgZImTNzCqzBBhLoUWjQqr1al7lXUAGw
+UvSrI7JdIMOfWEvQ2wgPqeoLL3AE97zEoiYyIaTrInQlNcA5Cs1mRa396ET6hw8b
+FfPZAXsW3m8JeGuqxJ23wL4jCee/w9v+qNL6oKwtpuCBOOnS5MEYH5GhvlBQ7baN
+zoTAT7uk9yc1qVd7aLr2RJ5yW2v2v5LazmUzeQIDAQABAoIBACPe7ZHevvK4Bajc
+AUiMTLPxCM4P6rkXxkpsLaBESwpb839WSZRf8CKE/UNSTyLwMybaQbXTKGDTCvDF
+3fuqUy9H8+VMMSKPnKI4iLdiCHiSYHyUp7ZooVbux66JTvZZzG+yzOCOgsrFK77K
+WBpoiVCx6g+fczQ7cjREPo/2TnXJY6ZQEntNeDnWN4CgJWDYTruPYTgTj84WUwPi
+4aTDzPwZcps45ZRBLlBmd2tIxy87jBSKzGvwKK9c7++mtiwr956d7HsosaqSSJCa
+jDO9JRlzcqAq8r+GZF1mmyoI3P6jgYWCElq+s1zZ0RZBaVfdhEl+AkR5pI3+oGdk
+h8AdzZECgYEA07YASLNHYm99H21e1X8Yh4zZK96MfTESvAHkcIXEU7y0zw9tczPL
+KTB6eXr9mY0JjSgVw94nx8YLYPU/dr0xqBZGD57FvqYRWdwdswk0CXSyEgWRccnB
+jfV391gauBi2gQLxzRZf7eNl2c15kabpV0KUi/Pfu645PB2bY1yD0/UCgYEAwOR7
+GAog8y4TsPYxNpNAdgnlFU/XpE3sJPIy6Q7cGN4cuWBsdaPToTkAQiQYa5UnC5ql
+dKHNwkVTwUvTa/pnRCShkdgNd2ycYDErbMe+DolEt1Ffr5+jVD4MILdGJq5CqGok
+PrLhH9ZU0mfFlc/4Hkt1VkcoWPt8uT4eL0GPsvUCgYEAhriCZcDv5AveK2mFt4Yx
+LdDLQcdUzzWzHkB2BcSZsk+bH0hJ9c03svZOeY9yYYwGT/T6JLHxzoaQJxrpT74F
+I1lJLBd07mTvFaeknpF0s6+2wREaBLbGnHdf594A4rWXLXGaPU/Hq7HQ1lCS08TL
+J+QOcyC1dtDfSwnsH8Z3fSECgYAS8t78v5H5EZ+xlJ3FBLYiYlp0u4EtjNIT1w8V
+QfZxIvCjbUt6SvuxLM5PsQgNGXvacfiq+nIiEXlm1bIRO2oFkauljhnUj4DVGj9v
+0jdjaiyr7Xx+3inHTskWNarYhenabYLd/eiLnhx7BuKsEuAG6da/AQJ/q0TXVbjV
+X5VkOQKBgQC5m6Rw2gsDXliMSmlZNbTpLRqyJPZ5agDjSUzBo7q2RkD7XOawgDGl
+LmL4bq52AdgstsAHooOerkrsmfVxgzE1ep2SfP8Pl/3yPcwEUw/AUJJxusxWKiEx
+Iz8+iP0AJqiUuDuTOkTZ7V7YVCbdmTRP+Vp3R4uVZEM1I+0+oyDvYg==
+-----END RSA PRIVATE KEY-----
diff --git a/integration/testdata/rsa2048.pub b/integration/testdata/rsa2048.pub
new file mode 100644
index 00000000..e60e592e
--- /dev/null
+++ b/integration/testdata/rsa2048.pub
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn4V0GxrIhjoheOpXLbJN
+g8GdjxCRhOGbY6HZRRWCzKaEq0+7iLh9z3v5ixWH6rnftTBsZeFKiikqb0tRRZym
+uOtUz42ZuzAoDiAwFQg/sYgNdkQQzJHYNlbd7FF2YQyJ0JAw+cmvqbLzO0E+mgZI
+mTNzCqzBBhLoUWjQqr1al7lXUAGwUvSrI7JdIMOfWEvQ2wgPqeoLL3AE97zEoiYy
+IaTrInQlNcA5Cs1mRa396ET6hw8bFfPZAXsW3m8JeGuqxJ23wL4jCee/w9v+qNL6
+oKwtpuCBOOnS5MEYH5GhvlBQ7baNzoTAT7uk9yc1qVd7aLr2RJ5yW2v2v5LazmUz
+eQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/integration/testdata/rsa_priv.pem b/integration/testdata/rsa_priv.pem
new file mode 100644
index 00000000..5c333d14
--- /dev/null
+++ b/integration/testdata/rsa_priv.pem
@@ -0,0 +1,30 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,76589C25A98A26E05C00F9EA8B258C36
+
+jb53BmdoS97e3MXLDkwSR7foObkKjpU+LrODHVl0Z2xFdb2Gtqg5OBZgwsdDbKO8
+ouCXxQk76cgZ/331xAtczjj77aBhRjmaBMupSZrb/CQHtfbJ2LxjdfyHiGDNdklG
+NLsbG+nwPITB1mf2YTd7Fo8o4qm7SCVvoVNRGbmzVuAnZw58ziPoq/FsUWXbsV4+
+EuDb3YfhBjW88wHEkbFrtpKtbF23c5YSIOL1jAanZBdJ6ML8b+DT0DOQQVEWLtZP
+c3t7Ft3KRH5MKUpkS/Ge085PuL16S//1r/BbdPTipO6dH+WffMynN3y+dAfjCAiY
+mY+/UMegi1/mFIt/tnA4iOb4kU5D7cpSrPkx4Sln3tTPgilw77Y12cN4VI95gOLX
+i++Ud3usZrXp6GceU91XGadY+o6ZaE/61GBWxZZRH6VpAPHezYpAHc3IKz02O/8E
+rZ8JpajxLmthndSWbvrC7NcErWfhC+wM4sDNgzYos8j4CUwrWzko9CyToSwz1mw6
+xSidqEY3c9B56EziFYJYytzY/tqcLPxQLxQDijfeMjVZ9lNTQEkf02DYcRzR9yQv
+gkrzM+tNpetTGUocct0D9u5oNYr/N+DANsFp+udHEh/OWwgtBeeMAJ1ET0kgWj+0
+Bho5+YurvK+E7yU2uilKh5CuRYQHQTGrdH5lrBJEOF9JDSAqvQ6Uah9Pdd4rPaHG
+V6SrJHg9rymuiI7TND7qMxtAlh8oseNHhG2R3mJVcmNEh3Ipoj7uAVW/T7X/qbcC
+Db214mW9umaQnp0PnUE+uRPjhYH+7YOwv2LfOmBHkPJ2Zx79hoY0VnCPdSkz4wfH
+FDRW0CO1piEU+DDvceGKF7zTPD6AhdY/y12s8Bpr/v9ZlDb/UWTqfAYH6SNJpl/y
+KCe4G1QDUTTLWsZMS12hxPzxfb7MR8EatAPD/92DJLLeYTr4JLc9v/LwMjfCNUyg
+2KXLl7dy95adiSpnPb98qZsuiane1nIN7AGbdXTY2ZJb9bdQcYahqw5dmANoK+fL
+PBEnC9iXZLKL7N5aA1EyFj7V3yXPJv6iiz/zxvKbTr650gDGA9g95UPSqVqAYAIj
+yAmhqdf9lfCBzuFeoVutYnD0IE6ifWE7Zuc/V/1OV+0d7eOux5BX2Fn8zyzVa+Uq
+KF+0eAPKe+zvLzLMYGq/mMDqXtCTG4jk0F/xRRpWJdertKWCNx4iX4BJ9ABhbRib
+DkM4+A0/8HMoqB1njE4wCLhLPb9Oskn2jmnth5EzFPbbXMH6IGKz+eAZxAeuulRG
+QKkkELbMR1OsXiYjQfcDNRpsudvZIoD2ky2pNUxcsQN8lxIkcTOjDqLha39dvF3z
+jHlLX7j6lMtkj7QJBZJYMTbTvL/n7axvASfhrwOR95bfxhss/lGDDL11uWqtuRfw
+WEDTlrmkjwR0eWz/oZIyHPomH9OOspglx9mYndHqkHVy5/enYrPEi3N/CevcA0nL
+ycGZouJvSHYg81nHpl03OxlQYaGW3cUHRq3JBQ9cPyebEz/85t+bmRj1K9IpJPPN
+K1vWUhH8OIdNsE/4XAVpr82atguLVW+frGf3q5+5wDwWGwv2nh+wGxWhzXlN2qva
+-----END RSA PRIVATE KEY-----
\ No newline at end of file
diff --git a/integration/testdata/rsa_pub.pem b/integration/testdata/rsa_pub.pem
new file mode 100644
index 00000000..a45cd32b
--- /dev/null
+++ b/integration/testdata/rsa_pub.pem
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyEU5ZhIhFn7v4bpMKlkz
+dmLCj9KfmqFWig29c6OzYoMUnbdodOmZ6RId/Gw5fnluH12eFxsItlXKDT4RPSm7
+m4D1sYgFmk88oo6z4XDuItDncoIg89jGK38OZ8A0gwEoy5JqukONGmAldzgzQyiq
+uzSNMeT1WO9zXCwOljcUio697M1kP/YN1Lp7n7YILVwdV8wQ2vyNKQK1M/5OZOFl
+lOqww4wsqLTDK0rfxp6LAVtczp1XdxbsnpdixrK38O+dHWe4IS5HhKLmmmdTfpFQ
+D3PIAXs/Naap/0+t0lsOplNPiF4BYyNBIqyyfm1o5ZQpGfmITKvDFZMBkQ2i2cou
+nQIDAQAB
+-----END PUBLIC KEY-----
\ No newline at end of file
diff --git a/integration/testdata/totp.secret b/integration/testdata/totp.secret
new file mode 100644
index 00000000..82089873
--- /dev/null
+++ b/integration/testdata/totp.secret
@@ -0,0 +1 @@
+UPCTJYT7MUR4RWOUJ3TGTUB43IYCBJ76
diff --git a/integration/testdata/totp.url b/integration/testdata/totp.url
new file mode 100644
index 00000000..2b95e8fd
--- /dev/null
+++ b/integration/testdata/totp.url
@@ -0,0 +1 @@
+otpauth://totp/example.com:foo@example.com?algorithm=SHA1&digits=6&issuer=example.com&period=30&secret=EW32D2CFTAIRTEAWTRQZZXAITVA4U6K4
diff --git a/integration/testdata/twopems.pem b/integration/testdata/twopems.pem
new file mode 100644
index 00000000..55218e7c
--- /dev/null
+++ b/integration/testdata/twopems.pem
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAn4V0GxrIhjoheOpXLbJNg8GdjxCRhOGbY6HZRRWCzKaEq0+7
+iLh9z3v5ixWH6rnftTBsZeFKiikqb0tRRZymuOtUz42ZuzAoDiAwFQg/sYgNdkQQ
+zJHYNlbd7FF2YQyJ0JAw+cmvqbLzO0E+mgZImTNzCqzBBhLoUWjQqr1al7lXUAGw
+UvSrI7JdIMOfWEvQ2wgPqeoLL3AE97zEoiYyIaTrInQlNcA5Cs1mRa396ET6hw8b
+FfPZAXsW3m8JeGuqxJ23wL4jCee/w9v+qNL6oKwtpuCBOOnS5MEYH5GhvlBQ7baN
+zoTAT7uk9yc1qVd7aLr2RJ5yW2v2v5LazmUzeQIDAQABAoIBACPe7ZHevvK4Bajc
+AUiMTLPxCM4P6rkXxkpsLaBESwpb839WSZRf8CKE/UNSTyLwMybaQbXTKGDTCvDF
+3fuqUy9H8+VMMSKPnKI4iLdiCHiSYHyUp7ZooVbux66JTvZZzG+yzOCOgsrFK77K
+WBpoiVCx6g+fczQ7cjREPo/2TnXJY6ZQEntNeDnWN4CgJWDYTruPYTgTj84WUwPi
+4aTDzPwZcps45ZRBLlBmd2tIxy87jBSKzGvwKK9c7++mtiwr956d7HsosaqSSJCa
+jDO9JRlzcqAq8r+GZF1mmyoI3P6jgYWCElq+s1zZ0RZBaVfdhEl+AkR5pI3+oGdk
+h8AdzZECgYEA07YASLNHYm99H21e1X8Yh4zZK96MfTESvAHkcIXEU7y0zw9tczPL
+KTB6eXr9mY0JjSgVw94nx8YLYPU/dr0xqBZGD57FvqYRWdwdswk0CXSyEgWRccnB
+jfV391gauBi2gQLxzRZf7eNl2c15kabpV0KUi/Pfu645PB2bY1yD0/UCgYEAwOR7
+GAog8y4TsPYxNpNAdgnlFU/XpE3sJPIy6Q7cGN4cuWBsdaPToTkAQiQYa5UnC5ql
+dKHNwkVTwUvTa/pnRCShkdgNd2ycYDErbMe+DolEt1Ffr5+jVD4MILdGJq5CqGok
+PrLhH9ZU0mfFlc/4Hkt1VkcoWPt8uT4eL0GPsvUCgYEAhriCZcDv5AveK2mFt4Yx
+LdDLQcdUzzWzHkB2BcSZsk+bH0hJ9c03svZOeY9yYYwGT/T6JLHxzoaQJxrpT74F
+I1lJLBd07mTvFaeknpF0s6+2wREaBLbGnHdf594A4rWXLXGaPU/Hq7HQ1lCS08TL
+J+QOcyC1dtDfSwnsH8Z3fSECgYAS8t78v5H5EZ+xlJ3FBLYiYlp0u4EtjNIT1w8V
+QfZxIvCjbUt6SvuxLM5PsQgNGXvacfiq+nIiEXlm1bIRO2oFkauljhnUj4DVGj9v
+0jdjaiyr7Xx+3inHTskWNarYhenabYLd/eiLnhx7BuKsEuAG6da/AQJ/q0TXVbjV
+X5VkOQKBgQC5m6Rw2gsDXliMSmlZNbTpLRqyJPZ5agDjSUzBo7q2RkD7XOawgDGl
+LmL4bq52AdgstsAHooOerkrsmfVxgzE1ep2SfP8Pl/3yPcwEUw/AUJJxusxWKiEx
+Iz8+iP0AJqiUuDuTOkTZ7V7YVCbdmTRP+Vp3R4uVZEM1I+0+oyDvYg==
+-----END RSA PRIVATE KEY-----
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAn4V0GxrIhjoheOpXLbJNg8GdjxCRhOGbY6HZRRWCzKaEq0+7
+iLh9z3v5ixWH6rnftTBsZeFKiikqb0tRRZymuOtUz42ZuzAoDiAwFQg/sYgNdkQQ
+zJHYNlbd7FF2YQyJ0JAw+cmvqbLzO0E+mgZImTNzCqzBBhLoUWjQqr1al7lXUAGw
+UvSrI7JdIMOfWEvQ2wgPqeoLL3AE97zEoiYyIaTrInQlNcA5Cs1mRa396ET6hw8b
+FfPZAXsW3m8JeGuqxJ23wL4jCee/w9v+qNL6oKwtpuCBOOnS5MEYH5GhvlBQ7baN
+zoTAT7uk9yc1qVd7aLr2RJ5yW2v2v5LazmUzeQIDAQABAoIBACPe7ZHevvK4Bajc
+AUiMTLPxCM4P6rkXxkpsLaBESwpb839WSZRf8CKE/UNSTyLwMybaQbXTKGDTCvDF
+3fuqUy9H8+VMMSKPnKI4iLdiCHiSYHyUp7ZooVbux66JTvZZzG+yzOCOgsrFK77K
+WBpoiVCx6g+fczQ7cjREPo/2TnXJY6ZQEntNeDnWN4CgJWDYTruPYTgTj84WUwPi
+4aTDzPwZcps45ZRBLlBmd2tIxy87jBSKzGvwKK9c7++mtiwr956d7HsosaqSSJCa
+jDO9JRlzcqAq8r+GZF1mmyoI3P6jgYWCElq+s1zZ0RZBaVfdhEl+AkR5pI3+oGdk
+h8AdzZECgYEA07YASLNHYm99H21e1X8Yh4zZK96MfTESvAHkcIXEU7y0zw9tczPL
+KTB6eXr9mY0JjSgVw94nx8YLYPU/dr0xqBZGD57FvqYRWdwdswk0CXSyEgWRccnB
+jfV391gauBi2gQLxzRZf7eNl2c15kabpV0KUi/Pfu645PB2bY1yD0/UCgYEAwOR7
+GAog8y4TsPYxNpNAdgnlFU/XpE3sJPIy6Q7cGN4cuWBsdaPToTkAQiQYa5UnC5ql
+dKHNwkVTwUvTa/pnRCShkdgNd2ycYDErbMe+DolEt1Ffr5+jVD4MILdGJq5CqGok
+PrLhH9ZU0mfFlc/4Hkt1VkcoWPt8uT4eL0GPsvUCgYEAhriCZcDv5AveK2mFt4Yx
+LdDLQcdUzzWzHkB2BcSZsk+bH0hJ9c03svZOeY9yYYwGT/T6JLHxzoaQJxrpT74F
+I1lJLBd07mTvFaeknpF0s6+2wREaBLbGnHdf594A4rWXLXGaPU/Hq7HQ1lCS08TL
+J+QOcyC1dtDfSwnsH8Z3fSECgYAS8t78v5H5EZ+xlJ3FBLYiYlp0u4EtjNIT1w8V
+QfZxIvCjbUt6SvuxLM5PsQgNGXvacfiq+nIiEXlm1bIRO2oFkauljhnUj4DVGj9v
+0jdjaiyr7Xx+3inHTskWNarYhenabYLd/eiLnhx7BuKsEuAG6da/AQJ/q0TXVbjV
+X5VkOQKBgQC5m6Rw2gsDXliMSmlZNbTpLRqyJPZ5agDjSUzBo7q2RkD7XOawgDGl
+LmL4bq52AdgstsAHooOerkrsmfVxgzE1ep2SfP8Pl/3yPcwEUw/AUJJxusxWKiEx
+Iz8+iP0AJqiUuDuTOkTZ7V7YVCbdmTRP+Vp3R4uVZEM1I+0+oyDvYg==
+-----END RSA PRIVATE KEY-----
diff --git a/pkg/blackfriday/LICENSE.txt b/pkg/blackfriday/LICENSE.txt
new file mode 100644
index 00000000..2885af36
--- /dev/null
+++ b/pkg/blackfriday/LICENSE.txt
@@ -0,0 +1,29 @@
+Blackfriday is distributed under the Simplified BSD License:
+
+> Copyright © 2011 Russ Ross
+> All rights reserved.
+>
+> Redistribution and use in source and binary forms, with or without
+> modification, are permitted provided that the following conditions
+> are met:
+>
+> 1. Redistributions of source code must retain the above copyright
+> notice, this list of conditions and the following disclaimer.
+>
+> 2. Redistributions in binary form must reproduce the above
+> copyright notice, this list of conditions and the following
+> disclaimer in the documentation and/or other materials provided with
+> the distribution.
+>
+> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+> POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkg/blackfriday/README.md b/pkg/blackfriday/README.md
new file mode 100644
index 00000000..2e0db355
--- /dev/null
+++ b/pkg/blackfriday/README.md
@@ -0,0 +1,283 @@
+Blackfriday [](https://travis-ci.org/russross/blackfriday)
+===========
+
+Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It
+is paranoid about its input (so you can safely feed it user-supplied
+data), it is fast, it supports common extensions (tables, smart
+punctuation substitutions, etc.), and it is safe for all utf-8
+(unicode) input.
+
+HTML output is currently supported, along with Smartypants
+extensions.
+
+It started as a translation from C of [Sundown][3].
+
+
+Installation
+------------
+
+Blackfriday is compatible with any modern Go release. With Go 1.7 and git
+installed:
+
+ go get gopkg.in/russross/blackfriday.v2
+
+will download, compile, and install the package into your `$GOPATH`
+directory hierarchy. Alternatively, you can achieve the same if you
+import it into a project:
+
+ import "gopkg.in/russross/blackfriday.v2"
+
+and `go get` without parameters.
+
+
+Versions
+--------
+
+Currently maintained and recommended version of Blackfriday is `v2`. It's being
+developed on its own branch: https://github.com/russross/blackfriday/v2. You
+should install and import it via [gopkg.in][6] at
+`gopkg.in/russross/blackfriday.v2`.
+
+Version 2 offers a number of improvements over v1:
+
+* Cleaned up API
+* A separate call to [`Parse`][4], which produces an abstract syntax tree for
+ the document
+* Latest bug fixes
+* Flexibility to easily add your own rendering extensions
+
+Potential drawbacks:
+
+* Our benchmarks show v2 to be slightly slower than v1. Currently in the
+ ballpark of around 15%.
+* API breakage. If you can't afford modifying your code to adhere to the new API
+ and don't care too much about the new features, v2 is probably not for you.
+* Several bug fixes are trailing behind and still need to be forward-ported to
+ v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for
+ tracking.
+
+Usage
+-----
+
+For the most sensible markdown processing, it is as simple as getting your input
+into a byte slice and calling:
+
+```go
+output := blackfriday.Run(input)
+```
+
+Your input will be parsed and the output rendered with a set of most popular
+extensions enabled. If you want the most basic feature set, corresponding with
+the bare Markdown specification, use:
+
+```go
+output := blackfriday.Run(input, blackfriday.WithNoExtensions())
+```
+
+### Sanitize untrusted content
+
+Blackfriday itself does nothing to protect against malicious content. If you are
+dealing with user-supplied markdown, we recommend running Blackfriday's output
+through HTML sanitizer such as [Bluemonday][5].
+
+Here's an example of simple usage of Blackfriday together with Bluemonday:
+
+```go
+import (
+ "github.com/microcosm-cc/bluemonday"
+ "github.com/russross/blackfriday"
+)
+
+// ...
+unsafe := blackfriday.Run(input)
+html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
+```
+
+### Custom options
+
+If you want to customize the set of options, use `blackfriday.WithExtensions`,
+`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`.
+
+You can also check out `blackfriday-tool` for a more complete example
+of how to use it. Download and install it using:
+
+ go get github.com/russross/blackfriday-tool
+
+This is a simple command-line tool that allows you to process a
+markdown file using a standalone program. You can also browse the
+source directly on github if you are just looking for some example
+code:
+
+*
+
+Note that if you have not already done so, installing
+`blackfriday-tool` will be sufficient to download and install
+blackfriday in addition to the tool itself. The tool binary will be
+installed in `$GOPATH/bin`. This is a statically-linked binary that
+can be copied to wherever you need it without worrying about
+dependencies and library versions.
+
+
+Features
+--------
+
+All features of Sundown are supported, including:
+
+* **Compatibility**. The Markdown v1.0.3 test suite passes with
+ the `--tidy` option. Without `--tidy`, the differences are
+ mostly in whitespace and entity escaping, where blackfriday is
+ more consistent and cleaner.
+
+* **Common extensions**, including table support, fenced code
+ blocks, autolinks, strikethroughs, non-strict emphasis, etc.
+
+* **Safety**. Blackfriday is paranoid when parsing, making it safe
+ to feed untrusted user input without fear of bad things
+ happening. The test suite stress tests this and there are no
+ known inputs that make it crash. If you find one, please let me
+ know and send me the input that does it.
+
+ NOTE: "safety" in this context means *runtime safety only*. In order to
+ protect yourself against JavaScript injection in untrusted content, see
+ [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content).
+
+* **Fast processing**. It is fast enough to render on-demand in
+ most web applications without having to cache the output.
+
+* **Thread safety**. You can run multiple parsers in different
+ goroutines without ill effect. There is no dependence on global
+ shared state.
+
+* **Minimal dependencies**. Blackfriday only depends on standard
+ library packages in Go. The source code is pretty
+ self-contained, so it is easy to add to any project, including
+ Google App Engine projects.
+
+* **Standards compliant**. Output successfully validates using the
+ W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional.
+
+
+Extensions
+----------
+
+In addition to the standard markdown syntax, this package
+implements the following extensions:
+
+* **Intra-word emphasis supression**. The `_` character is
+ commonly used inside words when discussing code, so having
+ markdown interpret it as an emphasis command is usually the
+ wrong thing. Blackfriday lets you treat all emphasis markers as
+ normal characters when they occur inside a word.
+
+* **Tables**. Tables can be created by drawing them in the input
+ using a simple syntax:
+
+ ```
+ Name | Age
+ --------|------
+ Bob | 27
+ Alice | 23
+ ```
+
+* **Fenced code blocks**. In addition to the normal 4-space
+ indentation to mark code blocks, you can explicitly mark them
+ and supply a language (to make syntax highlighting simple). Just
+ mark it like this:
+
+ ```go
+ func getTrue() bool {
+ return true
+ }
+ ```
+
+ You can use 3 or more backticks to mark the beginning of the
+ block, and the same number to mark the end of the block.
+
+* **Definition lists**. A simple definition list is made of a single-line
+ term followed by a colon and the definition for that term.
+
+ Cat
+ : Fluffy animal everyone likes
+
+ Internet
+ : Vector of transmission for pictures of cats
+
+ Terms must be separated from the previous definition by a blank line.
+
+* **Footnotes**. A marker in the text that will become a superscript number;
+ a footnote definition that will be placed in a list of footnotes at the
+ end of the document. A footnote looks like this:
+
+ This is a footnote.[^1]
+
+ [^1]: the footnote text.
+
+* **Autolinking**. Blackfriday can find URLs that have not been
+ explicitly marked as links and turn them into links.
+
+* **Strikethrough**. Use two tildes (`~~`) to mark text that
+ should be crossed out.
+
+* **Hard line breaks**. With this extension enabled newlines in the input
+ translate into line breaks in the output. This extension is off by default.
+
+* **Smart quotes**. Smartypants-style punctuation substitution is
+ supported, turning normal double- and single-quote marks into
+ curly quotes, etc.
+
+* **LaTeX-style dash parsing** is an additional option, where `--`
+ is translated into `–`, and `---` is translated into
+ `—`. This differs from most smartypants processors, which
+ turn a single hyphen into an ndash and a double hyphen into an
+ mdash.
+
+* **Smart fractions**, where anything that looks like a fraction
+ is translated into suitable HTML (instead of just a few special
+ cases like most smartypant processors). For example, `4/5`
+ becomes `4 ⁄5 `, which renders as
+ 4 ⁄5 .
+
+
+Other renderers
+---------------
+
+Blackfriday is structured to allow alternative rendering engines. Here
+are a few of note:
+
+* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown):
+ provides a GitHub Flavored Markdown renderer with fenced code block
+ highlighting, clickable heading anchor links.
+
+ It's not customizable, and its goal is to produce HTML output
+ equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode),
+ except the rendering is performed locally.
+
+* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
+ but for markdown.
+
+* [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-latex):
+ renders output as LaTeX.
+
+
+Todo
+----
+
+* More unit testing
+* Improve unicode support. It does not understand all unicode
+ rules (about what constitutes a letter, a punctuation symbol,
+ etc.), so it may fail to detect word boundaries correctly in
+ some instances. It is safe on all utf-8 input.
+
+
+License
+-------
+
+[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt)
+
+
+ [1]: https://daringfireball.net/projects/markdown/ "Markdown"
+ [2]: https://golang.org/ "Go Language"
+ [3]: https://github.com/vmg/sundown "Sundown"
+ [4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func"
+ [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday"
+ [6]: https://labix.org/gopkg.in "gopkg.in"
diff --git a/pkg/blackfriday/block.go b/pkg/blackfriday/block.go
new file mode 100644
index 00000000..1f9c1a84
--- /dev/null
+++ b/pkg/blackfriday/block.go
@@ -0,0 +1,1558 @@
+//
+// Blackfriday Markdown Processor
+// Available at http://github.com/russross/blackfriday
+//
+// Copyright © 2011 Russ Ross .
+// Distributed under the Simplified BSD License.
+// See README.md for details.
+//
+
+//
+// Functions to parse block-level elements.
+//
+
+package blackfriday
+
+import (
+ "bytes"
+ "html"
+ "regexp"
+
+ "github.com/shurcooL/sanitized_anchor_name"
+)
+
+const (
+ charEntity = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});"
+ escapable = "[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]"
+)
+
+var (
+ reBackslashOrAmp = regexp.MustCompile("[\\&]")
+ reEntityOrEscapedChar = regexp.MustCompile("(?i)\\\\" + escapable + "|" + charEntity)
+)
+
+// Parse block-level data.
+// Note: this function and many that it calls assume that
+// the input buffer ends with a newline.
+func (p *Markdown) block(data []byte) {
+ // this is called recursively: enforce a maximum depth
+ if p.nesting >= p.maxNesting {
+ return
+ }
+ p.nesting++
+
+ // parse out one block-level construct at a time
+ for len(data) > 0 {
+ // prefixed heading:
+ //
+ // # Heading 1
+ // ## Heading 2
+ // ...
+ // ###### Heading 6
+ if p.isPrefixHeading(data) {
+ data = data[p.prefixHeading(data):]
+ continue
+ }
+
+ // block of preformatted HTML:
+ //
+ //
+ // ...
+ //
+ if data[0] == '<' {
+ if i := p.html(data, true); i > 0 {
+ data = data[i:]
+ continue
+ }
+ }
+
+ // title block
+ //
+ // % stuff
+ // % more stuff
+ // % even more stuff
+ if p.extensions&Titleblock != 0 {
+ if data[0] == '%' {
+ if i := p.titleBlock(data, true); i > 0 {
+ data = data[i:]
+ continue
+ }
+ }
+ }
+
+ // blank lines. note: returns the # of bytes to skip
+ if i := p.isEmpty(data); i > 0 {
+ data = data[i:]
+ continue
+ }
+
+ // indented code block:
+ //
+ // func max(a, b int) int {
+ // if a > b {
+ // return a
+ // }
+ // return b
+ // }
+ if p.codePrefix(data) > 0 {
+ data = data[p.code(data):]
+ continue
+ }
+
+ // fenced code block:
+ //
+ // ``` go
+ // func fact(n int) int {
+ // if n <= 1 {
+ // return n
+ // }
+ // return n * fact(n-1)
+ // }
+ // ```
+ if p.extensions&FencedCode != 0 {
+ if i := p.fencedCodeBlock(data, true); i > 0 {
+ data = data[i:]
+ continue
+ }
+ }
+
+ // horizontal rule:
+ //
+ // ------
+ // or
+ // ******
+ // or
+ // ______
+ if p.isHRule(data) {
+ p.addBlock(HorizontalRule, nil)
+ var i int
+ for i = 0; i < len(data) && data[i] != '\n'; i++ {
+ }
+ data = data[i:]
+ continue
+ }
+
+ // block quote:
+ //
+ // > A big quote I found somewhere
+ // > on the web
+ if p.quotePrefix(data) > 0 {
+ data = data[p.quote(data):]
+ continue
+ }
+
+ // table:
+ //
+ // Name | Age | Phone
+ // ------|-----|---------
+ // Bob | 31 | 555-1234
+ // Alice | 27 | 555-4321
+ if p.extensions&Tables != 0 {
+ if i := p.table(data); i > 0 {
+ data = data[i:]
+ continue
+ }
+ }
+
+ // an itemized/unordered list:
+ //
+ // * Item 1
+ // * Item 2
+ //
+ // also works with + or -
+ if p.uliPrefix(data) > 0 {
+ data = data[p.list(data, 0):]
+ continue
+ }
+
+ // a numbered/ordered list:
+ //
+ // 1. Item 1
+ // 2. Item 2
+ if p.oliPrefix(data) > 0 {
+ data = data[p.list(data, ListTypeOrdered):]
+ continue
+ }
+
+ // definition lists:
+ //
+ // Term 1
+ // : Definition a
+ // : Definition b
+ //
+ // Term 2
+ // : Definition c
+ if p.extensions&DefinitionLists != 0 {
+ if p.dliPrefix(data) > 0 {
+ data = data[p.list(data, ListTypeDefinition):]
+ continue
+ }
+ }
+
+ // anything else must look like a normal paragraph
+ // note: this finds underlined headings, too
+ data = data[p.paragraph(data):]
+ }
+
+ p.nesting--
+}
+
+func (p *Markdown) addBlock(typ NodeType, content []byte) *Node {
+ p.closeUnmatchedBlocks()
+ container := p.addChild(typ, 0)
+ container.content = content
+ return container
+}
+
+func (p *Markdown) isPrefixHeading(data []byte) bool {
+ if data[0] != '#' {
+ return false
+ }
+
+ if p.extensions&SpaceHeadings != 0 {
+ level := 0
+ for level < 6 && level < len(data) && data[level] == '#' {
+ level++
+ }
+ if level == len(data) || data[level] != ' ' {
+ return false
+ }
+ }
+ return true
+}
+
+func (p *Markdown) prefixHeading(data []byte) int {
+ level := 0
+ for level < 6 && level < len(data) && data[level] == '#' {
+ level++
+ }
+ i := skipChar(data, level, ' ')
+ end := skipUntilChar(data, i, '\n')
+ skip := end
+ id := ""
+ if p.extensions&HeadingIDs != 0 {
+ j, k := 0, 0
+ // find start/end of heading id
+ for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ {
+ }
+ for k = j + 1; k < end && data[k] != '}'; k++ {
+ }
+ // extract heading id iff found
+ if j < end && k < end {
+ id = string(data[j+2 : k])
+ end = j
+ skip = k + 1
+ for end > 0 && data[end-1] == ' ' {
+ end--
+ }
+ }
+ }
+ for end > 0 && data[end-1] == '#' {
+ if isBackslashEscaped(data, end-1) {
+ break
+ }
+ end--
+ }
+ for end > 0 && data[end-1] == ' ' {
+ end--
+ }
+ if end > i {
+ if id == "" && p.extensions&AutoHeadingIDs != 0 {
+ id = sanitized_anchor_name.Create(string(data[i:end]))
+ }
+ block := p.addBlock(Heading, data[i:end])
+ block.HeadingID = id
+ block.Level = level
+ }
+ return skip
+}
+
+func (p *Markdown) isUnderlinedHeading(data []byte) int {
+ // test of level 1 heading
+ if data[0] == '=' {
+ i := skipChar(data, 1, '=')
+ i = skipChar(data, i, ' ')
+ if i < len(data) && data[i] == '\n' {
+ return 1
+ }
+ return 0
+ }
+
+ // test of level 2 heading
+ if data[0] == '-' {
+ i := skipChar(data, 1, '-')
+ i = skipChar(data, i, ' ')
+ if i < len(data) && data[i] == '\n' {
+ return 2
+ }
+ return 0
+ }
+
+ return 0
+}
+
+func (p *Markdown) titleBlock(data []byte, doRender bool) int {
+ if data[0] != '%' {
+ return 0
+ }
+ splitData := bytes.Split(data, []byte("\n"))
+ var i int
+ for idx, b := range splitData {
+ if !bytes.HasPrefix(b, []byte("%")) {
+ i = idx // - 1
+ break
+ }
+ }
+
+ data = bytes.Join(splitData[0:i], []byte("\n"))
+ consumed := len(data)
+ data = bytes.TrimPrefix(data, []byte("% "))
+ data = bytes.Replace(data, []byte("\n% "), []byte("\n"), -1)
+ block := p.addBlock(Heading, data)
+ block.Level = 1
+ block.IsTitleblock = true
+
+ return consumed
+}
+
+func (p *Markdown) html(data []byte, doRender bool) int {
+ var i, j int
+
+ // identify the opening tag
+ if data[0] != '<' {
+ return 0
+ }
+ curtag, tagfound := p.htmlFindTag(data[1:])
+
+ // handle special cases
+ if !tagfound {
+ // check for an HTML comment
+ if size := p.htmlComment(data, doRender); size > 0 {
+ return size
+ }
+
+ // check for an tag
+ if size := p.htmlHr(data, doRender); size > 0 {
+ return size
+ }
+
+ // no special case recognized
+ return 0
+ }
+
+ // look for an unindented matching closing tag
+ // followed by a blank line
+ found := false
+ /*
+ closetag := []byte("\n" + curtag + ">")
+ j = len(curtag) + 1
+ for !found {
+ // scan for a closing tag at the beginning of a line
+ if skip := bytes.Index(data[j:], closetag); skip >= 0 {
+ j += skip + len(closetag)
+ } else {
+ break
+ }
+
+ // see if it is the only thing on the line
+ if skip := p.isEmpty(data[j:]); skip > 0 {
+ // see if it is followed by a blank line/eof
+ j += skip
+ if j >= len(data) {
+ found = true
+ i = j
+ } else {
+ if skip := p.isEmpty(data[j:]); skip > 0 {
+ j += skip
+ found = true
+ i = j
+ }
+ }
+ }
+ }
+ */
+
+ // if not found, try a second pass looking for indented match
+ // but not if tag is "ins" or "del" (following original Markdown.pl)
+ if !found && curtag != "ins" && curtag != "del" {
+ i = 1
+ for i < len(data) {
+ i++
+ for i < len(data) && !(data[i-1] == '<' && data[i] == '/') {
+ i++
+ }
+
+ if i+2+len(curtag) >= len(data) {
+ break
+ }
+
+ j = p.htmlFindEnd(curtag, data[i-1:])
+
+ if j > 0 {
+ i += j - 1
+ found = true
+ break
+ }
+ }
+ }
+
+ if !found {
+ return 0
+ }
+
+ // the end of the block has been found
+ if doRender {
+ // trim newlines
+ end := i
+ for end > 0 && data[end-1] == '\n' {
+ end--
+ }
+ finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end]))
+ }
+
+ return i
+}
+
+func finalizeHTMLBlock(block *Node) {
+ block.Literal = block.content
+ block.content = nil
+}
+
+// HTML comment, lax form
+func (p *Markdown) htmlComment(data []byte, doRender bool) int {
+ i := p.inlineHTMLComment(data)
+ // needs to end with a blank line
+ if j := p.isEmpty(data[i:]); j > 0 {
+ size := i + j
+ if doRender {
+ // trim trailing newlines
+ end := size
+ for end > 0 && data[end-1] == '\n' {
+ end--
+ }
+ block := p.addBlock(HTMLBlock, data[:end])
+ finalizeHTMLBlock(block)
+ }
+ return size
+ }
+ return 0
+}
+
+// HR, which is the only self-closing block tag considered
+func (p *Markdown) htmlHr(data []byte, doRender bool) int {
+ if len(data) < 4 {
+ return 0
+ }
+ if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') {
+ return 0
+ }
+ if data[3] != ' ' && data[3] != '/' && data[3] != '>' {
+ // not an tag after all; at least not a valid one
+ return 0
+ }
+ i := 3
+ for i < len(data) && data[i] != '>' && data[i] != '\n' {
+ i++
+ }
+ if i < len(data) && data[i] == '>' {
+ i++
+ if j := p.isEmpty(data[i:]); j > 0 {
+ size := i + j
+ if doRender {
+ // trim newlines
+ end := size
+ for end > 0 && data[end-1] == '\n' {
+ end--
+ }
+ finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end]))
+ }
+ return size
+ }
+ }
+ return 0
+}
+
+func (p *Markdown) htmlFindTag(data []byte) (string, bool) {
+ i := 0
+ for i < len(data) && isalnum(data[i]) {
+ i++
+ }
+ key := string(data[:i])
+ if _, ok := blockTags[key]; ok {
+ return key, true
+ }
+ return "", false
+}
+
+func (p *Markdown) htmlFindEnd(tag string, data []byte) int {
+ // assume data[0] == '<' && data[1] == '/' already tested
+ if tag == "hr" {
+ return 2
+ }
+ // check if tag is a match
+ closetag := []byte("" + tag + ">")
+ if !bytes.HasPrefix(data, closetag) {
+ return 0
+ }
+ i := len(closetag)
+
+ // check that the rest of the line is blank
+ skip := 0
+ if skip = p.isEmpty(data[i:]); skip == 0 {
+ return 0
+ }
+ i += skip
+ skip = 0
+
+ if i >= len(data) {
+ return i
+ }
+
+ if p.extensions&LaxHTMLBlocks != 0 {
+ return i
+ }
+ if skip = p.isEmpty(data[i:]); skip == 0 {
+ // following line must be blank
+ return 0
+ }
+
+ return i + skip
+}
+
+func (*Markdown) isEmpty(data []byte) int {
+ // it is okay to call isEmpty on an empty buffer
+ if len(data) == 0 {
+ return 0
+ }
+
+ var i int
+ for i = 0; i < len(data) && data[i] != '\n'; i++ {
+ if data[i] != ' ' && data[i] != '\t' {
+ return 0
+ }
+ }
+ if i < len(data) && data[i] == '\n' {
+ i++
+ }
+ return i
+}
+
+func (*Markdown) isHRule(data []byte) bool {
+ i := 0
+
+ // skip up to three spaces
+ for i < 3 && data[i] == ' ' {
+ i++
+ }
+
+ // look at the hrule char
+ if data[i] != '*' && data[i] != '-' && data[i] != '_' {
+ return false
+ }
+ c := data[i]
+
+ // the whole line must be the char or whitespace
+ n := 0
+ for i < len(data) && data[i] != '\n' {
+ switch {
+ case data[i] == c:
+ n++
+ case data[i] != ' ':
+ return false
+ }
+ i++
+ }
+
+ return n >= 3
+}
+
+// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data,
+// and returns the end index if so, or 0 otherwise. It also returns the marker found.
+// If syntax is not nil, it gets set to the syntax specified in the fence line.
+func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker string) {
+ i, size := 0, 0
+
+ // skip up to three spaces
+ for i < len(data) && i < 3 && data[i] == ' ' {
+ i++
+ }
+
+ // check for the marker characters: ~ or `
+ if i >= len(data) {
+ return 0, ""
+ }
+ if data[i] != '~' && data[i] != '`' {
+ return 0, ""
+ }
+
+ c := data[i]
+
+ // the whole line must be the same char or whitespace
+ for i < len(data) && data[i] == c {
+ size++
+ i++
+ }
+
+ // the marker char must occur at least 3 times
+ if size < 3 {
+ return 0, ""
+ }
+ marker = string(data[i-size : i])
+
+ // if this is the end marker, it must match the beginning marker
+ if oldmarker != "" && marker != oldmarker {
+ return 0, ""
+ }
+
+ // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here
+ // into one, always get the syntax, and discard it if the caller doesn't care.
+ if syntax != nil {
+ syn := 0
+ i = skipChar(data, i, ' ')
+
+ if i >= len(data) {
+ if i == len(data) {
+ return i, marker
+ }
+ return 0, ""
+ }
+
+ syntaxStart := i
+
+ if data[i] == '{' {
+ i++
+ syntaxStart++
+
+ for i < len(data) && data[i] != '}' && data[i] != '\n' {
+ syn++
+ i++
+ }
+
+ if i >= len(data) || data[i] != '}' {
+ return 0, ""
+ }
+
+ // strip all whitespace at the beginning and the end
+ // of the {} block
+ for syn > 0 && isspace(data[syntaxStart]) {
+ syntaxStart++
+ syn--
+ }
+
+ for syn > 0 && isspace(data[syntaxStart+syn-1]) {
+ syn--
+ }
+
+ i++
+ } else {
+ for i < len(data) && !isspace(data[i]) {
+ syn++
+ i++
+ }
+ }
+
+ *syntax = string(data[syntaxStart : syntaxStart+syn])
+ }
+
+ i = skipChar(data, i, ' ')
+ if i >= len(data) || data[i] != '\n' {
+ if i == len(data) {
+ return i, marker
+ }
+ return 0, ""
+ }
+ return i + 1, marker // Take newline into account.
+}
+
+// fencedCodeBlock returns the end index if data contains a fenced code block at the beginning,
+// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects.
+// If doRender is true, a final newline is mandatory to recognize the fenced code block.
+func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int {
+ var syntax string
+ beg, marker := isFenceLine(data, &syntax, "")
+ if beg == 0 || beg >= len(data) {
+ return 0
+ }
+
+ var work bytes.Buffer
+ work.Write([]byte(syntax))
+ work.WriteByte('\n')
+
+ for {
+ // safe to assume beg < len(data)
+
+ // check for the end of the code block
+ fenceEnd, _ := isFenceLine(data[beg:], nil, marker)
+ if fenceEnd != 0 {
+ beg += fenceEnd
+ break
+ }
+
+ // copy the current line
+ end := skipUntilChar(data, beg, '\n') + 1
+
+ // did we reach the end of the buffer without a closing marker?
+ if end >= len(data) {
+ return 0
+ }
+
+ // verbatim copy to the working buffer
+ if doRender {
+ work.Write(data[beg:end])
+ }
+ beg = end
+ }
+
+ if doRender {
+ block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer
+ block.IsFenced = true
+ finalizeCodeBlock(block)
+ }
+
+ return beg
+}
+
+func unescapeChar(str []byte) []byte {
+ if str[0] == '\\' {
+ return []byte{str[1]}
+ }
+ return []byte(html.UnescapeString(string(str)))
+}
+
+func unescapeString(str []byte) []byte {
+ if reBackslashOrAmp.Match(str) {
+ return reEntityOrEscapedChar.ReplaceAllFunc(str, unescapeChar)
+ }
+ return str
+}
+
+func finalizeCodeBlock(block *Node) {
+ if block.IsFenced {
+ newlinePos := bytes.IndexByte(block.content, '\n')
+ firstLine := block.content[:newlinePos]
+ rest := block.content[newlinePos+1:]
+ block.Info = unescapeString(bytes.Trim(firstLine, "\n"))
+ block.Literal = rest
+ } else {
+ block.Literal = block.content
+ }
+ block.content = nil
+}
+
+func (p *Markdown) table(data []byte) int {
+ table := p.addBlock(Table, nil)
+ i, columns := p.tableHeader(data)
+ if i == 0 {
+ p.tip = table.Parent
+ table.Unlink()
+ return 0
+ }
+
+ p.addBlock(TableBody, nil)
+
+ for i < len(data) {
+ pipes, rowStart := 0, i
+ for ; i < len(data) && data[i] != '\n'; i++ {
+ if data[i] == '|' {
+ pipes++
+ }
+ }
+
+ if pipes == 0 {
+ i = rowStart
+ break
+ }
+
+ // include the newline in data sent to tableRow
+ if i < len(data) && data[i] == '\n' {
+ i++
+ }
+ p.tableRow(data[rowStart:i], columns, false)
+ }
+
+ return i
+}
+
+// check if the specified position is preceded by an odd number of backslashes
+func isBackslashEscaped(data []byte, i int) bool {
+ backslashes := 0
+ for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' {
+ backslashes++
+ }
+ return backslashes&1 == 1
+}
+
+func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) {
+ i := 0
+ colCount := 1
+ for i = 0; i < len(data) && data[i] != '\n'; i++ {
+ if data[i] == '|' && !isBackslashEscaped(data, i) {
+ colCount++
+ }
+ }
+
+ // doesn't look like a table header
+ if colCount == 1 {
+ return
+ }
+
+ // include the newline in the data sent to tableRow
+ j := i
+ if j < len(data) && data[j] == '\n' {
+ j++
+ }
+ header := data[:j]
+
+ // column count ignores pipes at beginning or end of line
+ if data[0] == '|' {
+ colCount--
+ }
+ if i > 2 && data[i-1] == '|' && !isBackslashEscaped(data, i-1) {
+ colCount--
+ }
+
+ columns = make([]CellAlignFlags, colCount)
+
+ // move on to the header underline
+ i++
+ if i >= len(data) {
+ return
+ }
+
+ if data[i] == '|' && !isBackslashEscaped(data, i) {
+ i++
+ }
+ i = skipChar(data, i, ' ')
+
+ // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3
+ // and trailing | optional on last column
+ col := 0
+ for i < len(data) && data[i] != '\n' {
+ dashes := 0
+
+ if data[i] == ':' {
+ i++
+ columns[col] |= TableAlignmentLeft
+ dashes++
+ }
+ for i < len(data) && data[i] == '-' {
+ i++
+ dashes++
+ }
+ if i < len(data) && data[i] == ':' {
+ i++
+ columns[col] |= TableAlignmentRight
+ dashes++
+ }
+ for i < len(data) && data[i] == ' ' {
+ i++
+ }
+ if i == len(data) {
+ return
+ }
+ // end of column test is messy
+ switch {
+ case dashes < 3:
+ // not a valid column
+ return
+
+ case data[i] == '|' && !isBackslashEscaped(data, i):
+ // marker found, now skip past trailing whitespace
+ col++
+ i++
+ for i < len(data) && data[i] == ' ' {
+ i++
+ }
+
+ // trailing junk found after last column
+ if col >= colCount && i < len(data) && data[i] != '\n' {
+ return
+ }
+
+ case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount:
+ // something else found where marker was required
+ return
+
+ case data[i] == '\n':
+ // marker is optional for the last column
+ col++
+
+ default:
+ // trailing junk found after last column
+ return
+ }
+ }
+ if col != colCount {
+ return
+ }
+
+ p.addBlock(TableHead, nil)
+ p.tableRow(header, columns, true)
+ size = i
+ if size < len(data) && data[size] == '\n' {
+ size++
+ }
+ return
+}
+
+func (p *Markdown) tableRow(data []byte, columns []CellAlignFlags, header bool) {
+ p.addBlock(TableRow, nil)
+ i, col := 0, 0
+
+ if data[i] == '|' && !isBackslashEscaped(data, i) {
+ i++
+ }
+
+ for col = 0; col < len(columns) && i < len(data); col++ {
+ for i < len(data) && data[i] == ' ' {
+ i++
+ }
+
+ cellStart := i
+
+ for i < len(data) && (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' {
+ i++
+ }
+
+ cellEnd := i
+
+ // skip the end-of-cell marker, possibly taking us past end of buffer
+ i++
+
+ for cellEnd > cellStart && cellEnd-1 < len(data) && data[cellEnd-1] == ' ' {
+ cellEnd--
+ }
+
+ cell := p.addBlock(TableCell, data[cellStart:cellEnd])
+ cell.IsHeader = header
+ cell.Align = columns[col]
+ }
+
+ // pad it out with empty columns to get the right number
+ for ; col < len(columns); col++ {
+ cell := p.addBlock(TableCell, nil)
+ cell.IsHeader = header
+ cell.Align = columns[col]
+ }
+
+ // silently ignore rows with too many cells
+}
+
+// returns blockquote prefix length
+func (p *Markdown) quotePrefix(data []byte) int {
+ i := 0
+ for i < 3 && i < len(data) && data[i] == ' ' {
+ i++
+ }
+ if i < len(data) && data[i] == '>' {
+ if i+1 < len(data) && data[i+1] == ' ' {
+ return i + 2
+ }
+ return i + 1
+ }
+ return 0
+}
+
+// blockquote ends with at least one blank line
+// followed by something without a blockquote prefix
+func (p *Markdown) terminateBlockquote(data []byte, beg, end int) bool {
+ if p.isEmpty(data[beg:]) <= 0 {
+ return false
+ }
+ if end >= len(data) {
+ return true
+ }
+ return p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0
+}
+
+// parse a blockquote fragment
+func (p *Markdown) quote(data []byte) int {
+ block := p.addBlock(BlockQuote, nil)
+ var raw bytes.Buffer
+ beg, end := 0, 0
+ for beg < len(data) {
+ end = beg
+ // Step over whole lines, collecting them. While doing that, check for
+ // fenced code and if one's found, incorporate it altogether,
+ // irregardless of any contents inside it
+ for end < len(data) && data[end] != '\n' {
+ if p.extensions&FencedCode != 0 {
+ if i := p.fencedCodeBlock(data[end:], false); i > 0 {
+ // -1 to compensate for the extra end++ after the loop:
+ end += i - 1
+ break
+ }
+ }
+ end++
+ }
+ if end < len(data) && data[end] == '\n' {
+ end++
+ }
+ if pre := p.quotePrefix(data[beg:]); pre > 0 {
+ // skip the prefix
+ beg += pre
+ } else if p.terminateBlockquote(data, beg, end) {
+ break
+ }
+ // this line is part of the blockquote
+ raw.Write(data[beg:end])
+ beg = end
+ }
+ p.block(raw.Bytes())
+ p.finalize(block)
+ return end
+}
+
+// returns prefix length for block code
+func (p *Markdown) codePrefix(data []byte) int {
+ if len(data) >= 1 && data[0] == '\t' {
+ return 1
+ }
+ if len(data) >= 4 && data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' {
+ return 4
+ }
+ return 0
+}
+
+func (p *Markdown) code(data []byte) int {
+ var work bytes.Buffer
+
+ i := 0
+ for i < len(data) {
+ beg := i
+ for i < len(data) && data[i] != '\n' {
+ i++
+ }
+ if i < len(data) && data[i] == '\n' {
+ i++
+ }
+
+ blankline := p.isEmpty(data[beg:i]) > 0
+ if pre := p.codePrefix(data[beg:i]); pre > 0 {
+ beg += pre
+ } else if !blankline {
+ // non-empty, non-prefixed line breaks the pre
+ i = beg
+ break
+ }
+
+ // verbatim copy to the working buffer
+ if blankline {
+ work.WriteByte('\n')
+ } else {
+ work.Write(data[beg:i])
+ }
+ }
+
+ // trim all the \n off the end of work
+ workbytes := work.Bytes()
+ eol := len(workbytes)
+ for eol > 0 && workbytes[eol-1] == '\n' {
+ eol--
+ }
+ if eol != len(workbytes) {
+ work.Truncate(eol)
+ }
+
+ work.WriteByte('\n')
+
+ block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer
+ block.IsFenced = false
+ finalizeCodeBlock(block)
+
+ return i
+}
+
+// returns unordered list item prefix
+func (p *Markdown) uliPrefix(data []byte) int {
+ i := 0
+ // start with up to 3 spaces
+ for i < len(data) && i < 3 && data[i] == ' ' {
+ i++
+ }
+ if i >= len(data)-1 {
+ return 0
+ }
+ // need one of {'*', '+', '-'} followed by a space or a tab
+ if (data[i] != '*' && data[i] != '+' && data[i] != '-') ||
+ (data[i+1] != ' ' && data[i+1] != '\t') {
+ return 0
+ }
+ return i + 2
+}
+
+// returns ordered list item prefix
+func (p *Markdown) oliPrefix(data []byte) int {
+ i := 0
+
+ // start with up to 3 spaces
+ for i < 3 && i < len(data) && data[i] == ' ' {
+ i++
+ }
+
+ // count the digits
+ start := i
+ for i < len(data) && data[i] >= '0' && data[i] <= '9' {
+ i++
+ }
+ if start == i || i >= len(data)-1 {
+ return 0
+ }
+
+ // we need >= 1 digits followed by a dot and a space or a tab
+ if data[i] != '.' || !(data[i+1] == ' ' || data[i+1] == '\t') {
+ return 0
+ }
+ return i + 2
+}
+
+// returns definition list item prefix
+func (p *Markdown) dliPrefix(data []byte) int {
+ if len(data) < 2 {
+ return 0
+ }
+ i := 0
+ // need a ':' followed by a space or a tab
+ if data[i] != ':' || !(data[i+1] == ' ' || data[i+1] == '\t') {
+ return 0
+ }
+ for i < len(data) && data[i] == ' ' {
+ i++
+ }
+ return i + 2
+}
+
+// parse ordered or unordered list block
+func (p *Markdown) list(data []byte, flags ListType) int {
+ i := 0
+ flags |= ListItemBeginningOfList
+ block := p.addBlock(List, nil)
+ block.ListFlags = flags
+ block.Tight = true
+
+ for i < len(data) {
+ skip := p.listItem(data[i:], &flags)
+ if flags&ListItemContainsBlock != 0 {
+ block.ListData.Tight = false
+ }
+ i += skip
+ if skip == 0 || flags&ListItemEndOfList != 0 {
+ break
+ }
+ flags &= ^ListItemBeginningOfList
+ }
+
+ above := block.Parent
+ finalizeList(block)
+ p.tip = above
+ return i
+}
+
+// Returns true if block ends with a blank line, descending if needed
+// into lists and sublists.
+func endsWithBlankLine(block *Node) bool {
+ // TODO: figure this out. Always false now.
+ for block != nil {
+ //if block.lastLineBlank {
+ //return true
+ //}
+ t := block.Type
+ if t == List || t == Item {
+ block = block.LastChild
+ } else {
+ break
+ }
+ }
+ return false
+}
+
+func finalizeList(block *Node) {
+ block.open = false
+ item := block.FirstChild
+ for item != nil {
+ // check for non-final list item ending with blank line:
+ if endsWithBlankLine(item) && item.Next != nil {
+ block.ListData.Tight = false
+ break
+ }
+ // recurse into children of list item, to see if there are spaces
+ // between any of them:
+ subItem := item.FirstChild
+ for subItem != nil {
+ if endsWithBlankLine(subItem) && (item.Next != nil || subItem.Next != nil) {
+ block.ListData.Tight = false
+ break
+ }
+ subItem = subItem.Next
+ }
+ item = item.Next
+ }
+}
+
+// Parse a single list item.
+// Assumes initial prefix is already removed if this is a sublist.
+func (p *Markdown) listItem(data []byte, flags *ListType) int {
+ // keep track of the indentation of the first line
+ itemIndent := 0
+ if data[0] == '\t' {
+ itemIndent += 4
+ } else {
+ for itemIndent < 3 && data[itemIndent] == ' ' {
+ itemIndent++
+ }
+ }
+
+ var bulletChar byte = '*'
+ i := p.uliPrefix(data)
+ if i == 0 {
+ i = p.oliPrefix(data)
+ } else {
+ bulletChar = data[i-2]
+ }
+ if i == 0 {
+ i = p.dliPrefix(data)
+ // reset definition term flag
+ if i > 0 {
+ *flags &= ^ListTypeTerm
+ }
+ }
+ if i == 0 {
+ // if in definition list, set term flag and continue
+ if *flags&ListTypeDefinition != 0 {
+ *flags |= ListTypeTerm
+ } else {
+ return 0
+ }
+ }
+
+ // skip leading whitespace on first line
+ for i < len(data) && data[i] == ' ' {
+ i++
+ }
+
+ // find the end of the line
+ line := i
+ for i > 0 && i < len(data) && data[i-1] != '\n' {
+ i++
+ }
+
+ // get working buffer
+ var raw bytes.Buffer
+
+ // put the first line into the working buffer
+ raw.Write(data[line:i])
+ line = i
+
+ // process the following lines
+ containsBlankLine := false
+ sublist := 0
+ lastChunkSize := 0
+
+gatherlines:
+ for line < len(data) {
+ i++
+
+ // find the end of this line
+ for i < len(data) && data[i-1] != '\n' {
+ i++
+ }
+
+ // if it is an empty line, guess that it is part of this item
+ // and move on to the next line
+ if p.isEmpty(data[line:i]) > 0 {
+ containsBlankLine = true
+ line = i
+ continue
+ }
+
+ // calculate the indentation
+ indent := 0
+ indentIndex := 0
+ if data[line] == '\t' {
+ indentIndex++
+ indent += 4
+ } else {
+ for indent < 4 && line+indent < i && data[line+indent] == ' ' {
+ indent++
+ indentIndex++
+ }
+ }
+
+ chunk := data[line+indentIndex : i]
+
+ // evaluate how this line fits in
+ switch {
+ // is this a nested list item?
+ case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) ||
+ p.oliPrefix(chunk) > 0 ||
+ p.dliPrefix(chunk) > 0:
+
+ if containsBlankLine {
+ *flags |= ListItemContainsBlock
+ }
+
+ // to be a nested list, it must be indented more
+ // if not, it is the next item in the same list
+ if indent <= itemIndent {
+ break gatherlines
+ }
+
+ // is this the first item in the nested list?
+ if sublist == 0 {
+ if p.dliPrefix(chunk) > 0 {
+ sublist = raw.Len() - lastChunkSize
+ } else {
+ sublist = raw.Len()
+ }
+ }
+
+ // is this a nested prefix heading?
+ case p.isPrefixHeading(chunk):
+ // if the heading is not indented, it is not nested in the list
+ // and thus ends the list
+ if containsBlankLine && indent < 4 {
+ *flags |= ListItemEndOfList
+ break gatherlines
+ }
+ *flags |= ListItemContainsBlock
+
+ // anything following an empty line is only part
+ // of this item if it is indented 4 spaces
+ // (regardless of the indentation of the beginning of the item)
+ case containsBlankLine && indent < 4:
+ if *flags&ListTypeDefinition != 0 && i < len(data)-1 {
+ // is the next item still a part of this list?
+ next := i
+ for next < len(data) && data[next] != '\n' {
+ next++
+ }
+ for next < len(data)-1 && data[next] == '\n' {
+ next++
+ }
+ if i < len(data)-1 && data[i] != ':' && data[next] != ':' {
+ *flags |= ListItemEndOfList
+ }
+ } else {
+ *flags |= ListItemEndOfList
+ }
+ break gatherlines
+
+ // a blank line means this should be parsed as a block
+ case containsBlankLine:
+ raw.WriteByte('\n')
+ *flags |= ListItemContainsBlock
+ }
+
+ // if this line was preceded by one or more blanks,
+ // re-introduce the blank into the buffer
+ if containsBlankLine {
+ containsBlankLine = false
+ raw.WriteByte('\n')
+ }
+
+ // add the line into the working buffer without prefix
+ raw.Write(data[line+indentIndex : i])
+
+ // remember how much was written into raw, if this turns out to be a
+ // definition list we'll need this number to know where the sublist starts
+ lastChunkSize = i - (line + indentIndex)
+
+ line = i
+ }
+
+ rawBytes := raw.Bytes()
+
+ block := p.addBlock(Item, nil)
+ block.ListFlags = *flags
+ block.Tight = false
+ block.BulletChar = bulletChar
+ block.Delimiter = '.' // Only '.' is possible in Markdown, but ')' will also be possible in CommonMark
+
+ // render the contents of the list item
+ if *flags&ListItemContainsBlock != 0 && *flags&ListTypeTerm == 0 {
+ // intermediate render of block item, except for definition term
+ if sublist > 0 {
+ p.block(rawBytes[:sublist])
+ p.block(rawBytes[sublist:])
+ } else {
+ p.block(rawBytes)
+ }
+ } else {
+ // intermediate render of inline item
+ if sublist > 0 {
+ child := p.addChild(Paragraph, 0)
+ child.content = rawBytes[:sublist]
+ p.block(rawBytes[sublist:])
+ } else {
+ child := p.addChild(Paragraph, 0)
+ child.content = rawBytes
+ }
+ }
+ return line
+}
+
+// render a single paragraph that has already been parsed out
+func (p *Markdown) renderParagraph(data []byte) {
+ if len(data) == 0 {
+ return
+ }
+
+ // trim leading spaces
+ beg := 0
+ for data[beg] == ' ' {
+ beg++
+ }
+
+ end := len(data)
+ // trim trailing newline
+ if data[len(data)-1] == '\n' {
+ end--
+ }
+
+ // trim trailing spaces
+ for end > beg && data[end-1] == ' ' {
+ end--
+ }
+
+ p.addBlock(Paragraph, data[beg:end])
+}
+
+func (p *Markdown) paragraph(data []byte) int {
+ // prev: index of 1st char of previous line
+ // line: index of 1st char of current line
+ // i: index of cursor/end of current line
+ var prev, line, i int
+ tabSize := TabSizeDefault
+ if p.extensions&TabSizeEight != 0 {
+ tabSize = TabSizeDouble
+ }
+ // keep going until we find something to mark the end of the paragraph
+ for i < len(data) {
+ // mark the beginning of the current line
+ prev = line
+ current := data[i:]
+ line = i
+
+ // did we find a reference or a footnote? If so, end a paragraph
+ // preceding it and report that we have consumed up to the end of that
+ // reference:
+ if refEnd := isReference(p, current, tabSize); refEnd > 0 {
+ p.renderParagraph(data[:i])
+ return i + refEnd
+ }
+
+ // did we find a blank line marking the end of the paragraph?
+ if n := p.isEmpty(current); n > 0 {
+ // did this blank line followed by a definition list item?
+ if p.extensions&DefinitionLists != 0 {
+ if i < len(data)-1 && data[i+1] == ':' {
+ return p.list(data[prev:], ListTypeDefinition)
+ }
+ }
+
+ p.renderParagraph(data[:i])
+ return i + n
+ }
+
+ // an underline under some text marks a heading, so our paragraph ended on prev line
+ if i > 0 {
+ if level := p.isUnderlinedHeading(current); level > 0 {
+ // render the paragraph
+ p.renderParagraph(data[:prev])
+
+ // ignore leading and trailing whitespace
+ eol := i - 1
+ for prev < eol && data[prev] == ' ' {
+ prev++
+ }
+ for eol > prev && data[eol-1] == ' ' {
+ eol--
+ }
+
+ id := ""
+ if p.extensions&AutoHeadingIDs != 0 {
+ id = sanitized_anchor_name.Create(string(data[prev:eol]))
+ }
+
+ block := p.addBlock(Heading, data[prev:eol])
+ block.Level = level
+ block.HeadingID = id
+
+ // find the end of the underline
+ for i < len(data) && data[i] != '\n' {
+ i++
+ }
+ return i
+ }
+ }
+
+ // if the next line starts a block of HTML, then the paragraph ends here
+ if p.extensions&LaxHTMLBlocks != 0 {
+ if data[i] == '<' && p.html(current, false) > 0 {
+ // rewind to before the HTML block
+ p.renderParagraph(data[:i])
+ return i
+ }
+ }
+
+ // if there's a prefixed heading or a horizontal rule after this, paragraph is over
+ if p.isPrefixHeading(current) || p.isHRule(current) {
+ p.renderParagraph(data[:i])
+ return i
+ }
+
+ // if there's a fenced code block, paragraph is over
+ if p.extensions&FencedCode != 0 {
+ if p.fencedCodeBlock(current, false) > 0 {
+ p.renderParagraph(data[:i])
+ return i
+ }
+ }
+
+ // if there's a definition list item, prev line is a definition term
+ if p.extensions&DefinitionLists != 0 {
+ if p.dliPrefix(current) != 0 {
+ ret := p.list(data[prev:], ListTypeDefinition)
+ return ret
+ }
+ }
+
+ // if there's a list after this, paragraph is over
+ if p.extensions&NoEmptyLineBeforeBlock != 0 {
+ if p.uliPrefix(current) != 0 ||
+ p.oliPrefix(current) != 0 ||
+ p.quotePrefix(current) != 0 ||
+ p.codePrefix(current) != 0 {
+ p.renderParagraph(data[:i])
+ return i
+ }
+ }
+
+ // otherwise, scan to the beginning of the next line
+ nl := bytes.IndexByte(data[i:], '\n')
+ if nl >= 0 {
+ i += nl + 1
+ } else {
+ i += len(data[i:])
+ }
+ }
+
+ p.renderParagraph(data[:i])
+ return i
+}
+
+func skipChar(data []byte, start int, char byte) int {
+ i := start
+ for i < len(data) && data[i] == char {
+ i++
+ }
+ return i
+}
+
+func skipUntilChar(text []byte, start int, char byte) int {
+ i := start
+ for i < len(text) && text[i] != char {
+ i++
+ }
+ return i
+}
diff --git a/pkg/blackfriday/block_test.go b/pkg/blackfriday/block_test.go
new file mode 100644
index 00000000..0a2a4d84
--- /dev/null
+++ b/pkg/blackfriday/block_test.go
@@ -0,0 +1,1691 @@
+//
+// Blackfriday Markdown Processor
+// Available at http://github.com/russross/blackfriday
+//
+// Copyright © 2011 Russ Ross .
+// Distributed under the Simplified BSD License.
+// See README.md for details.
+//
+
+//
+// Unit tests for block parsing
+//
+
+package blackfriday
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestPrefixHeaderNoExtensions(t *testing.T) {
+ var tests = []string{
+ "# Header 1\n",
+ "Header 1 \n",
+
+ "## Header 2\n",
+ "Header 2 \n",
+
+ "### Header 3\n",
+ "Header 3 \n",
+
+ "#### Header 4\n",
+ "Header 4 \n",
+
+ "##### Header 5\n",
+ "Header 5 \n",
+
+ "###### Header 6\n",
+ "Header 6 \n",
+
+ "####### Header 7\n",
+ "# Header 7 \n",
+
+ "#Header 1\n",
+ "Header 1 \n",
+
+ "##Header 2\n",
+ "Header 2 \n",
+
+ "###Header 3\n",
+ "Header 3 \n",
+
+ "####Header 4\n",
+ "Header 4 \n",
+
+ "#####Header 5\n",
+ "Header 5 \n",
+
+ "######Header 6\n",
+ "Header 6 \n",
+
+ "#######Header 7\n",
+ "#Header 7 \n",
+
+ "Hello\n# Header 1\nGoodbye\n",
+ "Hello
\n\nHeader 1 \n\nGoodbye
\n",
+
+ "* List\n# Header\n* List\n",
+ "\nList
\n\nHeader \n\nList
\n \n",
+
+ "* List\n#Header\n* List\n",
+ "\nList
\n\nHeader \n\nList
\n \n",
+
+ "* List\n * Nested list\n # Nested header\n",
+ "\nList
\n\n\nNested list
\n\n" +
+ "Nested header \n \n \n",
+
+ "#Header 1 \\#\n",
+ "Header 1 # \n",
+
+ "#Header 1 \\# foo\n",
+ "Header 1 # foo \n",
+
+ "#Header 1 #\\##\n",
+ "Header 1 ## \n",
+ }
+ doTestsBlock(t, tests, 0)
+}
+
+func TestPrefixHeaderSpaceExtension(t *testing.T) {
+ var tests = []string{
+ "# Header 1\n",
+ "Header 1 \n",
+
+ "## Header 2\n",
+ "Header 2 \n",
+
+ "### Header 3\n",
+ "Header 3 \n",
+
+ "#### Header 4\n",
+ "Header 4 \n",
+
+ "##### Header 5\n",
+ "Header 5 \n",
+
+ "###### Header 6\n",
+ "Header 6 \n",
+
+ "####### Header 7\n",
+ "####### Header 7
\n",
+
+ "#Header 1\n",
+ "#Header 1
\n",
+
+ "##Header 2\n",
+ "##Header 2
\n",
+
+ "###Header 3\n",
+ "###Header 3
\n",
+
+ "####Header 4\n",
+ "####Header 4
\n",
+
+ "#####Header 5\n",
+ "#####Header 5
\n",
+
+ "######Header 6\n",
+ "######Header 6
\n",
+
+ "#######Header 7\n",
+ "#######Header 7
\n",
+
+ "Hello\n# Header 1\nGoodbye\n",
+ "Hello
\n\nHeader 1 \n\nGoodbye
\n",
+
+ "* List\n# Header\n* List\n",
+ "\nList
\n\nHeader \n\nList
\n \n",
+
+ "* List\n#Header\n* List\n",
+ "\n",
+
+ "* List\n * Nested list\n # Nested header\n",
+ "\nList
\n\n\nNested list
\n\n" +
+ "Nested header \n \n \n",
+ }
+ doTestsBlock(t, tests, SpaceHeadings)
+}
+
+func TestPrefixHeaderIdExtension(t *testing.T) {
+ var tests = []string{
+ "# Header 1 {#someid}\n",
+ "Header 1 \n",
+
+ "# Header 1 {#someid} \n",
+ "Header 1 \n",
+
+ "# Header 1 {#someid}\n",
+ "Header 1 \n",
+
+ "# Header 1 {#someid\n",
+ "Header 1 {#someid \n",
+
+ "# Header 1 {#someid\n",
+ "Header 1 {#someid \n",
+
+ "# Header 1 {#someid}}\n",
+ "Header 1 \n\n}
\n",
+
+ "## Header 2 {#someid}\n",
+ "Header 2 \n",
+
+ "### Header 3 {#someid}\n",
+ "Header 3 \n",
+
+ "#### Header 4 {#someid}\n",
+ "Header 4 \n",
+
+ "##### Header 5 {#someid}\n",
+ "Header 5 \n",
+
+ "###### Header 6 {#someid}\n",
+ "Header 6 \n",
+
+ "####### Header 7 {#someid}\n",
+ "# Header 7 \n",
+
+ "# Header 1 # {#someid}\n",
+ "Header 1 \n",
+
+ "## Header 2 ## {#someid}\n",
+ "Header 2 \n",
+
+ "Hello\n# Header 1\nGoodbye\n",
+ "Hello
\n\nHeader 1 \n\nGoodbye
\n",
+
+ "* List\n# Header {#someid}\n* List\n",
+ "\nList
\n\nHeader \n\nList
\n \n",
+
+ "* List\n#Header {#someid}\n* List\n",
+ "\nList
\n\nHeader \n\nList
\n \n",
+
+ "* List\n * Nested list\n # Nested header {#someid}\n",
+ "\nList
\n\n\nNested list
\n\n" +
+ "Nested header \n \n \n",
+ }
+ doTestsBlock(t, tests, HeadingIDs)
+}
+
+func TestPrefixHeaderIdExtensionWithPrefixAndSuffix(t *testing.T) {
+ var tests = []string{
+ "# header 1 {#someid}\n",
+ "header 1 \n",
+
+ "## header 2 {#someid}\n",
+ "header 2 \n",
+
+ "### header 3 {#someid}\n",
+ "header 3 \n",
+
+ "#### header 4 {#someid}\n",
+ "header 4 \n",
+
+ "##### header 5 {#someid}\n",
+ "header 5 \n",
+
+ "###### header 6 {#someid}\n",
+ "header 6 \n",
+
+ "####### header 7 {#someid}\n",
+ "# header 7 \n",
+
+ "# header 1 # {#someid}\n",
+ "header 1 \n",
+
+ "## header 2 ## {#someid}\n",
+ "header 2 \n",
+
+ "* List\n# Header {#someid}\n* List\n",
+ "\nList
\n\nHeader \n\nList
\n \n",
+
+ "* List\n#Header {#someid}\n* List\n",
+ "\nList
\n\nHeader \n\nList
\n \n",
+
+ "* List\n * Nested list\n # Nested header {#someid}\n",
+ "\nList
\n\n\nNested list
\n\n" +
+ "Nested header \n \n \n",
+ }
+
+ parameters := HTMLRendererParameters{
+ HeadingIDPrefix: "PRE:",
+ HeadingIDSuffix: ":POST",
+ }
+
+ doTestsParam(t, tests, TestParams{
+ extensions: HeadingIDs,
+ HTMLFlags: UseXHTML,
+ HTMLRendererParameters: parameters,
+ })
+}
+
+func TestPrefixAutoHeaderIdExtension(t *testing.T) {
+ var tests = []string{
+ "# Header 1\n",
+ "\n",
+
+ "# Header 1 \n",
+ "\n",
+
+ "## Header 2\n",
+ "\n",
+
+ "### Header 3\n",
+ "\n",
+
+ "#### Header 4\n",
+ "\n",
+
+ "##### Header 5\n",
+ "\n",
+
+ "###### Header 6\n",
+ "\n",
+
+ "####### Header 7\n",
+ "\n",
+
+ "Hello\n# Header 1\nGoodbye\n",
+ "Hello
\n\n\n\nGoodbye
\n",
+
+ "* List\n# Header\n* List\n",
+ "\n",
+
+ "* List\n#Header\n* List\n",
+ "\n",
+
+ "* List\n * Nested list\n # Nested header\n",
+ "\nList
\n\n\nNested list
\n\n" +
+ " \n \n \n",
+
+ "# Header\n\n# Header\n",
+ "\n\n\n",
+
+ "# Header 1\n\n# Header 1",
+ "\n\n\n",
+
+ "# Header\n\n# Header 1\n\n# Header\n\n# Header",
+ "\n\n\n\n\n\n\n",
+ }
+ doTestsBlock(t, tests, AutoHeadingIDs)
+}
+
+func TestPrefixAutoHeaderIdExtensionWithPrefixAndSuffix(t *testing.T) {
+ var tests = []string{
+ "# Header 1\n",
+ "\n",
+
+ "# Header 1 \n",
+ "\n",
+
+ "## Header 2\n",
+ "\n",
+
+ "### Header 3\n",
+ "\n",
+
+ "#### Header 4\n",
+ "\n",
+
+ "##### Header 5\n",
+ "\n",
+
+ "###### Header 6\n",
+ "\n",
+
+ "####### Header 7\n",
+ "\n",
+
+ "Hello\n# Header 1\nGoodbye\n",
+ "Hello
\n\n\n\nGoodbye
\n",
+
+ "* List\n# Header\n* List\n",
+ "\n",
+
+ "* List\n#Header\n* List\n",
+ "\n",
+
+ "* List\n * Nested list\n # Nested header\n",
+ "\nList
\n\n\nNested list
\n\n" +
+ " \n \n \n",
+
+ "# Header\n\n# Header\n",
+ "\n\n\n",
+
+ "# Header 1\n\n# Header 1",
+ "\n\n\n",
+
+ "# Header\n\n# Header 1\n\n# Header\n\n# Header",
+ "\n\n\n\n\n\n\n",
+ }
+
+ parameters := HTMLRendererParameters{
+ HeadingIDPrefix: "PRE:",
+ HeadingIDSuffix: ":POST",
+ }
+
+ doTestsParam(t, tests, TestParams{
+ extensions: AutoHeadingIDs,
+ HTMLFlags: UseXHTML,
+ HTMLRendererParameters: parameters,
+ })
+}
+
+func TestPrefixMultipleHeaderExtensions(t *testing.T) {
+ var tests = []string{
+ "# Header\n\n# Header {#header}\n\n# Header 1",
+ "\n\n\n\n\n",
+ }
+ doTestsBlock(t, tests, AutoHeadingIDs|HeadingIDs)
+}
+
+func TestUnderlineHeaders(t *testing.T) {
+ var tests = []string{
+ "Header 1\n========\n",
+ "Header 1 \n",
+
+ "Header 2\n--------\n",
+ "Header 2 \n",
+
+ "A\n=\n",
+ "A \n",
+
+ "B\n-\n",
+ "B \n",
+
+ "Paragraph\nHeader\n=\n",
+ "Paragraph
\n\nHeader \n",
+
+ "Header\n===\nParagraph\n",
+ "Header \n\nParagraph
\n",
+
+ "Header\n===\nAnother header\n---\n",
+ "Header \n\nAnother header \n",
+
+ " Header\n======\n",
+ "Header \n",
+
+ " Code\n========\n",
+ "Code\n
\n\n========
\n",
+
+ "Header with *inline*\n=====\n",
+ "Header with inline \n",
+
+ "* List\n * Sublist\n Not a header\n ------\n",
+ "\nList\n\n\nSublist\nNot a header\n------ \n \n \n",
+
+ "Paragraph\n\n\n\n\nHeader\n===\n",
+ "Paragraph
\n\nHeader \n",
+
+ "Trailing space \n==== \n\n",
+ "Trailing space \n",
+
+ "Trailing spaces\n==== \n\n",
+ "Trailing spaces \n",
+
+ "Double underline\n=====\n=====\n",
+ "Double underline \n\n=====
\n",
+ }
+ doTestsBlock(t, tests, 0)
+}
+
+func TestUnderlineHeadersAutoIDs(t *testing.T) {
+ var tests = []string{
+ "Header 1\n========\n",
+ "\n",
+
+ "Header 2\n--------\n",
+ "\n",
+
+ "A\n=\n",
+ "A \n",
+
+ "B\n-\n",
+ "B \n",
+
+ "Paragraph\nHeader\n=\n",
+ "Paragraph
\n\n\n",
+
+ "Header\n===\nParagraph\n",
+ "\n\nParagraph
\n",
+
+ "Header\n===\nAnother header\n---\n",
+ "\n\n\n",
+
+ " Header\n======\n",
+ "\n",
+
+ "Header with *inline*\n=====\n",
+ "\n",
+
+ "Paragraph\n\n\n\n\nHeader\n===\n",
+ "Paragraph
\n\n\n",
+
+ "Trailing space \n==== \n\n",
+ "Trailing space \n",
+
+ "Trailing spaces\n==== \n\n",
+ "Trailing spaces \n",
+
+ "Double underline\n=====\n=====\n",
+ "Double underline \n\n=====
\n",
+
+ "Header\n======\n\nHeader\n======\n",
+ "\n\n\n",
+
+ "Header 1\n========\n\nHeader 1\n========\n",
+ "\n\n\n",
+ }
+ doTestsBlock(t, tests, AutoHeadingIDs)
+}
+
+func TestHorizontalRule(t *testing.T) {
+ var tests = []string{
+ "-\n",
+ "-
\n",
+
+ "--\n",
+ "--
\n",
+
+ "---\n",
+ " \n",
+
+ "----\n",
+ " \n",
+
+ "*\n",
+ "*
\n",
+
+ "**\n",
+ "**
\n",
+
+ "***\n",
+ " \n",
+
+ "****\n",
+ " \n",
+
+ "_\n",
+ "_
\n",
+
+ "__\n",
+ "__
\n",
+
+ "___\n",
+ " \n",
+
+ "____\n",
+ " \n",
+
+ "-*-\n",
+ "-*-
\n",
+
+ "- - -\n",
+ " \n",
+
+ "* * *\n",
+ " \n",
+
+ "_ _ _\n",
+ " \n",
+
+ "-----*\n",
+ "-----*
\n",
+
+ " ------ \n",
+ " \n",
+
+ "Hello\n***\n",
+ "Hello
\n\n \n",
+
+ "---\n***\n___\n",
+ " \n\n \n\n \n",
+ }
+ doTestsBlock(t, tests, 0)
+}
+
+func TestUnorderedList(t *testing.T) {
+ var tests = []string{
+ "* Hello\n",
+ "\n",
+
+ "* Yin\n* Yang\n",
+ "\n",
+
+ "* Ting\n* Bong\n* Goo\n",
+ "\n",
+
+ "* Yin\n\n* Yang\n",
+ "\n",
+
+ "* Ting\n\n* Bong\n* Goo\n",
+ "\n",
+
+ "+ Hello\n",
+ "\n",
+
+ "+ Yin\n+ Yang\n",
+ "\n",
+
+ "+ Ting\n+ Bong\n+ Goo\n",
+ "\n",
+
+ "+ Yin\n\n+ Yang\n",
+ "\n",
+
+ "+ Ting\n\n+ Bong\n+ Goo\n",
+ "\n",
+
+ "- Hello\n",
+ "\n",
+
+ "- Yin\n- Yang\n",
+ "\n",
+
+ "- Ting\n- Bong\n- Goo\n",
+ "\n",
+
+ "- Yin\n\n- Yang\n",
+ "\n",
+
+ "- Ting\n\n- Bong\n- Goo\n",
+ "\n",
+
+ "*Hello\n",
+ "*Hello
\n",
+
+ "* Hello \n",
+ "\n",
+
+ "* Hello \n Next line \n",
+ "\n",
+
+ "Paragraph\n* No linebreak\n",
+ "Paragraph\n* No linebreak
\n",
+
+ "Paragraph\n\n* Linebreak\n",
+ "Paragraph
\n\n\n",
+
+ "* List\n * Nested list\n",
+ "\n",
+
+ "* List\n\n * Nested list\n",
+ "\n",
+
+ "* List\n Second line\n\n + Nested\n",
+ "\nList\nSecond line
\n\n \n \n",
+
+ "* List\n + Nested\n\n Continued\n",
+ "\nList
\n\n\n\nContinued
\n \n",
+
+ "* List\n * shallow indent\n",
+ "\n",
+
+ "* List\n" +
+ " * shallow indent\n" +
+ " * part of second list\n" +
+ " * still second\n" +
+ " * almost there\n" +
+ " * third level\n",
+ "\n" +
+ "List\n\n" +
+ "\n" +
+ "shallow indent \n" +
+ "part of second list \n" +
+ "still second \n" +
+ "almost there\n\n" +
+ "\n" +
+ "third level \n" +
+ " \n" +
+ " \n" +
+ " \n",
+
+ "* List\n extra indent, same paragraph\n",
+ "\nList\n extra indent, same paragraph \n \n",
+
+ "* List\n\n code block\n",
+ "\n",
+
+ "* List\n\n code block with spaces\n",
+ "\nList
\n\n code block with spaces\n
\n \n",
+
+ "* List\n\n * sublist\n\n normal text\n\n * another sublist\n",
+ "\nList
\n\n\n\nnormal text
\n\n \n \n",
+ }
+ doTestsBlock(t, tests, 0)
+}
+
+func TestOrderedList(t *testing.T) {
+ var tests = []string{
+ "1. Hello\n",
+ "\nHello \n \n",
+
+ "1. Yin\n2. Yang\n",
+ "\nYin \nYang \n \n",
+
+ "1. Ting\n2. Bong\n3. Goo\n",
+ "\nTing \nBong \nGoo \n \n",
+
+ "1. Yin\n\n2. Yang\n",
+ "\nYin
\n\nYang
\n \n",
+
+ "1. Ting\n\n2. Bong\n3. Goo\n",
+ "\nTing
\n\nBong
\n\nGoo
\n \n",
+
+ "1 Hello\n",
+ "1 Hello
\n",
+
+ "1.Hello\n",
+ "1.Hello
\n",
+
+ "1. Hello \n",
+ "\nHello \n \n",
+
+ "1. Hello \n Next line \n",
+ "\nHello\nNext line \n \n",
+
+ "Paragraph\n1. No linebreak\n",
+ "Paragraph\n1. No linebreak
\n",
+
+ "Paragraph\n\n1. Linebreak\n",
+ "Paragraph
\n\n\nLinebreak \n \n",
+
+ "1. List\n 1. Nested list\n",
+ "\nList\n\n\nNested list \n \n \n",
+
+ "1. List\n\n 1. Nested list\n",
+ "\nList
\n\n\nNested list \n \n \n",
+
+ "1. List\n Second line\n\n 1. Nested\n",
+ "\nList\nSecond line
\n\n\nNested \n \n \n",
+
+ "1. List\n 1. Nested\n\n Continued\n",
+ "\nList
\n\n\nNested \n \n\nContinued
\n \n",
+
+ "1. List\n 1. shallow indent\n",
+ "\nList\n\n\nshallow indent \n \n \n",
+
+ "1. List\n" +
+ " 1. shallow indent\n" +
+ " 2. part of second list\n" +
+ " 3. still second\n" +
+ " 4. almost there\n" +
+ " 1. third level\n",
+ "\n" +
+ "List\n\n" +
+ "\n" +
+ "shallow indent \n" +
+ "part of second list \n" +
+ "still second \n" +
+ "almost there\n\n" +
+ "\n" +
+ "third level \n" +
+ " \n" +
+ " \n" +
+ " \n",
+
+ "1. List\n extra indent, same paragraph\n",
+ "\nList\n extra indent, same paragraph \n \n",
+
+ "1. List\n\n code block\n",
+ "\nList
\n\ncode block\n
\n \n",
+
+ "1. List\n\n code block with spaces\n",
+ "\nList
\n\n code block with spaces\n
\n \n",
+
+ "1. List\n * Mixted list\n",
+ "\nList\n\n \n \n",
+
+ "1. List\n * Mixed list\n",
+ "\nList\n\n \n \n",
+
+ "* Start with unordered\n 1. Ordered\n",
+ "\nStart with unordered\n\n\nOrdered \n \n \n",
+
+ "* Start with unordered\n 1. Ordered\n",
+ "\nStart with unordered\n\n\nOrdered \n \n \n",
+
+ "1. numbers\n1. are ignored\n",
+ "\nnumbers \nare ignored \n \n",
+ }
+ doTestsBlock(t, tests, 0)
+}
+
+func TestDefinitionList(t *testing.T) {
+ var tests = []string{
+ "Term 1\n: Definition a\n",
+ "\nTerm 1 \nDefinition a \n \n",
+
+ "Term 1\n: Definition a \n",
+ "\nTerm 1 \nDefinition a \n \n",
+
+ "Term 1\n: Definition a\n: Definition b\n",
+ "\nTerm 1 \nDefinition a \nDefinition b \n \n",
+
+ "Term 1\n: Definition a\n\nTerm 2\n: Definition b\n",
+ "\n" +
+ "Term 1 \n" +
+ "Definition a \n" +
+ "Term 2 \n" +
+ "Definition b \n" +
+ " \n",
+
+ "Term 1\n: Definition a\n\nTerm 2\n: Definition b\n\nTerm 3\n: Definition c\n",
+ "\n" +
+ "Term 1 \n" +
+ "Definition a \n" +
+ "Term 2 \n" +
+ "Definition b \n" +
+ "Term 3 \n" +
+ "Definition c \n" +
+ " \n",
+
+ "Term 1\n: Definition a\n: Definition b\n\nTerm 2\n: Definition c\n",
+ "\n" +
+ "Term 1 \n" +
+ "Definition a \n" +
+ "Definition b \n" +
+ "Term 2 \n" +
+ "Definition c \n" +
+ " \n",
+
+ "Term 1\n\n: Definition a\n\nTerm 2\n\n: Definition b\n",
+ "\n" +
+ "Term 1 \n" +
+ "Definition a
\n" +
+ "Term 2 \n" +
+ "Definition b
\n" +
+ " \n",
+
+ "Term 1\n\n: Definition a\n\n: Definition b\n\nTerm 2\n\n: Definition c\n",
+ "\n" +
+ "Term 1 \n" +
+ "Definition a
\n" +
+ "Definition b
\n" +
+ "Term 2 \n" +
+ "Definition c
\n" +
+ " \n",
+
+ "Term 1\n: Definition a\nNext line\n",
+ "\nTerm 1 \nDefinition a\nNext line \n \n",
+
+ "Term 1\n: Definition a\n Next line\n",
+ "\nTerm 1 \nDefinition a\nNext line \n \n",
+
+ "Term 1\n: Definition a \n Next line \n",
+ "\nTerm 1 \nDefinition a\nNext line \n \n",
+
+ "Term 1\n: Definition a\nNext line\n\nTerm 2\n: Definition b",
+ "\n" +
+ "Term 1 \n" +
+ "Definition a\nNext line \n" +
+ "Term 2 \n" +
+ "Definition b \n" +
+ " \n",
+
+ "Term 1\n: Definition a\n",
+ "\nTerm 1 \nDefinition a \n \n",
+
+ "Term 1\n:Definition a\n",
+ "Term 1\n:Definition a
\n",
+
+ "Term 1\n\n: Definition a\n\nTerm 2\n\n: Definition b\n\nText 1",
+ "\n" +
+ "Term 1 \n" +
+ "Definition a
\n" +
+ "Term 2 \n" +
+ "Definition b
\n" +
+ " \n" +
+ "\nText 1
\n",
+
+ "Term 1\n\n: Definition a\n\nText 1\n\nTerm 2\n\n: Definition b\n\nText 2",
+ "\n" +
+ "Term 1 \n" +
+ "Definition a
\n" +
+ " \n" +
+ "\nText 1
\n" +
+ "\n\n" +
+ "Term 2 \n" +
+ "Definition b
\n" +
+ " \n" +
+ "\nText 2
\n",
+ }
+ doTestsBlock(t, tests, DefinitionLists)
+}
+
+func TestPreformattedHtml(t *testing.T) {
+ var tests = []string{
+ "
\n",
+ "
\n",
+
+ "\n
\n",
+ "\n
\n",
+
+ "\n
\nParagraph\n",
+ "
\n
\nParagraph
\n",
+
+ "\n
\n",
+ "\n
\n",
+
+ "\nAnything here\n
\n",
+ "\nAnything here\n
\n",
+
+ "\n Anything here\n
\n",
+ "\n Anything here\n
\n",
+
+ "\nAnything here\n
\n",
+ "\nAnything here\n
\n",
+
+ "\nThis is *not* &proceessed\n
\n",
+ "\nThis is *not* &proceessed\n
\n",
+
+ "\n Something\n \n",
+ "\n Something\n
\n",
+
+ "\n Something here\n\n",
+ "
\n Something here\n\n",
+
+ "Paragraph\n
\nHere? >&<\n
\n",
+ "
Paragraph\n
\nHere? >&<\n
\n",
+
+ "Paragraph\n\n
\nHow about here? >&<\n
\n",
+ "
Paragraph
\n\n
\nHow about here? >&<\n
\n",
+
+ "Paragraph\n
\nHere? >&<\n
\nAnd here?\n",
+ "
Paragraph\n
\nHere? >&<\n
\nAnd here?\n",
+
+ "Paragraph\n\n
\nHow about here? >&<\n
\nAnd here?\n",
+ "
Paragraph
\n\n
\nHow about here? >&<\n
\nAnd here?\n",
+
+ "Paragraph\n
\nHere? >&<\n
\n\nAnd here?\n",
+ "
Paragraph\n
\nHere? >&<\n
\n\n
And here?
\n",
+
+ "Paragraph\n\n
\nHow about here? >&<\n
\n\nAnd here?\n",
+ "
Paragraph
\n\n
\nHow about here? >&<\n
\n\n
And here?
\n",
+ }
+ doTestsBlock(t, tests, 0)
+}
+
+func TestPreformattedHtmlLax(t *testing.T) {
+ var tests = []string{
+ "Paragraph\n
\nHere? >&<\n
\n",
+ "
Paragraph
\n\n
\nHere? >&<\n
\n",
+
+ "Paragraph\n\n
\nHow about here? >&<\n
\n",
+ "
Paragraph
\n\n
\nHow about here? >&<\n
\n",
+
+ "Paragraph\n
\nHere? >&<\n
\nAnd here?\n",
+ "
Paragraph
\n\n
\nHere? >&<\n
\n\n
And here?
\n",
+
+ "Paragraph\n\n
\nHow about here? >&<\n
\nAnd here?\n",
+ "
Paragraph
\n\n
\nHow about here? >&<\n
\n\n
And here?
\n",
+
+ "Paragraph\n
\nHere? >&<\n
\n\nAnd here?\n",
+ "
Paragraph
\n\n
\nHere? >&<\n
\n\n
And here?
\n",
+
+ "Paragraph\n\n
\nHow about here? >&<\n
\n\nAnd here?\n",
+ "
Paragraph
\n\n
\nHow about here? >&<\n
\n\n
And here?
\n",
+ }
+ doTestsBlock(t, tests, LaxHTMLBlocks)
+}
+
+func TestFencedCodeBlock(t *testing.T) {
+ var tests = []string{
+ "``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n",
+ "
func foo() bool {\n\treturn true;\n}\n
\n",
+
+ "``` c\n/* special & char < > \" escaping */\n```\n",
+ "
/* special & char < > " escaping */\n
\n",
+
+ "``` c\nno *inline* processing ~~of text~~\n```\n",
+ "
no *inline* processing ~~of text~~\n
\n",
+
+ "```\nNo language\n```\n",
+ "
No language\n
\n",
+
+ "``` {ocaml}\nlanguage in braces\n```\n",
+ "
language in braces\n
\n",
+
+ "``` {ocaml} \nwith extra whitespace\n```\n",
+ "
with extra whitespace\n
\n",
+
+ "```{ ocaml }\nwith extra whitespace\n```\n",
+ "
with extra whitespace\n
\n",
+
+ "~ ~~ java\nWith whitespace\n~~~\n",
+ "
~ ~~ java\nWith whitespace\n~~~
\n",
+
+ "~~\nonly two\n~~\n",
+ "
~~\nonly two\n~~
\n",
+
+ "```` python\nextra\n````\n",
+ "
extra\n
\n",
+
+ "~~~ perl\nthree to start, four to end\n~~~~\n",
+ "
~~~ perl\nthree to start, four to end\n~~~~
\n",
+
+ "~~~~ perl\nfour to start, three to end\n~~~\n",
+ "
~~~~ perl\nfour to start, three to end\n~~~
\n",
+
+ "~~~ bash\ntildes\n~~~\n",
+ "
tildes\n
\n",
+
+ "``` lisp\nno ending\n",
+ "
``` lisp\nno ending
\n",
+
+ "~~~ lisp\nend with language\n~~~ lisp\n",
+ "
~~~ lisp\nend with language\n~~~ lisp
\n",
+
+ "```\nmismatched begin and end\n~~~\n",
+ "
```\nmismatched begin and end\n~~~
\n",
+
+ "~~~\nmismatched begin and end\n```\n",
+ "
~~~\nmismatched begin and end\n```
\n",
+
+ " ``` oz\nleading spaces\n```\n",
+ "
leading spaces\n
\n",
+
+ " ``` oz\nleading spaces\n ```\n",
+ "
leading spaces\n
\n",
+
+ " ``` oz\nleading spaces\n ```\n",
+ "
leading spaces\n
\n",
+
+ "``` oz\nleading spaces\n ```\n",
+ "
leading spaces\n
\n",
+
+ " ``` oz\nleading spaces\n ```\n",
+ "
``` oz\n
\n\n
leading spaces\n ```
\n",
+
+ "Bla bla\n\n``` oz\ncode blocks breakup paragraphs\n```\n\nBla Bla\n",
+ "
Bla bla
\n\n
code blocks breakup paragraphs\n
\n\n
Bla Bla
\n",
+
+ "Some text before a fenced code block\n``` oz\ncode blocks breakup paragraphs\n```\nAnd some text after a fenced code block",
+ "
Some text before a fenced code block
\n\n
code blocks breakup paragraphs\n
\n\n
And some text after a fenced code block
\n",
+
+ "`",
+ "
`
\n",
+
+ "Bla bla\n\n``` oz\ncode blocks breakup paragraphs\n```\n\nBla Bla\n\n``` oz\nmultiple code blocks work okay\n```\n\nBla Bla\n",
+ "
Bla bla
\n\n
code blocks breakup paragraphs\n
\n\n
Bla Bla
\n\n
multiple code blocks work okay\n
\n\n
Bla Bla
\n",
+
+ "Some text before a fenced code block\n``` oz\ncode blocks breakup paragraphs\n```\nSome text in between\n``` oz\nmultiple code blocks work okay\n```\nAnd some text after a fenced code block",
+ "
Some text before a fenced code block
\n\n
code blocks breakup paragraphs\n
\n\n
Some text in between
\n\n
multiple code blocks work okay\n
\n\n
And some text after a fenced code block
\n",
+
+ "```\n[]:()\n```\n",
+ "
[]:()\n
\n",
+
+ "```\n[]:()\n[]:)\n[]:(\n[]:x\n[]:testing\n[:testing\n\n[]:\nlinebreak\n[]()\n\n[]:\n[]()\n```",
+ "
[]:()\n[]:)\n[]:(\n[]:x\n[]:testing\n[:testing\n\n[]:\nlinebreak\n[]()\n\n[]:\n[]()\n
\n",
+ }
+ doTestsBlock(t, tests, FencedCode)
+}
+
+func TestFencedCodeInsideBlockquotes(t *testing.T) {
+ cat := func(s ...string) string { return strings.Join(s, "\n") }
+ var tests = []string{
+ cat("> ```go",
+ "package moo",
+ "",
+ "```",
+ ""),
+ `
+package moo
+
+
+
+`,
+ // -------------------------------------------
+ cat("> foo",
+ "> ",
+ "> ```go",
+ "package moo",
+ "```",
+ "> ",
+ "> goo.",
+ ""),
+ `
+foo
+
+package moo
+
+
+goo.
+
+`,
+ // -------------------------------------------
+ cat("> foo",
+ "> ",
+ "> quote",
+ "continues",
+ "```",
+ ""),
+ `
+foo
+
+quote
+continues
+` + "```" + `
+
+`,
+ // -------------------------------------------
+ cat("> foo",
+ "> ",
+ "> ```go",
+ "package moo",
+ "```",
+ "> ",
+ "> goo.",
+ "> ",
+ "> ```go",
+ "package zoo",
+ "```",
+ "> ",
+ "> woo.",
+ ""),
+ `
+foo
+
+package moo
+
+
+goo.
+
+package zoo
+
+
+woo.
+
+`,
+ }
+
+ // These 2 alternative forms of blockquoted fenced code blocks should produce same output.
+ forms := [2]string{
+ cat("> plain quoted text",
+ "> ```fenced",
+ "code",
+ " with leading single space correctly preserved",
+ "okay",
+ "```",
+ "> rest of quoted text"),
+ cat("> plain quoted text",
+ "> ```fenced",
+ "> code",
+ "> with leading single space correctly preserved",
+ "> okay",
+ "> ```",
+ "> rest of quoted text"),
+ }
+ want := `
+plain quoted text
+
+code
+ with leading single space correctly preserved
+okay
+
+
+rest of quoted text
+
+`
+ tests = append(tests, forms[0], want)
+ tests = append(tests, forms[1], want)
+
+ doTestsBlock(t, tests, FencedCode)
+}
+
+func TestTable(t *testing.T) {
+ var tests = []string{
+ "a | b\n---|---\nc | d\n",
+ "
\n\n\na \nb \n \n \n\n" +
+ "\n\nc \nd \n \n \n
\n",
+
+ "a | b\n---|--\nc | d\n",
+ "
a | b\n---|--\nc | d
\n",
+
+ "|a|b|c|d|\n|----|----|----|---|\n|e|f|g|h|\n",
+ "
\n\n\na \nb \nc \nd \n \n \n\n" +
+ "\n\ne \nf \ng \nh \n \n \n
\n",
+
+ "*a*|__b__|[c](C)|d\n---|---|---|---\ne|f|g|h\n",
+ "
\n\n\na \nb \nc \nd \n \n \n\n" +
+ "\n\ne \nf \ng \nh \n \n \n
\n",
+
+ "a|b|c\n---|---|---\nd|e|f\ng|h\ni|j|k|l|m\nn|o|p\n",
+ "
\n\n\na \nb \nc \n \n \n\n" +
+ "\n\nd \ne \nf \n \n\n" +
+ "\ng \nh \n \n \n\n" +
+ "\ni \nj \nk \n \n\n" +
+ "\nn \no \np \n \n \n
\n",
+
+ "a|b|c\n---|---|---\n*d*|__e__|f\n",
+ "
\n\n\na \nb \nc \n \n \n\n" +
+ "\n\nd \ne \nf \n \n \n
\n",
+
+ "a|b|c|d\n:--|--:|:-:|---\ne|f|g|h\n",
+ "
\n\n\na \nb \n" +
+ "c \nd \n \n \n\n" +
+ "\n\ne \nf \n" +
+ "g \nh \n \n \n
\n",
+
+ "a|b|c\n---|---|---\n",
+ "
\n\n\na \nb \nc \n \n \n\n\n \n
\n",
+
+ "a| b|c | d | e\n---|---|---|---|---\nf| g|h | i |j\n",
+ "
\n\n\na \nb \nc \nd \ne \n \n \n\n" +
+ "\n\nf \ng \nh \ni \nj \n \n \n
\n",
+
+ "a|b\\|c|d\n---|---|---\nf|g\\|h|i\n",
+ "
\n\n\na \nb|c \nd \n \n \n\n\n\nf \ng|h \ni \n \n \n
\n",
+ }
+ doTestsBlock(t, tests, Tables)
+}
+
+func TestUnorderedListWith_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) {
+ var tests = []string{
+ "* Hello\n",
+ "
\n",
+
+ "* Yin\n* Yang\n",
+ "
\n",
+
+ "* Ting\n* Bong\n* Goo\n",
+ "
\n",
+
+ "* Yin\n\n* Yang\n",
+ "
\n",
+
+ "* Ting\n\n* Bong\n* Goo\n",
+ "
\n",
+
+ "+ Hello\n",
+ "
\n",
+
+ "+ Yin\n+ Yang\n",
+ "
\n",
+
+ "+ Ting\n+ Bong\n+ Goo\n",
+ "
\n",
+
+ "+ Yin\n\n+ Yang\n",
+ "
\n",
+
+ "+ Ting\n\n+ Bong\n+ Goo\n",
+ "
\n",
+
+ "- Hello\n",
+ "
\n",
+
+ "- Yin\n- Yang\n",
+ "
\n",
+
+ "- Ting\n- Bong\n- Goo\n",
+ "
\n",
+
+ "- Yin\n\n- Yang\n",
+ "
\n",
+
+ "- Ting\n\n- Bong\n- Goo\n",
+ "
\n",
+
+ "*Hello\n",
+ "
*Hello
\n",
+
+ "* Hello \n",
+ "
\n",
+
+ "* Hello \n Next line \n",
+ "
\n",
+
+ "Paragraph\n* No linebreak\n",
+ "
Paragraph
\n\n
\n",
+
+ "Paragraph\n\n* Linebreak\n",
+ "
Paragraph
\n\n
\n",
+
+ "* List\n * Nested list\n",
+ "
\n",
+
+ "* List\n\n * Nested list\n",
+ "
\n",
+
+ "* List\n Second line\n\n + Nested\n",
+ "
\nList\nSecond line
\n\n \n \n",
+
+ "* List\n + Nested\n\n Continued\n",
+ "
\nList
\n\n\n\nContinued
\n \n",
+
+ "* List\n * shallow indent\n",
+ "
\n",
+
+ "* List\n" +
+ " * shallow indent\n" +
+ " * part of second list\n" +
+ " * still second\n" +
+ " * almost there\n" +
+ " * third level\n",
+ "
\n" +
+ "List\n\n" +
+ "\n" +
+ "shallow indent \n" +
+ "part of second list \n" +
+ "still second \n" +
+ "almost there\n\n" +
+ "\n" +
+ "third level \n" +
+ " \n" +
+ " \n" +
+ " \n",
+
+ "* List\n extra indent, same paragraph\n",
+ "
\nList\n extra indent, same paragraph \n \n",
+
+ "* List\n\n code block\n",
+ "
\n",
+
+ "* List\n\n code block with spaces\n",
+ "
\nList
\n\n code block with spaces\n
\n \n",
+
+ "* List\n\n * sublist\n\n normal text\n\n * another sublist\n",
+ "
\nList
\n\n\n\nnormal text
\n\n \n \n",
+ }
+ doTestsBlock(t, tests, NoEmptyLineBeforeBlock)
+}
+
+func TestOrderedList_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) {
+ var tests = []string{
+ "1. Hello\n",
+ "
\nHello \n \n",
+
+ "1. Yin\n2. Yang\n",
+ "
\nYin \nYang \n \n",
+
+ "1. Ting\n2. Bong\n3. Goo\n",
+ "
\nTing \nBong \nGoo \n \n",
+
+ "1. Yin\n\n2. Yang\n",
+ "
\nYin
\n\nYang
\n \n",
+
+ "1. Ting\n\n2. Bong\n3. Goo\n",
+ "
\nTing
\n\nBong
\n\nGoo
\n \n",
+
+ "1 Hello\n",
+ "
1 Hello
\n",
+
+ "1.Hello\n",
+ "
1.Hello
\n",
+
+ "1. Hello \n",
+ "
\nHello \n \n",
+
+ "1. Hello \n Next line \n",
+ "
\nHello\nNext line \n \n",
+
+ "Paragraph\n1. No linebreak\n",
+ "
Paragraph
\n\n
\nNo linebreak \n \n",
+
+ "Paragraph\n\n1. Linebreak\n",
+ "
Paragraph
\n\n
\nLinebreak \n \n",
+
+ "1. List\n 1. Nested list\n",
+ "
\nList\n\n\nNested list \n \n \n",
+
+ "1. List\n\n 1. Nested list\n",
+ "
\nList
\n\n\nNested list \n \n \n",
+
+ "1. List\n Second line\n\n 1. Nested\n",
+ "
\nList\nSecond line
\n\n\nNested \n \n \n",
+
+ "1. List\n 1. Nested\n\n Continued\n",
+ "
\nList
\n\n\nNested \n \n\nContinued
\n \n",
+
+ "1. List\n 1. shallow indent\n",
+ "
\nList\n\n\nshallow indent \n \n \n",
+
+ "1. List\n" +
+ " 1. shallow indent\n" +
+ " 2. part of second list\n" +
+ " 3. still second\n" +
+ " 4. almost there\n" +
+ " 1. third level\n",
+ "
\n" +
+ "List\n\n" +
+ "\n" +
+ "shallow indent \n" +
+ "part of second list \n" +
+ "still second \n" +
+ "almost there\n\n" +
+ "\n" +
+ "third level \n" +
+ " \n" +
+ " \n" +
+ " \n",
+
+ "1. List\n extra indent, same paragraph\n",
+ "
\nList\n extra indent, same paragraph \n \n",
+
+ "1. List\n\n code block\n",
+ "
\nList
\n\ncode block\n
\n \n",
+
+ "1. List\n\n code block with spaces\n",
+ "
\nList
\n\n code block with spaces\n
\n \n",
+
+ "1. List\n * Mixted list\n",
+ "
\nList\n\n \n \n",
+
+ "1. List\n * Mixed list\n",
+ "
\nList\n\n \n \n",
+
+ "* Start with unordered\n 1. Ordered\n",
+ "
\nStart with unordered\n\n\nOrdered \n \n \n",
+
+ "* Start with unordered\n 1. Ordered\n",
+ "
\nStart with unordered\n\n\nOrdered \n \n \n",
+
+ "1. numbers\n1. are ignored\n",
+ "
\nnumbers \nare ignored \n \n",
+ }
+ doTestsBlock(t, tests, NoEmptyLineBeforeBlock)
+}
+
+func TestFencedCodeBlock_EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK(t *testing.T) {
+ var tests = []string{
+ "``` go\nfunc foo() bool {\n\treturn true;\n}\n```\n",
+ "
func foo() bool {\n\treturn true;\n}\n
\n",
+
+ "``` c\n/* special & char < > \" escaping */\n```\n",
+ "
/* special & char < > " escaping */\n
\n",
+
+ "``` c\nno *inline* processing ~~of text~~\n```\n",
+ "
no *inline* processing ~~of text~~\n
\n",
+
+ "```\nNo language\n```\n",
+ "
No language\n
\n",
+
+ "``` {ocaml}\nlanguage in braces\n```\n",
+ "
language in braces\n
\n",
+
+ "``` {ocaml} \nwith extra whitespace\n```\n",
+ "
with extra whitespace\n
\n",
+
+ "```{ ocaml }\nwith extra whitespace\n```\n",
+ "
with extra whitespace\n
\n",
+
+ "~ ~~ java\nWith whitespace\n~~~\n",
+ "
~ ~~ java\nWith whitespace\n~~~
\n",
+
+ "~~\nonly two\n~~\n",
+ "
~~\nonly two\n~~
\n",
+
+ "```` python\nextra\n````\n",
+ "
extra\n
\n",
+
+ "~~~ perl\nthree to start, four to end\n~~~~\n",
+ "
~~~ perl\nthree to start, four to end\n~~~~
\n",
+
+ "~~~~ perl\nfour to start, three to end\n~~~\n",
+ "
~~~~ perl\nfour to start, three to end\n~~~
\n",
+
+ "~~~ bash\ntildes\n~~~\n",
+ "
tildes\n
\n",
+
+ "``` lisp\nno ending\n",
+ "
``` lisp\nno ending
\n",
+
+ "~~~ lisp\nend with language\n~~~ lisp\n",
+ "
~~~ lisp\nend with language\n~~~ lisp
\n",
+
+ "```\nmismatched begin and end\n~~~\n",
+ "
```\nmismatched begin and end\n~~~
\n",
+
+ "~~~\nmismatched begin and end\n```\n",
+ "
~~~\nmismatched begin and end\n```
\n",
+
+ " ``` oz\nleading spaces\n```\n",
+ "
leading spaces\n
\n",
+
+ " ``` oz\nleading spaces\n ```\n",
+ "
leading spaces\n
\n",
+
+ " ``` oz\nleading spaces\n ```\n",
+ "
leading spaces\n
\n",
+
+ "``` oz\nleading spaces\n ```\n",
+ "
leading spaces\n
\n",
+
+ " ``` oz\nleading spaces\n ```\n",
+ "
``` oz\n
\n\n
leading spaces
\n\n
```\n
\n",
+ }
+ doTestsBlock(t, tests, FencedCode|NoEmptyLineBeforeBlock)
+}
+
+func TestTitleBlock_EXTENSION_TITLEBLOCK(t *testing.T) {
+ var tests = []string{
+ "% Some title\n" +
+ "% Another title line\n" +
+ "% Yep, more here too\n",
+ "
" +
+ "Some title\n" +
+ "Another title line\n" +
+ "Yep, more here too" +
+ " \n",
+ }
+ doTestsBlock(t, tests, Titleblock)
+}
+
+func TestBlockComments(t *testing.T) {
+ var tests = []string{
+ "Some text\n\n\n",
+ "
Some text
\n\n\n",
+
+ "Some text\n\n\n",
+ "
Some text
\n\n\n",
+
+ "Some text\n\n\n",
+ "
Some text
\n\n\n",
+ }
+ doTestsBlock(t, tests, 0)
+}
+
+func TestTOC(t *testing.T) {
+ var tests = []string{
+ "# Title\n\n##Subtitle1\n\n##Subtitle2",
+ //"
\n\n \n\n
Title \n\n
Subtitle1 \n\n
Subtitle2 \n",
+ `
+
+
+
+
+
+
Title
+
+
Subtitle1
+
+
Subtitle2
+`,
+
+ "# Title\n\n##Subtitle\n\n#Title2",
+ //"
\n\n \n\n
Title \n\n
Subtitle \n\n
Title2 \n",
+ `
+
+
+
+
+
+
Title
+
+
Subtitle
+
+
Title2
+`,
+
+ "## Subtitle\n\n# Title",
+ `
+
+
+
+
+
+
Subtitle
+
+
Title
+`,
+
+ "# Title 1\n\n## Subtitle 1\n\n### Subsubtitle 1\n\n# Title 2\n\n### Subsubtitle 2",
+ `
+
+
+
+
+
+
Title 1
+
+
Subtitle 1
+
+
Subsubtitle 1
+
+
Title 2
+
+
Subsubtitle 2
+`,
+
+ "# Title with `code`",
+ `
+
+
+
+
+
+
Title with code
+`,
+
+ // Trigger empty TOC
+ "#",
+ "",
+ }
+ doTestsParam(t, tests, TestParams{
+ HTMLFlags: UseXHTML | TOC,
+ })
+}
+
+func TestCompletePage(t *testing.T) {
+ var tests = []string{
+ "*foo*",
+ `
+
+
+
+
+
+
+
+
+
foo
+
+
+
+`,
+ }
+ doTestsParam(t, tests, TestParams{HTMLFlags: UseXHTML | CompletePage})
+}
+
+func TestIsFenceLine(t *testing.T) {
+ tests := []struct {
+ data []byte
+ syntaxRequested bool
+ wantEnd int
+ wantMarker string
+ wantSyntax string
+ }{
+ {
+ data: []byte("```"),
+ wantEnd: 3,
+ wantMarker: "```",
+ },
+ {
+ data: []byte("```\nstuff here\n"),
+ wantEnd: 4,
+ wantMarker: "```",
+ },
+ {
+ data: []byte("```\nstuff here\n"),
+ syntaxRequested: true,
+ wantEnd: 4,
+ wantMarker: "```",
+ },
+ {
+ data: []byte("stuff here\n```\n"),
+ wantEnd: 0,
+ },
+ {
+ data: []byte("```"),
+ syntaxRequested: true,
+ wantEnd: 3,
+ wantMarker: "```",
+ },
+ {
+ data: []byte("``` go"),
+ syntaxRequested: true,
+ wantEnd: 6,
+ wantMarker: "```",
+ wantSyntax: "go",
+ },
+ }
+
+ for _, test := range tests {
+ var syntax *string
+ if test.syntaxRequested {
+ syntax = new(string)
+ }
+ end, marker := isFenceLine(test.data, syntax, "```")
+ if got, want := end, test.wantEnd; got != want {
+ t.Errorf("got end %v, want %v", got, want)
+ }
+ if got, want := marker, test.wantMarker; got != want {
+ t.Errorf("got marker %q, want %q", got, want)
+ }
+ if test.syntaxRequested {
+ if got, want := *syntax, test.wantSyntax; got != want {
+ t.Errorf("got syntax %q, want %q", got, want)
+ }
+ }
+ }
+}
diff --git a/pkg/blackfriday/doc.go b/pkg/blackfriday/doc.go
new file mode 100644
index 00000000..5b3fa987
--- /dev/null
+++ b/pkg/blackfriday/doc.go
@@ -0,0 +1,18 @@
+// Package blackfriday is a markdown processor.
+//
+// It translates plain text with simple formatting rules into an AST, which can
+// then be further processed to HTML (provided by Blackfriday itself) or other
+// formats (provided by the community).
+//
+// The simplest way to invoke Blackfriday is to call the Run function. It will
+// take a text input and produce a text output in HTML (or other format).
+//
+// A slightly more sophisticated way to use Blackfriday is to create a Markdown
+// processor and to call Parse, which returns a syntax tree for the input
+// document. You can leverage Blackfriday's parsing for content extraction from
+// markdown documents. You can assign a custom renderer and set various options
+// to the Markdown processor.
+//
+// If you're interested in calling Blackfriday from command line, see
+// https://github.com/russross/blackfriday-tool.
+package blackfriday
diff --git a/pkg/blackfriday/esc.go b/pkg/blackfriday/esc.go
new file mode 100644
index 00000000..6385f27c
--- /dev/null
+++ b/pkg/blackfriday/esc.go
@@ -0,0 +1,34 @@
+package blackfriday
+
+import (
+ "html"
+ "io"
+)
+
+var htmlEscaper = [256][]byte{
+ '&': []byte("&"),
+ '<': []byte("<"),
+ '>': []byte(">"),
+ '"': []byte("""),
+}
+
+func escapeHTML(w io.Writer, s []byte) {
+ var start, end int
+ for end < len(s) {
+ escSeq := htmlEscaper[s[end]]
+ if escSeq != nil {
+ w.Write(s[start:end])
+ w.Write(escSeq)
+ start = end + 1
+ }
+ end++
+ }
+ if start < len(s) && end <= len(s) {
+ w.Write(s[start:end])
+ }
+}
+
+func escLink(w io.Writer, text []byte) {
+ unesc := html.UnescapeString(string(text))
+ escapeHTML(w, []byte(unesc))
+}
diff --git a/pkg/blackfriday/esc_test.go b/pkg/blackfriday/esc_test.go
new file mode 100644
index 00000000..ff67d546
--- /dev/null
+++ b/pkg/blackfriday/esc_test.go
@@ -0,0 +1,48 @@
+package blackfriday
+
+import (
+ "bytes"
+ "testing"
+)
+
+func TestEsc(t *testing.T) {
+ tests := []string{
+ "abc", "abc",
+ "a&c", "a&c",
+ "<", "<",
+ "[]:<", "[]:<",
+ "Hello |"
+ processingInstruction = "[<][?].*?[?][>]"
+ singleQuotedValue = "'[^']*'"
+ tagName = "[A-Za-z][A-Za-z0-9-]*"
+ unquotedValue = "[^\"'=<>`\\x00-\\x20]+"
+)
+
+// HTMLRendererParameters is a collection of supplementary parameters tweaking
+// the behavior of various parts of HTML renderer.
+type HTMLRendererParameters struct {
+ // Prepend this text to each relative URL.
+ AbsolutePrefix string
+ // Add this text to each footnote anchor, to ensure uniqueness.
+ FootnoteAnchorPrefix string
+ // Show this text inside the
tag for a footnote return link, if the
+ // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
+ // [return] is used.
+ FootnoteReturnLinkContents string
+ // If set, add this text to the front of each Heading ID, to ensure
+ // uniqueness.
+ HeadingIDPrefix string
+ // If set, add this text to the back of each Heading ID, to ensure uniqueness.
+ HeadingIDSuffix string
+
+ Title string // Document title (used if CompletePage is set)
+ CSS string // Optional CSS file URL (used if CompletePage is set)
+ Icon string // Optional icon file URL (used if CompletePage is set)
+
+ Flags HTMLFlags // Flags allow customizing this renderer's behavior
+}
+
+// HTMLRenderer is a type that implements the Renderer interface for HTML output.
+//
+// Do not create this directly, instead use the NewHTMLRenderer function.
+type HTMLRenderer struct {
+ HTMLRendererParameters
+
+ closeTag string // how to end singleton tags: either " />" or ">"
+
+ // Track heading IDs to prevent ID collision in a single generation.
+ headingIDs map[string]int
+
+ lastOutputLen int
+ disableTags int
+
+ sr *SPRenderer
+}
+
+const (
+ xhtmlClose = " />"
+ htmlClose = ">"
+)
+
+// NewHTMLRenderer creates and configures an HTMLRenderer object, which
+// satisfies the Renderer interface.
+func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
+ // configure the rendering engine
+ closeTag := htmlClose
+ if params.Flags&UseXHTML != 0 {
+ closeTag = xhtmlClose
+ }
+
+ if params.FootnoteReturnLinkContents == "" {
+ params.FootnoteReturnLinkContents = `[return] `
+ }
+
+ return &HTMLRenderer{
+ HTMLRendererParameters: params,
+
+ closeTag: closeTag,
+ headingIDs: make(map[string]int),
+
+ sr: NewSmartypantsRenderer(params.Flags),
+ }
+}
+
+func isHTMLTag(tag []byte, tagname string) bool {
+ found, _ := findHTMLTagPos(tag, tagname)
+ return found
+}
+
+// Look for a character, but ignore it when it's in any kind of quotes, it
+// might be JavaScript
+func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
+ inSingleQuote := false
+ inDoubleQuote := false
+ inGraveQuote := false
+ i := start
+ for i < len(html) {
+ switch {
+ case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
+ return i
+ case html[i] == '\'':
+ inSingleQuote = !inSingleQuote
+ case html[i] == '"':
+ inDoubleQuote = !inDoubleQuote
+ case html[i] == '`':
+ inGraveQuote = !inGraveQuote
+ }
+ i++
+ }
+ return start
+}
+
+func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
+ i := 0
+ if i < len(tag) && tag[0] != '<' {
+ return false, -1
+ }
+ i++
+ i = skipSpace(tag, i)
+
+ if i < len(tag) && tag[i] == '/' {
+ i++
+ }
+
+ i = skipSpace(tag, i)
+ j := 0
+ for ; i < len(tag); i, j = i+1, j+1 {
+ if j >= len(tagname) {
+ break
+ }
+
+ if strings.ToLower(string(tag[i]))[0] != tagname[j] {
+ return false, -1
+ }
+ }
+
+ if i == len(tag) {
+ return false, -1
+ }
+
+ rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
+ if rightAngle >= i {
+ return true, rightAngle
+ }
+
+ return false, -1
+}
+
+func skipSpace(tag []byte, i int) int {
+ for i < len(tag) && isspace(tag[i]) {
+ i++
+ }
+ return i
+}
+
+func isRelativeLink(link []byte) (yes bool) {
+ // a tag begin with '#'
+ if link[0] == '#' {
+ return true
+ }
+
+ // link begin with '/' but not '//', the second maybe a protocol relative link
+ if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
+ return true
+ }
+
+ // only the root '/'
+ if len(link) == 1 && link[0] == '/' {
+ return true
+ }
+
+ // current directory : begin with "./"
+ if bytes.HasPrefix(link, []byte("./")) {
+ return true
+ }
+
+ // parent directory : begin with "../"
+ if bytes.HasPrefix(link, []byte("../")) {
+ return true
+ }
+
+ return false
+}
+
+func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string {
+ for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
+ tmp := fmt.Sprintf("%s-%d", id, count+1)
+
+ if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
+ r.headingIDs[id] = count + 1
+ id = tmp
+ } else {
+ id = id + "-1"
+ }
+ }
+
+ if _, found := r.headingIDs[id]; !found {
+ r.headingIDs[id] = 0
+ }
+
+ return id
+}
+
+func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
+ if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
+ newDest := r.AbsolutePrefix
+ if link[0] != '/' {
+ newDest += "/"
+ }
+ newDest += string(link)
+ return []byte(newDest)
+ }
+ return link
+}
+
+func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
+ if isRelativeLink(link) {
+ return attrs
+ }
+ val := []string{}
+ if flags&NofollowLinks != 0 {
+ val = append(val, "nofollow")
+ }
+ if flags&NoreferrerLinks != 0 {
+ val = append(val, "noreferrer")
+ }
+ if flags&HrefTargetBlank != 0 {
+ attrs = append(attrs, "target=\"_blank\"")
+ }
+ if len(val) == 0 {
+ return attrs
+ }
+ attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
+ return append(attrs, attr)
+}
+
+func isMailto(link []byte) bool {
+ return bytes.HasPrefix(link, []byte("mailto:"))
+}
+
+func needSkipLink(flags HTMLFlags, dest []byte) bool {
+ if flags&SkipLinks != 0 {
+ return true
+ }
+ return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
+}
+
+func isSmartypantable(node *Node) bool {
+ pt := node.Parent.Type
+ return pt != Link && pt != CodeBlock && pt != Code
+}
+
+func appendLanguageAttr(attrs []string, info []byte) []string {
+ if len(info) == 0 {
+ return attrs
+ }
+ endOfLang := bytes.IndexAny(info, "\t ")
+ if endOfLang < 0 {
+ endOfLang = len(info)
+ }
+ return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang]))
+}
+
+func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) {
+ w.Write(name)
+ if len(attrs) > 0 {
+ w.Write(spaceBytes)
+ w.Write([]byte(strings.Join(attrs, " ")))
+ }
+ w.Write(gtBytes)
+ r.lastOutputLen = 1
+}
+
+func footnoteRef(prefix string, node *Node) []byte {
+ urlFrag := prefix + string(slugify(node.Destination))
+ anchor := fmt.Sprintf(` %d `, urlFrag, node.NoteID)
+ return []byte(fmt.Sprintf(``, urlFrag, anchor))
+}
+
+func footnoteItem(prefix string, slug []byte) []byte {
+ return []byte(fmt.Sprintf(`
`, prefix, slug))
+}
+
+func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
+ const format = ` `
+ return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
+}
+
+func itemOpenCR(node *Node) bool {
+ if node.Prev == nil {
+ return false
+ }
+ ld := node.Parent.ListData
+ return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
+}
+
+func skipParagraphTags(node *Node) bool {
+ grandparent := node.Parent.Parent
+ if grandparent == nil || grandparent.Type != List {
+ return false
+ }
+ tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
+ return grandparent.Type == List && tightOrTerm
+}
+
+func cellAlignment(align CellAlignFlags) string {
+ switch align {
+ case TableAlignmentLeft:
+ return "left"
+ case TableAlignmentRight:
+ return "right"
+ case TableAlignmentCenter:
+ return "center"
+ default:
+ return ""
+ }
+}
+
+func (r *HTMLRenderer) out(w io.Writer, text []byte) {
+ if r.disableTags > 0 {
+ w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
+ } else {
+ w.Write(text)
+ }
+ r.lastOutputLen = len(text)
+}
+
+func (r *HTMLRenderer) cr(w io.Writer) {
+ if r.lastOutputLen > 0 {
+ r.out(w, nlBytes)
+ }
+}
+
+var (
+ nlBytes = []byte{'\n'}
+ gtBytes = []byte{'>'}
+ spaceBytes = []byte{' '}
+)
+
+var (
+ brTag = []byte(" ")
+ brXHTMLTag = []byte(" ")
+ emTag = []byte("")
+ emCloseTag = []byte(" ")
+ strongTag = []byte("")
+ strongCloseTag = []byte(" ")
+ delTag = []byte("")
+ delCloseTag = []byte("")
+ ttTag = []byte("")
+ ttCloseTag = []byte(" ")
+ aTag = []byte("")
+ preTag = []byte("")
+ preCloseTag = []byte(" ")
+ codeTag = []byte("")
+ codeCloseTag = []byte("
")
+ pTag = []byte("")
+ pCloseTag = []byte("
")
+ blockquoteTag = []byte("")
+ blockquoteCloseTag = []byte(" ")
+ hrTag = []byte(" ")
+ hrXHTMLTag = []byte(" ")
+ ulTag = []byte("")
+ ulCloseTag = []byte(" ")
+ olTag = []byte("")
+ olCloseTag = []byte(" ")
+ dlTag = []byte("")
+ dlCloseTag = []byte(" ")
+ liTag = []byte("")
+ liCloseTag = []byte(" ")
+ ddTag = []byte("")
+ ddCloseTag = []byte(" ")
+ dtTag = []byte("")
+ dtCloseTag = []byte(" ")
+ tableTag = []byte(" ")
+ tableCloseTag = []byte("
")
+ tdTag = []byte("")
+ thTag = []byte(" ")
+ theadTag = []byte("")
+ theadCloseTag = []byte(" ")
+ tbodyTag = []byte(" ")
+ tbodyCloseTag = []byte(" ")
+ trTag = []byte("")
+ trCloseTag = []byte(" ")
+ h1Tag = []byte("")
+ h2Tag = []byte("")
+ h3Tag = []byte("")
+ h4Tag = []byte("")
+ h5Tag = []byte("")
+ h6Tag = []byte("")
+
+ footnotesDivBytes = []byte("\n\n")
+)
+
+func headingTagsFromLevel(level int) ([]byte, []byte) {
+ switch level {
+ case 1:
+ return h1Tag, h1CloseTag
+ case 2:
+ return h2Tag, h2CloseTag
+ case 3:
+ return h3Tag, h3CloseTag
+ case 4:
+ return h4Tag, h4CloseTag
+ case 5:
+ return h5Tag, h5CloseTag
+ default:
+ return h6Tag, h6CloseTag
+ }
+}
+
+func (r *HTMLRenderer) outHRTag(w io.Writer) {
+ if r.Flags&UseXHTML == 0 {
+ r.out(w, hrTag)
+ } else {
+ r.out(w, hrXHTMLTag)
+ }
+}
+
+// RenderNode is a default renderer of a single node of a syntax tree. For
+// block nodes it will be called twice: first time with entering=true, second
+// time with entering=false, so that it could know when it's working on an open
+// tag and when on close. It writes the result to w.
+//
+// The return value is a way to tell the calling walker to adjust its walk
+// pattern: e.g. it can terminate the traversal by returning Terminate. Or it
+// can ask the walker to skip a subtree of this node by returning SkipChildren.
+// The typical behavior is to return GoToNext, which asks for the usual
+// traversal to the next node.
+func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
+ attrs := []string{}
+ switch node.Type {
+ case Text:
+ if r.Flags&Smartypants != 0 {
+ var tmp bytes.Buffer
+ escapeHTML(&tmp, node.Literal)
+ r.sr.Process(w, tmp.Bytes())
+ } else {
+ if node.Parent.Type == Link {
+ escLink(w, node.Literal)
+ } else {
+ escapeHTML(w, node.Literal)
+ }
+ }
+ case Softbreak:
+ r.cr(w)
+ // TODO: make it configurable via out(renderer.softbreak)
+ case Hardbreak:
+ if r.Flags&UseXHTML == 0 {
+ r.out(w, brTag)
+ } else {
+ r.out(w, brXHTMLTag)
+ }
+ r.cr(w)
+ case Emph:
+ if entering {
+ r.out(w, emTag)
+ } else {
+ r.out(w, emCloseTag)
+ }
+ case Strong:
+ if entering {
+ r.out(w, strongTag)
+ } else {
+ r.out(w, strongCloseTag)
+ }
+ case Del:
+ if entering {
+ r.out(w, delTag)
+ } else {
+ r.out(w, delCloseTag)
+ }
+ case HTMLSpan:
+ if r.Flags&SkipHTML != 0 {
+ break
+ }
+ r.out(w, node.Literal)
+ case Link:
+ // mark it but don't link it if it is not a safe link: no smartypants
+ dest := node.LinkData.Destination
+ if needSkipLink(r.Flags, dest) {
+ if entering {
+ r.out(w, ttTag)
+ } else {
+ r.out(w, ttCloseTag)
+ }
+ } else {
+ if entering {
+ dest = r.addAbsPrefix(dest)
+ var hrefBuf bytes.Buffer
+ hrefBuf.WriteString("href=\"")
+ escLink(&hrefBuf, dest)
+ hrefBuf.WriteByte('"')
+ attrs = append(attrs, hrefBuf.String())
+ if node.NoteID != 0 {
+ r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
+ break
+ }
+ attrs = appendLinkAttrs(attrs, r.Flags, dest)
+ if len(node.LinkData.Title) > 0 {
+ var titleBuff bytes.Buffer
+ titleBuff.WriteString("title=\"")
+ escapeHTML(&titleBuff, node.LinkData.Title)
+ titleBuff.WriteByte('"')
+ attrs = append(attrs, titleBuff.String())
+ }
+ r.tag(w, aTag, attrs)
+ } else {
+ if node.NoteID != 0 {
+ break
+ }
+ r.out(w, aCloseTag)
+ }
+ }
+ case Image:
+ if r.Flags&SkipImages != 0 {
+ return SkipChildren
+ }
+ if entering {
+ dest := node.LinkData.Destination
+ dest = r.addAbsPrefix(dest)
+ if r.disableTags == 0 {
+ //if options.safe && potentiallyUnsafe(dest) {
+ //out(w, ` `))
+ }
+ }
+ case Code:
+ r.out(w, codeTag)
+ escapeHTML(w, node.Literal)
+ r.out(w, codeCloseTag)
+ case Document:
+ break
+ case Paragraph:
+ if skipParagraphTags(node) {
+ break
+ }
+ if entering {
+ // TODO: untangle this clusterfuck about when the newlines need
+ // to be added and when not.
+ if node.Prev != nil {
+ switch node.Prev.Type {
+ case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule:
+ r.cr(w)
+ }
+ }
+ if node.Parent.Type == BlockQuote && node.Prev == nil {
+ r.cr(w)
+ }
+ r.out(w, pTag)
+ } else {
+ r.out(w, pCloseTag)
+ if !(node.Parent.Type == Item && node.Next == nil) {
+ r.cr(w)
+ }
+ }
+ case BlockQuote:
+ if entering {
+ r.cr(w)
+ r.out(w, blockquoteTag)
+ } else {
+ r.out(w, blockquoteCloseTag)
+ r.cr(w)
+ }
+ case HTMLBlock:
+ if r.Flags&SkipHTML != 0 {
+ break
+ }
+ r.cr(w)
+ r.out(w, node.Literal)
+ r.cr(w)
+ case Heading:
+ openTag, closeTag := headingTagsFromLevel(node.Level)
+ if entering {
+ if node.IsTitleblock {
+ attrs = append(attrs, `class="title"`)
+ }
+ if node.HeadingID != "" {
+ id := r.ensureUniqueHeadingID(node.HeadingID)
+ if r.HeadingIDPrefix != "" {
+ id = r.HeadingIDPrefix + id
+ }
+ if r.HeadingIDSuffix != "" {
+ id = id + r.HeadingIDSuffix
+ }
+ attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
+ }
+ r.cr(w)
+ r.tag(w, openTag, attrs)
+ } else {
+ r.out(w, closeTag)
+ if !(node.Parent.Type == Item && node.Next == nil) {
+ r.cr(w)
+ }
+ }
+ case HorizontalRule:
+ r.cr(w)
+ r.outHRTag(w)
+ r.cr(w)
+ case List:
+ openTag := ulTag
+ closeTag := ulCloseTag
+ if node.ListFlags&ListTypeOrdered != 0 {
+ openTag = olTag
+ closeTag = olCloseTag
+ }
+ if node.ListFlags&ListTypeDefinition != 0 {
+ openTag = dlTag
+ closeTag = dlCloseTag
+ }
+ if entering {
+ if node.IsFootnotesList {
+ r.out(w, footnotesDivBytes)
+ r.outHRTag(w)
+ r.cr(w)
+ }
+ r.cr(w)
+ if node.Parent.Type == Item && node.Parent.Parent.Tight {
+ r.cr(w)
+ }
+ r.tag(w, openTag[:len(openTag)-1], attrs)
+ r.cr(w)
+ } else {
+ r.out(w, closeTag)
+ //cr(w)
+ //if node.parent.Type != Item {
+ // cr(w)
+ //}
+ if node.Parent.Type == Item && node.Next != nil {
+ r.cr(w)
+ }
+ if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
+ r.cr(w)
+ }
+ if node.IsFootnotesList {
+ r.out(w, footnotesCloseDivBytes)
+ }
+ }
+ case Item:
+ openTag := liTag
+ closeTag := liCloseTag
+ if node.ListFlags&ListTypeDefinition != 0 {
+ openTag = ddTag
+ closeTag = ddCloseTag
+ }
+ if node.ListFlags&ListTypeTerm != 0 {
+ openTag = dtTag
+ closeTag = dtCloseTag
+ }
+ if entering {
+ if itemOpenCR(node) {
+ r.cr(w)
+ }
+ if node.ListData.RefLink != nil {
+ slug := slugify(node.ListData.RefLink)
+ r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
+ break
+ }
+ r.out(w, openTag)
+ } else {
+ if node.ListData.RefLink != nil {
+ slug := slugify(node.ListData.RefLink)
+ if r.Flags&FootnoteReturnLinks != 0 {
+ r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
+ }
+ }
+ r.out(w, closeTag)
+ r.cr(w)
+ }
+ case CodeBlock:
+ attrs = appendLanguageAttr(attrs, node.Info)
+ r.cr(w)
+ r.out(w, preTag)
+ r.tag(w, codeTag[:len(codeTag)-1], attrs)
+ escapeHTML(w, node.Literal)
+ r.out(w, codeCloseTag)
+ r.out(w, preCloseTag)
+ if node.Parent.Type != Item {
+ r.cr(w)
+ }
+ case Table:
+ if entering {
+ r.cr(w)
+ r.out(w, tableTag)
+ } else {
+ r.out(w, tableCloseTag)
+ r.cr(w)
+ }
+ case TableCell:
+ openTag := tdTag
+ closeTag := tdCloseTag
+ if node.IsHeader {
+ openTag = thTag
+ closeTag = thCloseTag
+ }
+ if entering {
+ align := cellAlignment(node.Align)
+ if align != "" {
+ attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
+ }
+ if node.Prev == nil {
+ r.cr(w)
+ }
+ r.tag(w, openTag, attrs)
+ } else {
+ r.out(w, closeTag)
+ r.cr(w)
+ }
+ case TableHead:
+ if entering {
+ r.cr(w)
+ r.out(w, theadTag)
+ } else {
+ r.out(w, theadCloseTag)
+ r.cr(w)
+ }
+ case TableBody:
+ if entering {
+ r.cr(w)
+ r.out(w, tbodyTag)
+ // XXX: this is to adhere to a rather silly test. Should fix test.
+ if node.FirstChild == nil {
+ r.cr(w)
+ }
+ } else {
+ r.out(w, tbodyCloseTag)
+ r.cr(w)
+ }
+ case TableRow:
+ if entering {
+ r.cr(w)
+ r.out(w, trTag)
+ } else {
+ r.out(w, trCloseTag)
+ r.cr(w)
+ }
+ default:
+ panic("Unknown node type " + node.Type.String())
+ }
+ return GoToNext
+}
+
+// RenderHeader writes HTML document preamble and TOC if requested.
+func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
+ r.writeDocumentHeader(w)
+ if r.Flags&TOC != 0 {
+ r.writeTOC(w, ast)
+ }
+}
+
+// RenderFooter writes HTML document footer.
+func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
+ if r.Flags&CompletePage == 0 {
+ return
+ }
+ io.WriteString(w, "\n