1
0
mirror of https://github.com/smallstep/cli.git synced 2025-08-09 03:22:43 +03:00

Initial commit

This commit is contained in:
Mariano Cano
2018-07-17 16:06:17 -07:00
commit c58db6b98b
200 changed files with 28749 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@@ -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

9
CHANGELOG.md Normal file
View File

@@ -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`

44
GETTING_STARTED.md Normal file
View File

@@ -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.

311
Gopkg.lock generated Normal file
View File

@@ -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

84
Gopkg.toml Normal file
View File

@@ -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"

131
Makefile Normal file
View File

@@ -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

17
README.md Normal file
View File

@@ -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)

180
cmd/step/main.go Normal file
View File

@@ -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:
// - "<foo>" 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 = "<value>"
}
return cli.FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder) + "\t" + usage
}

104
command/README.md Normal file
View File

@@ -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 <command> -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`.

View File

@@ -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
}

View File

@@ -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 <group | command> [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)
}

View File

@@ -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
})
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

161
command/command.go Normal file
View File

@@ -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
}

166
command/crypto/crypto.go Normal file
View File

@@ -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)
}

276
command/crypto/hash/hash.go Normal file
View File

@@ -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> [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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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 ""
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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, "")
}
}

96
command/crypto/jwe/jwe.go Normal file
View File

@@ -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 <subcommand> [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(),
},
}
}

View File

@@ -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** <public-jwk-file> <private-jwk-file>
[**--kty**=<type>] [**--alg**=<algorithm>] [**--use**=<use>]
[**--size**=<size>] [**--crv**=<curve>] [**--kid**=<kid>]
[**--from-pem**=<pem-file>]`,
Description: `**step crypto jwk create** generates a new JWK (JSON Web Key) or constructs a
JWK from an existing key. The generated JWK conforms to RFC7517 and can be used
to sign and encrypt data using JWT, JWS, and JWE.
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
<public-jwk-file>
: Path to which the the public JWK should be written
<private-jwk-file>
: 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 <type> of key to create. Corresponds to the **"kty"** JWK parameter.
If unset, default is EC.
: <type> is a case-sensitive string and must be one of:
**EC**
: 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 <size> (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 <curve> to use for EC and OKP key types. Corresponds
to the **"crv"** JWK parameter. Valid curves are defined in JWA [RFC7518]. If
unset, default is P-256 for EC keys and Ed25519 for OKP keys.
: <curve> is a case-sensitive string and must be one of:
**P-256**
: NIST P-256 Curve
**P-384**
: NIST P-384 Curve
**P-521**
: NIST P-521 Curve
**Ed25519**
: Ed25519 Curve
`,
},
cli.StringFlag{
Name: "alg, algorithm",
Usage: `The <algorithm> intended for use with this key. Corresponds to the
**"alg"** JWK parameter. <algorithm> 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) <algorithm> 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) <algorithm> 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 <use> 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.
: <use> 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 <kid> (key ID) for this JWK. Corresponds to the
"kid" JWK parameter. Used to identify an individual key in a JWK Set, for
example. <kid> is a case-sensitive string. If unset, the JWK Thumbprint
[RFC7638] is used as <kid>. 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.
<key-op> 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 <pem-file> instead of creating a new key.`,
},
cli.BoolFlag{
Name: "no-password",
Usage: `Do not ask for a password to encrypt the JWK. Sensitive
key material will be written to disk unencrypted. This is not
recommended. Requires **--insecure** flag.`,
},
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
}

34
command/crypto/jwk/jwk.go Normal file
View File

@@ -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(),
},
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

37
command/crypto/jwt/jwt.go Normal file
View File

@@ -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(),
},
}
}

340
command/crypto/jwt/sign.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

240
command/crypto/kdf/kdf.go Normal file
View File

@@ -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> [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")
}

106
command/crypto/kdf/phc.go Normal file
View File

@@ -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
}

View File

@@ -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)
}

146
command/crypto/keypair.go Normal file
View File

@@ -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 (RivestShamirAdleman) 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
}

109
command/crypto/nacl/auth.go Normal file
View File

@@ -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")
}

243
command/crypto/nacl/box.go Normal file
View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

169
command/crypto/nacl/sign.go Normal file
View File

@@ -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
}

View File

@@ -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
}

52
command/crypto/otp/otp.go Normal file
View File

@@ -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,
})
}

View File

@@ -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
}

640
command/oauth/cmd.go Normal file
View File

@@ -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**=<provider>] [**--client-id**=<client-id> **--client-secret**=<client-secret>]
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
**step oauth** **--authorization-endpoint**=<authorization-endpoint> **--token-endpoint**=<token-endpoint>
**--client-id**=<client-id> **--client-secret**=<client-secret> [**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
**step oauth** [**--account**=<account>] [**--authorization-endpoint**=<authorization-endpoint> **--token-endpoint**=<token-endpoint>]
[**--scope**=<scope> ...] [**--bare** [**--oidc**]] [**--header** [**--oidc**]]
**step oauth** **--account**=<account> **--jwt** [**--scope**=<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)
}

View File

@@ -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
}

86
config/config.go Normal file
View File

@@ -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
}

View File

@@ -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)))
}

View File

@@ -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: <error>, 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: <error>, 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 <error> but not <nil>")
}
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: <error>, 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: <error>, 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: <error>, 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: <error>, 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")
}
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 "},
{"<path>", "error opening certificate file <path>"},
{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)
}

View File

@@ -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)
}

View File

@@ -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")
}
}
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,36 @@
-----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE-----

View File

@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@@ -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-----

View File

@@ -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,
}

26
crypto/keys/clean_test.go Normal file
View File

@@ -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)
}

311
crypto/keys/key.go Normal file
View File

@@ -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
}

24
crypto/keys/keyPair.go Normal file
View File

@@ -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
}

414
crypto/keys/key_test.go Normal file
View File

@@ -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)
}
}
}
}

46
crypto/random.go Normal file
View File

@@ -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
}

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIEZVK8QMUOQK09NkTsoGXCBeniAERbR2eow5OB66qQqeoAoGCCqGSM49
AwEHoUQDQgAETbf9/jeZxYmhPeLsfCP77x7vvfLw1NOd+eMzJTdIqUletHs3LSHU
0D3b/fMlRJq498hFf7brnwQXfJiW9+gNng==
-----END EC PRIVATE KEY-----

View File

@@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

256
errs/errs.go Normal file
View File

@@ -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)
}
}

145
exec/exec.go Normal file
View File

@@ -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)
}
}
}

206
flags/flags.go Normal file
View File

@@ -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,
}
}

133
integration/command.go Normal file
View File

@@ -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)
})
}

View File

@@ -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"])
}

419
integration/jwk_test.go Normal file
View File

@@ -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 '<j.prvfile>' will be encrypted with the key:
// <password>
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")
})
}

Some files were not shown because too many files have changed in this diff Show More