diff --git a/.gitignore b/.gitignore index 4ae6b5bd..ed2ab99d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,8 +18,3 @@ coverage.txt vendor output - -# Ignore modules until switch from gopkg -go.mod -go.sum - diff --git a/.golangci.yml b/.golangci.yml index 4defd73f..2028654a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -51,6 +51,7 @@ linters: - deadcode - staticcheck - unused + - gosimple run: skip-dirs: diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 1b3d5509..00000000 --- a/Gopkg.lock +++ /dev/null @@ -1,479 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - branch = "master" - digest = "1:6716c9fe6333591128e72848f246fc01dc72240e1e64185d8b4e124e7280b35d" - name = "github.com/AndreasBriese/bbloom" - packages = ["."] - pruneopts = "UT" - revision = "e2d15f34fcf99d5dbb871c820ec73f710fca9815" - -[[projects]] - digest = "1:3b10c6fd33854dc41de2cf78b7bae105da94c2789b6fa5b9ac9e593ea43484ac" - name = "github.com/Masterminds/goutils" - packages = ["."] - pruneopts = "UT" - revision = "41ac8693c5c10a92ea1ff5ac3a7f95646f6123b0" - version = "v1.1.0" - -[[projects]] - digest = "1:181a6a5506bd83827d826df5b3272040e92c487516b2fc8edd066be941d68d9e" - name = "github.com/Masterminds/sprig" - packages = ["."] - pruneopts = "UT" - revision = "0e09f04f09aede2cc36cbecad7ec27b0055303e0" - version = "v3.0.0" - -[[projects]] - branch = "master" - digest = "1:454adc7f974228ff789428b6dc098638c57a64aa0718f0bd61e53d3cd39d7a75" - name = "github.com/chzyer/readline" - packages = ["."] - pruneopts = "UT" - revision = "2972be24d48e78746da79ba8e24e8b488c9880de" - -[[projects]] - digest = "1:21ac9938fb1098b3a7b0dd909fb30878d33231177fac11a2821114eb9c1088ff" - name = "github.com/dgraph-io/badger" - packages = [ - ".", - "options", - "protos", - "skl", - "table", - "y", - ] - pruneopts = "UT" - revision = "391b6d3b93e6014fe8c2971fcc0c1266e47dbbd9" - version = "v1.5.3" - -[[projects]] - branch = "master" - digest = "1:6e8109ce247a59ab1eeb5330166c12735f6590de99c9647b6162d11518d32c9a" - name = "github.com/dgryski/go-farm" - packages = ["."] - pruneopts = "UT" - revision = "6a90982ecee230ff6cba02d5bd386acc030be9d3" - -[[projects]] - branch = "master" - digest = "1:81fda4d18a16651bf92245ce5d6178cdd99f918db30ae9794732655f0686e895" - name = "github.com/go-chi/chi" - packages = ["."] - pruneopts = "UT" - revision = "0ebf7795c516423a110473652e9ba3a59a504863" - -[[projects]] - digest = "1:ec6f9bf5e274c833c911923c9193867f3f18788c461f76f05f62bb1510e0ae65" - name = "github.com/go-sql-driver/mysql" - packages = ["."] - pruneopts = "UT" - revision = "72cd26f257d44c1114970e19afddcd812016007e" - version = "v1.4.1" - -[[projects]] - digest = "1:97df918963298c287643883209a2c3f642e6593379f97ab400c2a2e219ab647d" - name = "github.com/golang/protobuf" - packages = ["proto"] - pruneopts = "UT" - revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" - version = "v1.2.0" - -[[projects]] - digest = "1:582b704bebaa06b48c29b0cec224a6058a09c86883aaddabde889cd1a5f73e1b" - name = "github.com/google/uuid" - packages = ["."] - pruneopts = "UT" - revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4" - version = "v1.1.1" - -[[projects]] - digest = "1:f9a5e090336881be43cfc1cf468330c1bdd60abdc9dd194e0b1ab69f4b94dd7c" - name = "github.com/huandu/xstrings" - packages = ["."] - pruneopts = "UT" - revision = "f02667b379e2fb5916c3cda2cf31e0eb885d79f8" - version = "v1.2.0" - -[[projects]] - digest = "1:78d28d5b84a26159c67ea51996a230da4bc07cac648adaae1dfb5fc0ec8e40d3" - name = "github.com/imdario/mergo" - packages = ["."] - pruneopts = "UT" - revision = "1afb36080aec31e0d1528973ebe6721b191b0369" - version = "v0.3.8" - -[[projects]] - branch = "master" - digest = "1:e51f40f0c19b39c1825eadd07d5c0a98a2ad5942b166d9fc4f54750ce9a04810" - name = "github.com/juju/ansiterm" - packages = [ - ".", - "tabwriter", - ] - pruneopts = "UT" - revision = "720a0952cc2ac777afc295d9861263e2a4cf96a1" - -[[projects]] - digest = "1:0a69a1c0db3591fcefb47f115b224592c8dfa4368b7ba9fae509d5e16cdc95c8" - name = "github.com/konsorten/go-windows-terminal-sequences" - packages = ["."] - pruneopts = "UT" - revision = "5c8c8bd35d3832f5d134ae1e1e375b69a4d25242" - version = "v1.0.1" - -[[projects]] - branch = "master" - digest = "1:bb08c7bb1c7224636b1a00639f079ed4391eb822945f26db74b8d8ee3f14d991" - name = "github.com/lunixbochs/vtclean" - packages = ["."] - pruneopts = "UT" - revision = "2d01aacdc34a083dca635ba869909f5fc0cd4f41" - -[[projects]] - digest = "1:2d2bc0f23cca6b59cec3fbece9abc102bdb19f548dd58d7667e57699074a2c76" - name = "github.com/manifoldco/promptui" - packages = [ - ".", - "list", - "screenbuf", - ] - pruneopts = "UT" - revision = "157c96fb638a14d268b305cf2012582431fcc410" - version = "v0.3.1" - -[[projects]] - digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67" - name = "github.com/mattn/go-colorable" - packages = ["."] - pruneopts = "UT" - revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" - version = "v0.0.9" - -[[projects]] - digest = "1:0981502f9816113c9c8c4ac301583841855c8cf4da8c72f696b3ebedf6d0e4e5" - name = "github.com/mattn/go-isatty" - packages = ["."] - pruneopts = "UT" - revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" - version = "v0.0.4" - -[[projects]] - digest = "1:09ca328575f38b80969ccf857f6d7302f2ce09d53778ea7aaba526cfd2cec739" - name = "github.com/mitchellh/copystructure" - packages = ["."] - pruneopts = "UT" - revision = "9a1b6f44e8da0e0e374624fb0a825a231b00c537" - version = "v1.0.0" - -[[projects]] - digest = "1:2a7e6f8bebdca6bd8bc359c37f01ae1c4ea4f8481eaabf93b1ae4863f15b72c7" - name = "github.com/mitchellh/reflectwalk" - packages = ["."] - pruneopts = "UT" - revision = "3e2c75dfad4fbf904b58782a80fd595c760ad185" - version = "v1.0.1" - -[[projects]] - branch = "master" - digest = "1:ae08d850ba158ea3ba4a7bb90f8372608172d8920644e5a6693b940a1f4e5d01" - name = "github.com/mmcloughlin/avo" - packages = [ - "attr", - "build", - "buildtags", - "gotypes", - "internal/prnt", - "internal/stack", - "ir", - "operand", - "pass", - "printer", - "reg", - "src", - "x86", - ] - pruneopts = "UT" - revision = "2e7d06bc7ada2979f17ccf8ebf486dba23b84fc7" - -[[projects]] - digest = "1:266d082179f3a29a4bdcf1dcc49d4a304f5c7107e65bd22d1fecacf45f1ac348" - name = "github.com/newrelic/go-agent" - packages = [ - ".", - "internal", - "internal/cat", - "internal/jsonx", - "internal/logger", - "internal/sysinfo", - "internal/utilization", - ] - pruneopts = "UT" - revision = "f5bce3387232559bcbe6a5f8227c4bf508dac1ba" - version = "v1.11.0" - -[[projects]] - digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b" - name = "github.com/pkg/errors" - packages = ["."] - pruneopts = "UT" - revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" - version = "v0.8.1" - -[[projects]] - digest = "1:2e76a73cb51f42d63a2a1a85b3dc5731fd4faf6821b434bd0ef2c099186031d6" - name = "github.com/rs/xid" - packages = ["."] - pruneopts = "UT" - revision = "15d26544def341f036c5f8dca987a4cbe575032c" - version = "v1.2.1" - -[[projects]] - branch = "master" - digest = "1:8baa3b16f20963c54e296627ea1dabfd79d1b486f81baf8759e99d73bddf2687" - name = "github.com/samfoo/ansi" - packages = ["."] - pruneopts = "UT" - revision = "b6bd2ded7189ce35bc02233b554eb56a5146af73" - -[[projects]] - branch = "master" - digest = "1:def689e73e9252f6f7fe66834a76751a41b767e03daab299e607e7226c58a855" - name = "github.com/shurcooL/sanitized_anchor_name" - packages = ["."] - pruneopts = "UT" - revision = "86672fcb3f950f35f2e675df2240550f2a50762f" - -[[projects]] - digest = "1:3f53e9e4dfbb664cd62940c9c4b65a2171c66acd0b7621a1a6b8e78513525a52" - name = "github.com/sirupsen/logrus" - packages = ["."] - pruneopts = "UT" - revision = "ad15b42461921f1fb3529b058c6786c6a45d5162" - version = "v1.1.1" - -[[projects]] - branch = "master" - digest = "1:4d1f0640875aefefdb2151f297c144518a71f5729c4b9f9423f09df501f699c5" - name = "github.com/smallstep/assert" - packages = ["."] - pruneopts = "UT" - revision = "de77670473b5492f5d0bce155b5c01534c2d13f7" - -[[projects]] - branch = "ssh-login" - digest = "1:35cb5bef5983a5e43466c00bd6bd7b3b401a94f4abe22873b7b72bad344ee855" - name = "github.com/smallstep/cli" - packages = [ - "command", - "command/version", - "config", - "crypto/keys", - "crypto/pemutil", - "crypto/randutil", - "crypto/tlsutil", - "crypto/x509util", - "errs", - "jose", - "pkg/blackfriday", - "pkg/x509", - "token", - "token/provision", - "ui", - "usage", - "utils", - ] - pruneopts = "UT" - revision = "15911d8625df46378c68ecda3b97290e745d7097" - -[[projects]] - branch = "master" - digest = "1:7d03323edb817ca94efaee5489cde6acd06ceeaca9e6eee106d2d6a90deca997" - name = "github.com/smallstep/nosql" - packages = [ - ".", - "badger", - "bolt", - "database", - "mysql", - ] - pruneopts = "UT" - revision = "f80b3f432de0662f07ebd58fe52b0a119fe5dcd9" - -[[projects]] - digest = "1:08d65904057412fc0270fc4812a1c90c594186819243160dc779a402d4b6d0bc" - name = "github.com/spf13/cast" - packages = ["."] - pruneopts = "UT" - revision = "8c9545af88b134710ab1cd196795e7f2388358d7" - version = "v1.3.0" - -[[projects]] - branch = "master" - digest = "1:6743b69de0d73e91004e4e201cf4965b59a0fa5caf6f0ffbe0cb9ee8807738a7" - name = "github.com/urfave/cli" - packages = ["."] - pruneopts = "UT" - revision = "b67dcf995b6a7b7f14fad5fcb7cc5441b05e814b" - -[[projects]] - digest = "1:5f7414cf41466d4b4dd7ec52b2cd3e481e08cfd11e7e24fef730c0e483e88bb1" - name = "go.etcd.io/bbolt" - packages = ["."] - pruneopts = "UT" - revision = "63597a96ec0ad9e6d43c3fc81e809909e0237461" - version = "v1.3.2" - -[[projects]] - branch = "master" - digest = "1:f492081c0e705c98ea169765a0fa60c9cfee2358a2434e96abd5bb6f59dbc190" - name = "golang.org/x/crypto" - packages = [ - "cryptobyte", - "cryptobyte/asn1", - "curve25519", - "ed25519", - "ed25519/internal/edwards25519", - "internal/chacha20", - "internal/subtle", - "ocsp", - "pbkdf2", - "poly1305", - "scrypt", - "ssh", - "ssh/terminal", - ] - pruneopts = "UT" - revision = "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06" - -[[projects]] - branch = "master" - digest = "1:2f7468b0b3fd7d926072f0dcbb6ec81e337278b4e5de639d017e54f785f0b475" - name = "golang.org/x/net" - packages = [ - "context", - "html", - "html/atom", - "http/httpguts", - "http2", - "http2/hpack", - "idna", - "internal/timeseries", - "trace", - ] - pruneopts = "UT" - revision = "c44066c5c816ec500d459a2a324a753f78531ae0" - -[[projects]] - branch = "master" - digest = "1:417d27a82efb8473554234a282be33d23b0d6adc121e636b55950f913ac071d6" - name = "golang.org/x/sys" - packages = [ - "unix", - "windows", - ] - pruneopts = "UT" - revision = "9b800f95dbbc54abff0acf7ee32d88ba4e328c89" - -[[projects]] - digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" - 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", - ] - pruneopts = "UT" - revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" - version = "v0.3.0" - -[[projects]] - branch = "master" - digest = "1:6b4a1c844969280f4d3e36ef4b0762e3522e701c015f688b68ef91c2ea6b5ac7" - name = "golang.org/x/tools" - packages = [ - "go/ast/astutil", - "go/gcexportdata", - "go/internal/cgo", - "go/internal/gcimporter", - "go/packages", - "go/types/typeutil", - "internal/fastwalk", - "internal/gopathwalk", - "internal/semver", - ] - pruneopts = "UT" - revision = "3a10b9bf0a52df7e992a8c3eb712a86d3c896c75" - -[[projects]] - digest = "1:c25289f43ac4a68d88b02245742347c94f1e108c534dda442188015ff80669b3" - name = "google.golang.org/appengine" - packages = ["cloudsql"] - pruneopts = "UT" - revision = "54a98f90d1c46b7731eb8fb305d2a321c30ef610" - version = "v1.5.0" - -[[projects]] - digest = "1:9593bab40e981b1f90b7e07faeab0d09b75fe338880d08880f986a9d3283c53f" - name = "gopkg.in/square/go-jose.v2" - packages = [ - ".", - "cipher", - "json", - "jwt", - ] - pruneopts = "UT" - revision = "730df5f748271903322feb182be83b43ebbbe27d" - version = "v2.3.1" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - input-imports = [ - "github.com/Masterminds/sprig", - "github.com/go-chi/chi", - "github.com/newrelic/go-agent", - "github.com/pkg/errors", - "github.com/rs/xid", - "github.com/sirupsen/logrus", - "github.com/smallstep/assert", - "github.com/smallstep/cli/command", - "github.com/smallstep/cli/command/version", - "github.com/smallstep/cli/config", - "github.com/smallstep/cli/crypto/keys", - "github.com/smallstep/cli/crypto/pemutil", - "github.com/smallstep/cli/crypto/randutil", - "github.com/smallstep/cli/crypto/tlsutil", - "github.com/smallstep/cli/crypto/x509util", - "github.com/smallstep/cli/errs", - "github.com/smallstep/cli/jose", - "github.com/smallstep/cli/pkg/x509", - "github.com/smallstep/cli/token", - "github.com/smallstep/cli/token/provision", - "github.com/smallstep/cli/ui", - "github.com/smallstep/cli/usage", - "github.com/smallstep/cli/utils", - "github.com/smallstep/nosql", - "github.com/smallstep/nosql/database", - "github.com/urfave/cli", - "golang.org/x/crypto/ed25519", - "golang.org/x/crypto/ocsp", - "golang.org/x/crypto/ssh", - "golang.org/x/net/http2", - "gopkg.in/square/go-jose.v2", - "gopkg.in/square/go-jose.v2/jwt", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index 18745f11..00000000 --- a/Gopkg.toml +++ /dev/null @@ -1,63 +0,0 @@ -# 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 - -ignored = ["github.com/Masterminds/semver/v3"] - -[[override]] - name = "gopkg.in/alecthomas/kingpin.v3-unstable" - revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306" - -[[constraint]] - branch = "master" - name = "github.com/go-chi/chi" - -[[override]] - branch = "ssh-login" - name = "github.com/smallstep/cli" - -[[constraint]] - branch = "master" - name = "github.com/smallstep/nosql" - -[[constraint]] - name = "github.com/newrelic/go-agent" - version = "1.11.0" - -[[constraint]] - name = "github.com/sirupsen/logrus" - version = "1.0.6" - -[[constraint]] - name = "gopkg.in/square/go-jose.v2" - version = "2.3.1" - -[prune] - go-tests = true - unused-packages = true - -[[constraint]] - name = "github.com/Masterminds/sprig" - version = "3.0.0" diff --git a/Makefile b/Makefile index 644b2d9d..7e06d837 100644 --- a/Makefile +++ b/Makefile @@ -17,21 +17,9 @@ all: build test lint ######################################### bootstra%: - $Q which dep || go get github.com/golang/dep/cmd/dep - $Q dep ensure $Q GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.18.0 - -vendor: Gopkg.lock - $Q dep ensure - -define VENDOR_BIN_TMPL -vendor/bin/$(notdir $(1)): vendor - $Q go build -o $$@ ./vendor/$(1) -VENDOR_BINS += vendor/bin/$(notdir $(1)) -endef - -.PHONY: bootstra% vendor +.PHONY: bootstra% ################################################# # Determine the type of `push` and `version` @@ -64,20 +52,23 @@ 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 +download: + $Q go mod download + build: $(PREFIX)bin/$(BINNAME) @echo "Build Complete!" -$(PREFIX)bin/$(BINNAME): vendor $(call rwildcard,*.go) +$(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go) $Q mkdir -p $(@D) $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) -# Target for building without calling dep ensure +# Target to force a build of step-ca without running tests simple: - $Q mkdir -p bin/ - $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o bin/$(BINNAME) $(LDFLAGS) $(PKG) + $Q mkdir -p $(PREFIX)bin + $Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) @echo "Build Complete!" -.PHONY: build simple +.PHONY: download build simple ######################################### # Go generate @@ -134,8 +125,6 @@ uninstall: ######################################### 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 diff --git a/acme/api/account.go b/acme/api/account.go index 05d6a084..fb43d4f9 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -128,7 +128,6 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", h.Auth.GetLink(acme.AccountLink, acme.URLSafeProvisionerName(prov), true, acc.GetID())) api.JSONStatus(w, acc, httpStatus) - return } // GetUpdateAccount is the api for updating an ACME account. @@ -172,7 +171,6 @@ func (h *Handler) GetUpdateAccount(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Location", h.Auth.GetLink(acme.AccountLink, acme.URLSafeProvisionerName(prov), true, acc.GetID())) api.JSON(w, acc) - return } func logOrdersByAccount(w http.ResponseWriter, oids []string) { @@ -209,5 +207,4 @@ func (h *Handler) GetOrdersByAccount(w http.ResponseWriter, r *http.Request) { } api.JSON(w, orders) logOrdersByAccount(w, orders) - return } diff --git a/acme/api/handler.go b/acme/api/handler.go index 423c08ea..11cd74f2 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -113,7 +113,6 @@ func (h *Handler) GetNonce(w http.ResponseWriter, r *http.Request) { } else { w.WriteHeader(http.StatusNoContent) } - return } // GetDirectory is the ACME resource for returning a directory configuration @@ -126,7 +125,6 @@ func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) { } dir := h.Auth.GetDirectory(prov) api.JSON(w, dir) - return } // GetAuthz ACME api for retrieving an Authz. @@ -149,7 +147,6 @@ func (h *Handler) GetAuthz(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", h.Auth.GetLink(acme.AuthzLink, acme.URLSafeProvisionerName(prov), true, authz.GetID())) api.JSON(w, authz) - return } // GetChallenge ACME api for retrieving a Challenge. @@ -191,7 +188,6 @@ func (h *Handler) GetChallenge(w http.ResponseWriter, r *http.Request) { w.Header().Add("Link", link(getLink(acme.AuthzLink, acme.URLSafeProvisionerName(prov), true, ch.GetAuthzID()), "up")) w.Header().Set("Location", getLink(acme.ChallengeLink, acme.URLSafeProvisionerName(prov), true, ch.GetID())) api.JSON(w, ch) - return } // GetCertificate ACME api for retrieving a Certificate. @@ -210,5 +206,4 @@ func (h *Handler) GetCertificate(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/pem-certificate-chain; charset=utf-8") w.Write(certBytes) - return } diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index cf256eb2..3757882f 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -17,6 +17,7 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/db" "github.com/smallstep/cli/crypto/pemutil" "github.com/smallstep/cli/jose" ) @@ -230,7 +231,8 @@ func TestHandlerGetNonce(t *testing.T) { } func TestHandlerGetDirectory(t *testing.T) { - auth := acme.NewAuthority(nil, "ca.smallstep.com", "acme", nil) + auth, err := acme.NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) prov := newProv() url := fmt.Sprintf("http://ca.smallstep.com/acme/%s/directory", acme.URLSafeProvisionerName(prov)) diff --git a/acme/api/middleware.go b/acme/api/middleware.go index 3f4c99a5..af2618bf 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -42,7 +42,6 @@ func (h *Handler) addNonce(next nextHTTP) nextHTTP { w.Header().Set("Cache-Control", "no-store") logNonce(w, nonce) next(w, r) - return } } @@ -57,7 +56,6 @@ func (h *Handler) addDirLink(next nextHTTP) nextHTTP { } w.Header().Add("Link", link(h.Auth.GetLink(acme.DirectoryLink, acme.URLSafeProvisionerName(prov), true), "index")) next(w, r) - return } } @@ -87,7 +85,6 @@ func (h *Handler) verifyContentType(next nextHTTP) nextHTTP { } api.WriteError(w, acme.MalformedErr(errors.Errorf( "expected content-type to be in %s, but got %s", expected, ct))) - return } } @@ -106,7 +103,6 @@ func (h *Handler) parseJWS(next nextHTTP) nextHTTP { } ctx := context.WithValue(r.Context(), jwsContextKey, jws) next(w, r.WithContext(ctx)) - return } } @@ -202,7 +198,6 @@ func (h *Handler) validateJWS(next nextHTTP) nextHTTP { return } next(w, r) - return } } @@ -248,7 +243,6 @@ func (h *Handler) extractJWK(next nextHTTP) nextHTTP { ctx = context.WithValue(ctx, accContextKey, acc) } next(w, r.WithContext(ctx)) - return } } @@ -275,7 +269,6 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { } ctx = context.WithValue(ctx, provisionerContextKey, p) next(w, r.WithContext(ctx)) - return } } @@ -355,7 +348,6 @@ func (h *Handler) verifyAndExtractJWSPayload(next nextHTTP) nextHTTP { isEmptyJSON: string(payload) == "{}", }) next(w, r.WithContext(ctx)) - return } } @@ -372,6 +364,5 @@ func (h *Handler) isPostAsGet(next nextHTTP) nextHTTP { return } next(w, r) - return } } diff --git a/acme/api/middleware_test.go b/acme/api/middleware_test.go index 18fafd8d..f8aa322c 100644 --- a/acme/api/middleware_test.go +++ b/acme/api/middleware_test.go @@ -26,7 +26,6 @@ var testBody = []byte("foo") func testNext(w http.ResponseWriter, r *http.Request) { w.Write(testBody) - return } func TestHandlerAddNonce(t *testing.T) { @@ -471,7 +470,6 @@ func TestHandlerParseJWS(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, gotRaw, expRaw) w.Write(testBody) - return }, statusCode: 200, } @@ -923,7 +921,6 @@ func TestHandlerLookupJWK(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, _jwk, jwk) w.Write(testBody) - return }, statusCode: 200, } @@ -1114,7 +1111,6 @@ func TestHandlerExtractJWK(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, _jwk.KeyID, pub.KeyID) w.Write(testBody) - return }, statusCode: 200, } @@ -1139,7 +1135,6 @@ func TestHandlerExtractJWK(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, _jwk.KeyID, pub.KeyID) w.Write(testBody) - return }, statusCode: 200, } @@ -1448,7 +1443,6 @@ func TestHandlerValidateJWS(t *testing.T) { ctx: context.WithValue(context.Background(), jwsContextKey, jws), next: func(w http.ResponseWriter, r *http.Request) { w.Write(testBody) - return }, statusCode: 200, } @@ -1479,7 +1473,6 @@ func TestHandlerValidateJWS(t *testing.T) { ctx: context.WithValue(context.Background(), jwsContextKey, jws), next: func(w http.ResponseWriter, r *http.Request) { w.Write(testBody) - return }, statusCode: 200, } @@ -1510,7 +1503,6 @@ func TestHandlerValidateJWS(t *testing.T) { ctx: context.WithValue(context.Background(), jwsContextKey, jws), next: func(w http.ResponseWriter, r *http.Request) { w.Write(testBody) - return }, statusCode: 200, } diff --git a/acme/api/order.go b/acme/api/order.go index 83d1e26e..1d491102 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -97,7 +97,6 @@ func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.GetID())) api.JSONStatus(w, o, http.StatusCreated) - return } // GetOrder ACME api for retrieving an order. @@ -121,7 +120,6 @@ func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.GetID())) api.JSON(w, o) - return } // FinalizeOrder attemptst to finalize an order and create a certificate. @@ -160,5 +158,4 @@ func (h *Handler) FinalizeOrder(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", h.Auth.GetLink(acme.OrderLink, acme.URLSafeProvisionerName(prov), true, o.ID)) api.JSON(w, o) - return } diff --git a/acme/authority.go b/acme/authority.go index a96b1f49..ddbc9213 100644 --- a/acme/authority.go +++ b/acme/authority.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" + database "github.com/smallstep/certificates/db" "github.com/smallstep/cli/jose" "github.com/smallstep/nosql" ) @@ -43,11 +44,35 @@ type Authority struct { signAuth SignAuthority } +var ( + accountTable = []byte("acme_accounts") + accountByKeyIDTable = []byte("acme_keyID_accountID_index") + authzTable = []byte("acme_authzs") + challengeTable = []byte("acme_challenges") + nonceTable = []byte("nonces") + orderTable = []byte("acme_orders") + ordersByAccountIDTable = []byte("acme_account-orders-index") + certTable = []byte("acme_certs") +) + // NewAuthority returns a new Authority that implements the ACME interface. -func NewAuthority(db nosql.DB, dns, prefix string, signAuth SignAuthority) *Authority { +func NewAuthority(db nosql.DB, dns, prefix string, signAuth SignAuthority) (*Authority, error) { + if _, ok := db.(*database.SimpleDB); !ok { + // If it's not a SimpleDB then go ahead and bootstrap the DB with the + // necessary ACME tables. SimpleDB should ONLY be used for testing. + tables := [][]byte{accountTable, accountByKeyIDTable, authzTable, + challengeTable, nonceTable, orderTable, ordersByAccountIDTable, + certTable} + for _, b := range tables { + if err := db.CreateTable(b); err != nil { + return nil, errors.Wrapf(err, "error creating table %s", + string(b)) + } + } + } return &Authority{ db: db, dir: newDirectory(dns, prefix), signAuth: signAuth, - } + }, nil } // GetLink returns the requested link from the directory. diff --git a/acme/authority_test.go b/acme/authority_test.go index a5041fec..f3c47966 100644 --- a/acme/authority_test.go +++ b/acme/authority_test.go @@ -14,7 +14,8 @@ import ( ) func TestAuthorityGetLink(t *testing.T) { - auth := NewAuthority(nil, "ca.smallstep.com", "acme", nil) + auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) provID := "acme-test-provisioner" type test struct { auth *Authority @@ -69,7 +70,8 @@ func TestAuthorityGetLink(t *testing.T) { } func TestAuthorityGetDirectory(t *testing.T) { - auth := NewAuthority(nil, "ca.smallstep.com", "acme", nil) + auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) prov := newProv() acmeDir := auth.GetDirectory(prov) assert.Equals(t, acmeDir.NewNonce, fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-nonce", URLSafeProvisionerName(prov))) @@ -88,11 +90,12 @@ func TestAuthorityNewNonce(t *testing.T) { } tests := map[string]func(t *testing.T) test{ "fail/newNonce-error": func(t *testing.T) test { - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { return nil, false, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, res: nil, @@ -102,12 +105,13 @@ func TestAuthorityNewNonce(t *testing.T) { "ok": func(t *testing.T) test { var _res string res := &_res - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { *res = string(key) return nil, true, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, res: res, @@ -141,22 +145,24 @@ func TestAuthorityUseNonce(t *testing.T) { } tests := map[string]func(t *testing.T) test{ "fail/newNonce-error": func(t *testing.T) test { - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MUpdate: func(tx *database.Tx) error { return errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, err: ServerInternalErr(errors.New("error deleting nonce foo: force")), } }, "ok": func(t *testing.T) test { - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MUpdate: func(tx *database.Tx) error { return nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, } @@ -195,11 +201,12 @@ func TestAuthorityNewAccount(t *testing.T) { } tests := map[string]func(t *testing.T) test{ "fail/newAccount-error": func(t *testing.T) test { - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { return nil, false, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, ops: ops, @@ -213,7 +220,7 @@ func TestAuthorityNewAccount(t *testing.T) { count = 0 dir = newDirectory("ca.smallstep.com", "acme") ) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { if count == 1 { var acc *account @@ -225,6 +232,7 @@ func TestAuthorityNewAccount(t *testing.T) { return nil, true, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, ops: ops, @@ -267,13 +275,14 @@ func TestAuthorityGetAccount(t *testing.T) { tests := map[string]func(t *testing.T) test{ "fail/getAccount-error": func(t *testing.T) test { id := "foo" - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, accountTable) assert.Equals(t, key, []byte(id)) return nil, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -285,11 +294,12 @@ func TestAuthorityGetAccount(t *testing.T) { assert.FatalError(t, err) b, err := json.Marshal(acc) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { return b, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: acc.ID, @@ -338,7 +348,8 @@ func TestAuthorityGetAccountByKey(t *testing.T) { jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) jwk.Key = "foo" - auth := NewAuthority(nil, "ca.smallstep.com", "acme", nil) + auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, jwk: jwk, @@ -350,13 +361,14 @@ func TestAuthorityGetAccountByKey(t *testing.T) { assert.FatalError(t, err) kid, err := keyToID(jwk) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, accountByKeyIDTable) assert.Equals(t, key, []byte(kid)) return nil, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, jwk: jwk, @@ -371,7 +383,7 @@ func TestAuthorityGetAccountByKey(t *testing.T) { count := 0 kid, err := keyToID(acc.Key) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { var ret []byte switch { @@ -388,6 +400,7 @@ func TestAuthorityGetAccountByKey(t *testing.T) { return ret, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, jwk: acc.Key, @@ -434,13 +447,14 @@ func TestAuthorityGetOrder(t *testing.T) { tests := map[string]func(t *testing.T) test{ "fail/getOrder-error": func(t *testing.T) test { id := "foo" - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, orderTable) assert.Equals(t, key, []byte(id)) return nil, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -452,13 +466,14 @@ func TestAuthorityGetOrder(t *testing.T) { assert.FatalError(t, err) b, err := json.Marshal(o) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, orderTable) assert.Equals(t, key, []byte(o.ID)) return b, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: o.ID, @@ -472,7 +487,7 @@ func TestAuthorityGetOrder(t *testing.T) { b, err := json.Marshal(o) assert.FatalError(t, err) i := 0 - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { switch { case i == 0: @@ -487,6 +502,7 @@ func TestAuthorityGetOrder(t *testing.T) { } }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: o.ID, @@ -500,13 +516,14 @@ func TestAuthorityGetOrder(t *testing.T) { o.Status = "valid" b, err := json.Marshal(o) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, orderTable) assert.Equals(t, key, []byte(o.ID)) return b, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: o.ID, @@ -553,13 +570,14 @@ func TestAuthorityGetCertificate(t *testing.T) { tests := map[string]func(t *testing.T) test{ "fail/getCertificate-error": func(t *testing.T) test { id := "foo" - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, certTable) assert.Equals(t, key, []byte(id)) return nil, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -571,13 +589,14 @@ func TestAuthorityGetCertificate(t *testing.T) { assert.FatalError(t, err) b, err := json.Marshal(cert) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, certTable) assert.Equals(t, key, []byte(cert.ID)) return b, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: cert.ID, @@ -590,13 +609,14 @@ func TestAuthorityGetCertificate(t *testing.T) { assert.FatalError(t, err) b, err := json.Marshal(cert) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, certTable) assert.Equals(t, key, []byte(cert.ID)) return b, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: cert.ID, @@ -644,13 +664,14 @@ func TestAuthorityGetAuthz(t *testing.T) { tests := map[string]func(t *testing.T) test{ "fail/getAuthz-error": func(t *testing.T) test { id := "foo" - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, authzTable) assert.Equals(t, key, []byte(id)) return nil, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -662,13 +683,14 @@ func TestAuthorityGetAuthz(t *testing.T) { assert.FatalError(t, err) b, err := json.Marshal(az) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, authzTable) assert.Equals(t, key, []byte(az.getID())) return b, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: az.getID(), @@ -682,7 +704,7 @@ func TestAuthorityGetAuthz(t *testing.T) { b, err := json.Marshal(az) assert.FatalError(t, err) count := 0 - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { var ret []byte switch count { @@ -699,6 +721,7 @@ func TestAuthorityGetAuthz(t *testing.T) { return ret, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: az.getID(), @@ -757,7 +780,7 @@ func TestAuthorityGetAuthz(t *testing.T) { assert.FatalError(t, err) count = 0 - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { var ret []byte switch count { @@ -778,6 +801,7 @@ func TestAuthorityGetAuthz(t *testing.T) { return ret, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: az.getID(), @@ -822,11 +846,12 @@ func TestAuthorityNewOrder(t *testing.T) { } tests := map[string]func(t *testing.T) test{ "fail/newOrder-error": func(t *testing.T) test { - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { return nil, false, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, ops: defaultOrderOps(), @@ -843,7 +868,7 @@ func TestAuthorityNewOrder(t *testing.T) { _accID string accID = &_accID ) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) { switch count { case 0: @@ -876,6 +901,7 @@ func TestAuthorityNewOrder(t *testing.T) { return nil, database.ErrNotFound }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, ops: defaultOrderOps(), @@ -918,13 +944,14 @@ func TestAuthorityGetOrdersByAccount(t *testing.T) { tests := map[string]func(t *testing.T) test{ "fail/getOrderIDsByAccount-error": func(t *testing.T) test { id := "foo" - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, ordersByAccountIDTable) assert.Equals(t, key, []byte(id)) return nil, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -938,7 +965,7 @@ func TestAuthorityGetOrdersByAccount(t *testing.T) { count = 0 err error ) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { var ret []byte switch count { @@ -956,6 +983,7 @@ func TestAuthorityGetOrdersByAccount(t *testing.T) { return ret, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -973,7 +1001,7 @@ func TestAuthorityGetOrdersByAccount(t *testing.T) { baz, err := newO() bar.Status = StatusInvalid - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { var ret []byte switch count { @@ -1002,6 +1030,7 @@ func TestAuthorityGetOrdersByAccount(t *testing.T) { return ret, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -1043,13 +1072,14 @@ func TestAuthorityFinalizeOrder(t *testing.T) { tests := map[string]func(t *testing.T) test{ "fail/getOrder-error": func(t *testing.T) test { id := "foo" - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, orderTable) assert.Equals(t, key, []byte(id)) return nil, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -1061,13 +1091,14 @@ func TestAuthorityFinalizeOrder(t *testing.T) { assert.FatalError(t, err) b, err := json.Marshal(o) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, orderTable) assert.Equals(t, key, []byte(o.ID)) return b, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: o.ID, @@ -1081,7 +1112,7 @@ func TestAuthorityFinalizeOrder(t *testing.T) { o.Expires = time.Now().Add(-time.Minute) b, err := json.Marshal(o) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, orderTable) assert.Equals(t, key, []byte(o.ID)) @@ -1093,6 +1124,7 @@ func TestAuthorityFinalizeOrder(t *testing.T) { return nil, false, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: o.ID, @@ -1107,13 +1139,14 @@ func TestAuthorityFinalizeOrder(t *testing.T) { o.Certificate = "certID" b, err := json.Marshal(o) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, orderTable) assert.Equals(t, key, []byte(o.ID)) return b, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: o.ID, @@ -1161,13 +1194,14 @@ func TestAuthorityValidateChallenge(t *testing.T) { tests := map[string]func(t *testing.T) test{ "fail/getChallenge-error": func(t *testing.T) test { id := "foo" - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, challengeTable) assert.Equals(t, key, []byte(id)) return nil, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -1179,13 +1213,14 @@ func TestAuthorityValidateChallenge(t *testing.T) { assert.FatalError(t, err) b, err := json.Marshal(ch) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, challengeTable) assert.Equals(t, key, []byte(ch.getID())) return b, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: ch.getID(), @@ -1198,7 +1233,7 @@ func TestAuthorityValidateChallenge(t *testing.T) { assert.FatalError(t, err) b, err := json.Marshal(ch) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, challengeTable) assert.Equals(t, key, []byte(ch.getID())) @@ -1210,6 +1245,7 @@ func TestAuthorityValidateChallenge(t *testing.T) { return nil, false, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: ch.getID(), @@ -1226,13 +1262,14 @@ func TestAuthorityValidateChallenge(t *testing.T) { _ch.baseChallenge.Validated = clock.Now() b, err := json.Marshal(ch) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, challengeTable) assert.Equals(t, key, []byte(ch.getID())) return b, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: ch.getID(), @@ -1282,13 +1319,14 @@ func TestAuthorityUpdateAccount(t *testing.T) { tests := map[string]func(t *testing.T) test{ "fail/getAccount-error": func(t *testing.T) test { id := "foo" - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, accountTable) assert.Equals(t, key, []byte(id)) return nil, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -1302,7 +1340,7 @@ func TestAuthorityUpdateAccount(t *testing.T) { b, err := json.Marshal(acc) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { return b, nil }, @@ -1310,6 +1348,7 @@ func TestAuthorityUpdateAccount(t *testing.T) { return nil, false, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: acc.ID, @@ -1327,7 +1366,7 @@ func TestAuthorityUpdateAccount(t *testing.T) { _acc := *acc clone := &_acc clone.Contact = contact - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { return b, nil }, @@ -1337,6 +1376,7 @@ func TestAuthorityUpdateAccount(t *testing.T) { return nil, true, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: acc.ID, @@ -1384,13 +1424,14 @@ func TestAuthorityDeactivateAccount(t *testing.T) { tests := map[string]func(t *testing.T) test{ "fail/getAccount-error": func(t *testing.T) test { id := "foo" - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, accountTable) assert.Equals(t, key, []byte(id)) return nil, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: id, @@ -1403,7 +1444,7 @@ func TestAuthorityDeactivateAccount(t *testing.T) { b, err := json.Marshal(acc) assert.FatalError(t, err) - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { return b, nil }, @@ -1411,6 +1452,7 @@ func TestAuthorityDeactivateAccount(t *testing.T) { return nil, false, errors.New("force") }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: acc.ID, @@ -1428,7 +1470,7 @@ func TestAuthorityDeactivateAccount(t *testing.T) { clone := &_acc clone.Status = StatusDeactivated clone.Deactivated = clock.Now() - auth := NewAuthority(&db.MockNoSQLDB{ + auth, err := NewAuthority(&db.MockNoSQLDB{ MGet: func(bucket, key []byte) ([]byte, error) { return b, nil }, @@ -1438,6 +1480,7 @@ func TestAuthorityDeactivateAccount(t *testing.T) { return nil, true, nil }, }, "ca.smallstep.com", "acme", nil) + assert.FatalError(t, err) return test{ auth: auth, id: acc.ID, diff --git a/acme/common.go b/acme/common.go index 577c35cd..ecdd371a 100644 --- a/acme/common.go +++ b/acme/common.go @@ -12,7 +12,7 @@ import ( // SignAuthority is the interface implemented by a CA authority. type SignAuthority interface { - Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error) + Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) LoadProvisionerByID(string) (provisioner.Interface, error) } @@ -22,17 +22,6 @@ type Identifier struct { Value string `json:"value"` } -var ( - accountTable = []byte("acme-accounts") - accountByKeyIDTable = []byte("acme-keyID-accountID-index") - authzTable = []byte("acme-authzs") - challengeTable = []byte("acme-challenges") - nonceTable = []byte("nonce-table") - orderTable = []byte("acme-orders") - ordersByAccountIDTable = []byte("acme-account-orders-index") - certTable = []byte("acme-certs") -) - var ( // StatusValid -- valid StatusValid = "valid" diff --git a/acme/order.go b/acme/order.go index ab4ac659..8d22b7db 100644 --- a/acme/order.go +++ b/acme/order.go @@ -73,11 +73,11 @@ func newOrder(db nosql.DB, ops OrderOptions) (*order, error) { authzs := make([]string, len(ops.Identifiers)) for i, identifier := range ops.Identifiers { - authz, err := newAuthz(db, ops.AccountID, identifier) + az, err := newAuthz(db, ops.AccountID, identifier) if err != nil { return nil, err } - authzs[i] = authz.getID() + authzs[i] = az.getID() } now := clock.Now() @@ -203,14 +203,14 @@ func (o *order) updateStatus(db nosql.DB) (*order, error) { StatusPending: 0, } for _, azID := range o.Authorizations { - authz, err := getAuthz(db, azID) + az, err := getAuthz(db, azID) if err != nil { return nil, err } - if authz, err = authz.updateStatus(db); err != nil { + if az, err = az.updateStatus(db); err != nil { return nil, err } - st := authz.getStatus() + st := az.getStatus() count[st]++ } switch { @@ -274,7 +274,7 @@ func (o *order) finalize(db nosql.DB, csr *x509.CertificateRequest, auth SignAut } // Create and store a new certificate. - leaf, inter, err := auth.Sign(csr, provisioner.Options{ + certChain, err := auth.Sign(csr, provisioner.Options{ NotBefore: provisioner.NewTimeDuration(o.NotBefore), NotAfter: provisioner.NewTimeDuration(o.NotAfter), }, signOps...) @@ -285,8 +285,8 @@ func (o *order) finalize(db nosql.DB, csr *x509.CertificateRequest, auth SignAut cert, err := newCert(db, CertOptions{ AccountID: o.AccountID, OrderID: o.ID, - Leaf: leaf, - Intermediates: []*x509.Certificate{inter}, + Leaf: certChain[0], + Intermediates: certChain[1:], }) if err != nil { return nil, err diff --git a/acme/order_test.go b/acme/order_test.go index 31601fae..18a46589 100644 --- a/acme/order_test.go +++ b/acme/order_test.go @@ -789,19 +789,19 @@ func TestOrderUpdateStatus(t *testing.T) { } type mockSignAuth struct { - sign func(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error) + sign func(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) loadProvisionerByID func(string) (provisioner.Interface, error) ret1, ret2 interface{} err error } -func (m *mockSignAuth) Sign(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error) { +func (m *mockSignAuth) Sign(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { if m.sign != nil { return m.sign(csr, signOpts, extraOpts...) } else if m.err != nil { - return nil, nil, m.err + return nil, m.err } - return m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate), m.err + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err } func (m *mockSignAuth) LoadProvisionerByID(id string) (provisioner.Interface, error) { @@ -1082,9 +1082,9 @@ func TestOrderFinalize(t *testing.T) { res: clone, csr: csr, sa: &mockSignAuth{ - sign: func(csr *x509.CertificateRequest, pops provisioner.Options, signOps ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error) { + sign: func(csr *x509.CertificateRequest, pops provisioner.Options, signOps ...provisioner.SignOption) ([]*x509.Certificate, error) { assert.Equals(t, len(signOps), 4) - return crt, inter, nil + return []*x509.Certificate{crt, inter}, nil }, }, db: &db.MockNoSQLDB{ diff --git a/api/api.go b/api/api.go index afe62762..0284167f 100644 --- a/api/api.go +++ b/api/api.go @@ -33,8 +33,8 @@ type Authority interface { AuthorizeSign(ott string) ([]provisioner.SignOption, error) GetTLSOptions() *tlsutil.TLSOptions Root(shasum string) (*x509.Certificate, error) - Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error) - Renew(peer *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) + Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) + Renew(peer *x509.Certificate) ([]*x509.Certificate, error) LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error) LoadProvisionerByID(string) (provisioner.Interface, error) GetProvisioners(cursor string, limit int) (provisioner.List, string, error) @@ -211,10 +211,11 @@ func (s *SignRequest) Validate() error { // SignResponse is the response object of the certificate signature request. type SignResponse struct { - ServerPEM Certificate `json:"crt"` - CaPEM Certificate `json:"ca"` - TLSOptions *tlsutil.TLSOptions `json:"tlsOptions,omitempty"` - TLS *tls.ConnectionState `json:"-"` + ServerPEM Certificate `json:"crt"` + CaPEM Certificate `json:"ca"` + CertChainPEM []Certificate `json:"certChain"` + TLSOptions *tlsutil.TLSOptions `json:"tlsOptions,omitempty"` + TLS *tls.ConnectionState `json:"-"` } // RootsResponse is the response object of the roots request. @@ -282,6 +283,14 @@ func (h *caHandler) Root(w http.ResponseWriter, r *http.Request) { JSON(w, &RootResponse{RootPEM: Certificate{cert}}) } +func certChainToPEM(certChain []*x509.Certificate) []Certificate { + certChainPEM := make([]Certificate, 0, len(certChain)) + for _, c := range certChain { + certChainPEM = append(certChainPEM, Certificate{c}) + } + return certChainPEM +} + // Sign is an HTTP handler that reads a certificate request and an // one-time-token (ott) from the body and creates a new certificate with the // information in the certificate request. @@ -309,17 +318,22 @@ func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) { return } - cert, root, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...) + certChain, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...) if err != nil { WriteError(w, Forbidden(err)) return } - - logCertificate(w, cert) + certChainPEM := certChainToPEM(certChain) + var caPEM Certificate + if len(certChainPEM) > 0 { + caPEM = certChainPEM[1] + } + logCertificate(w, certChain[0]) JSONStatus(w, &SignResponse{ - ServerPEM: Certificate{cert}, - CaPEM: Certificate{root}, - TLSOptions: h.Authority.GetTLSOptions(), + ServerPEM: certChainPEM[0], + CaPEM: caPEM, + CertChainPEM: certChainPEM, + TLSOptions: h.Authority.GetTLSOptions(), }, http.StatusCreated) } @@ -331,17 +345,23 @@ func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) { return } - cert, root, err := h.Authority.Renew(r.TLS.PeerCertificates[0]) + certChain, err := h.Authority.Renew(r.TLS.PeerCertificates[0]) if err != nil { WriteError(w, Forbidden(err)) return } + certChainPEM := certChainToPEM(certChain) + var caPEM Certificate + if len(certChainPEM) > 0 { + caPEM = certChainPEM[1] + } - logCertificate(w, cert) + logCertificate(w, certChain[0]) JSONStatus(w, &SignResponse{ - ServerPEM: Certificate{cert}, - CaPEM: Certificate{root}, - TLSOptions: h.Authority.GetTLSOptions(), + ServerPEM: certChainPEM[0], + CaPEM: caPEM, + CertChainPEM: certChainPEM, + TLSOptions: h.Authority.GetTLSOptions(), }, http.StatusCreated) } diff --git a/api/api_test.go b/api/api_test.go index e42de188..6232dde5 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -502,10 +502,10 @@ type mockAuthority struct { authorizeSign func(ott string) ([]provisioner.SignOption, error) getTLSOptions func() *tlsutil.TLSOptions root func(shasum string) (*x509.Certificate, error) - sign func(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error) + sign func(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) signSSH func(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) signSSHAddUser func(key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) - renew func(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) + renew func(cert *x509.Certificate) ([]*x509.Certificate, error) loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error) loadProvisionerByID func(provID string) (provisioner.Interface, error) getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) @@ -545,11 +545,11 @@ func (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) { return m.ret1.(*x509.Certificate), m.err } -func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error) { +func (m *mockAuthority) Sign(cr *x509.CertificateRequest, opts provisioner.Options, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { if m.sign != nil { return m.sign(cr, opts, signOpts...) } - return m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate), m.err + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err } func (m *mockAuthority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) { @@ -566,11 +566,11 @@ func (m *mockAuthority) SignSSHAddUser(key ssh.PublicKey, cert *ssh.Certificate) return m.ret1.(*ssh.Certificate), m.err } -func (m *mockAuthority) Renew(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) { +func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) { if m.renew != nil { return m.renew(cert) } - return m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate), m.err + return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err } func (m *mockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) { @@ -757,8 +757,8 @@ func Test_caHandler_Sign(t *testing.T) { t.Fatal(err) } - expected1 := []byte(`{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"}`) - expected2 := []byte(`{"crt":"` + strings.Replace(stepCertPEM, "\n", `\n`, -1) + `\n","ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"}`) + expected1 := []byte(`{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n","certChain":["` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"]}`) + expected2 := []byte(`{"crt":"` + strings.Replace(stepCertPEM, "\n", `\n`, -1) + `\n","ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n","certChain":["` + strings.Replace(stepCertPEM, "\n", `\n`, -1) + `\n","` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"]}`) tests := []struct { name string @@ -831,7 +831,7 @@ func Test_caHandler_Renew(t *testing.T) { {"renew error", cs, nil, nil, fmt.Errorf("an error"), http.StatusForbidden}, } - expected := []byte(`{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"}`) + expected := []byte(`{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n","certChain":["` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"]}`) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 2d78e5a2..d1933d47 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -69,10 +69,12 @@ func (p *ACME) AuthorizeSign(ctx context.Context, _ string) ([]SignOption, error return nil, errors.Errorf("unexpected method type %d in context", m) } return []SignOption{ - profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // modifiers / withOptions newProvisionerExtensionOption(TypeACME, p.Name, ""), - newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // validators defaultPublicKeyValidator{}, + newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), }, nil } diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index c5e6b13a..51231ba3 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -155,28 +155,23 @@ func TestACME_AuthorizeSign(t *testing.T) { if assert.NotNil(t, got) { assert.Len(t, 4, got) - _pdd := got[0] - pdd, ok := _pdd.(profileDefaultDuration) - assert.True(t, ok) - assert.Equals(t, pdd, profileDefaultDuration(86400000000000)) - - _peo := got[1] - peo, ok := _peo.(*provisionerExtensionOption) - assert.True(t, ok) - assert.Equals(t, peo.Type, 6) - assert.Equals(t, peo.Name, "test@acme-provisioner.com") - assert.Equals(t, peo.CredentialID, "") - assert.Equals(t, peo.KeyValuePairs, nil) - - _vv := got[2] - vv, ok := _vv.(*validityValidator) - assert.True(t, ok) - assert.Equals(t, vv.min, time.Duration(300000000000)) - assert.Equals(t, vv.max, time.Duration(86400000000000)) - - _dpkv := got[3] - _, ok = _dpkv.(defaultPublicKeyValidator) - assert.True(t, ok) + for _, o := range got { + switch v := o.(type) { + case *provisionerExtensionOption: + assert.Equals(t, v.Type, int(TypeACME)) + assert.Equals(t, v.Name, tt.prov.GetName()) + assert.Equals(t, v.CredentialID, "") + assert.Len(t, 0, v.KeyValuePairs) + case profileDefaultDuration: + assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration()) + case defaultPublicKeyValidator: + case *validityValidator: + assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration()) + assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration()) + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } + } } } }) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index c6360dbd..e1b2ef9d 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -274,8 +274,8 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } // Check for the sign ssh method, default to sign X.509 - if m := MethodFromContext(ctx); m == SignSSHMethod { - if p.claimer.IsSSHCAEnabled() == false { + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) } return p.authorizeSSHSign(payload) @@ -296,10 +296,12 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } return append(so, + // modifiers / withOptions + newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID), + profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // validators defaultPublicKeyValidator{}, commonNameValidator(payload.Claims.Subject), - profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), - newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID), newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), ), nil } @@ -466,13 +468,15 @@ func (p *AWS) authorizeSSHSign(claims *awsPayload) ([]SignOption, error) { signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) return append(signOptions, - // set the default extensions + // Set the default extensions. &sshDefaultExtensionModifier{}, - // checks the validity bounds, and set the validity if has not been set - &sshCertificateValidityModifier{p.claimer}, - // validate public key + // Set the validity bounds if not set. + sshDefaultValidityModifier(p.claimer), + // Validate public key &sshDefaultPublicKeyValidator{}, - // require all the fields in the SSH certificate + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require all the fields in the SSH certificate &sshCertificateDefaultValidator{}, ), nil } diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index e5e858dd..d8252799 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -266,8 +266,8 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } // Check for the sign ssh method, default to sign X.509 - if m := MethodFromContext(ctx); m == SignSSHMethod { - if p.claimer.IsSSHCAEnabled() == false { + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) } return p.authorizeSSHSign(claims, name) @@ -284,9 +284,11 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, } return append(so, - defaultPublicKeyValidator{}, - profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // modifiers / withOptions newProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID), + profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // validators + defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), ), nil } @@ -323,13 +325,15 @@ func (p *Azure) authorizeSSHSign(claims azurePayload, name string) ([]SignOption signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) return append(signOptions, - // set the default extensions + // Set the default extensions. &sshDefaultExtensionModifier{}, - // checks the validity bounds, and set the validity if has not been set - &sshCertificateValidityModifier{p.claimer}, - // validate public key + // Set the validity bounds if not set. + sshDefaultValidityModifier(p.claimer), + // Validate public key &sshDefaultPublicKeyValidator{}, - // require all the fields in the SSH certificate + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require all the fields in the SSH certificate &sshCertificateDefaultValidator{}, ), nil } diff --git a/authority/provisioner/collection.go b/authority/provisioner/collection.go index 906c5260..0d14a65b 100644 --- a/authority/provisioner/collection.go +++ b/authority/provisioner/collection.go @@ -129,6 +129,8 @@ func (c *Collection) LoadByCertificate(cert *x509.Certificate) (Interface, bool) return c.Load("gcp/" + string(provisioner.Name)) case TypeACME: return c.Load("acme/" + string(provisioner.Name)) + case TypeX5C: + return c.Load("x5c/" + string(provisioner.Name)) default: return c.Load(string(provisioner.CredentialID)) } diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 63492fd4..b2ec509a 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -213,8 +213,8 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } // Check for the sign ssh method, default to sign X.509 - if m := MethodFromContext(ctx); m == SignSSHMethod { - if p.claimer.IsSSHCAEnabled() == false { + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) } return p.authorizeSSHSign(claims) @@ -237,9 +237,11 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } return append(so, - defaultPublicKeyValidator{}, - profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // modifiers / withOptions newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject, "InstanceID", ce.InstanceID, "InstanceName", ce.InstanceName), + profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // validators + defaultPublicKeyValidator{}, newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), ), nil } @@ -378,13 +380,15 @@ func (p *GCP) authorizeSSHSign(claims *gcpPayload) ([]SignOption, error) { signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) return append(signOptions, - // set the default extensions + // Set the default extensions &sshDefaultExtensionModifier{}, - // checks the validity bounds, and set the validity if has not been set - &sshCertificateValidityModifier{p.claimer}, - // validate public key + // Set the validity bounds if not set. + sshDefaultValidityModifier(p.claimer), + // Validate public key &sshDefaultPublicKeyValidator{}, - // require all the fields in the SSH certificate + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require all the fields in the SSH certificate &sshCertificateDefaultValidator{}, ), nil } diff --git a/authority/provisioner/jwk.go b/authority/provisioner/jwk.go index cdc90ff6..f9178bb7 100644 --- a/authority/provisioner/jwk.go +++ b/authority/provisioner/jwk.go @@ -141,9 +141,9 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er return nil, err } - // Check for SSH token - if claims.Step != nil && claims.Step.SSH != nil { - if p.claimer.IsSSHCAEnabled() == false { + // Check for SSH sign-ing request. + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) } return p.authorizeSSHSign(claims) @@ -158,13 +158,15 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er dnsNames, ips, emails := x509util.SplitSANs(claims.SANs) return []SignOption{ - defaultPublicKeyValidator{}, - commonNameValidator(claims.Subject), - dnsNamesValidator(dnsNames), - ipAddressesValidator(ips), - emailAddressesValidator(emails), - profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // modifiers / withOptions newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID), + profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), + // validators + commonNameValidator(claims.Subject), + defaultPublicKeyValidator{}, + dnsNamesValidator(dnsNames), + emailAddressesValidator(emails), + ipAddressesValidator(ips), newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), }, nil } @@ -180,6 +182,9 @@ func (p *JWK) AuthorizeRenewal(cert *x509.Certificate) error { // authorizeSSHSign returns the list of SignOption for a SignSSH request. func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) { t := now() + if claims.Step == nil || claims.Step.SSH == nil { + return nil, errors.New("authorization token must be an SSH provisioning token") + } opts := claims.Step.SSH signOptions := []SignOption{ // validates user's SSHOptions with the ones in the token @@ -206,13 +211,15 @@ func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) { signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert}) return append(signOptions, - // set the default extensions + // Set the default extensions. &sshDefaultExtensionModifier{}, - // checks the validity bounds, and set the validity if has not been set - &sshCertificateValidityModifier{p.claimer}, - // validate public key + // Set the validity bounds if not set. + sshDefaultValidityModifier(p.claimer), + // Validate public key &sshDefaultPublicKeyValidator{}, - // require all the fields in the SSH certificate + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require and validate all the default fields in the SSH certificate. &sshCertificateDefaultValidator{}, ), nil } diff --git a/authority/provisioner/jwk_test.go b/authority/provisioner/jwk_test.go index 0e7ed57a..185f1596 100644 --- a/authority/provisioner/jwk_test.go +++ b/authority/provisioner/jwk_test.go @@ -6,33 +6,16 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" - "errors" + "net" "strings" "testing" "time" + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/cli/jose" ) -var ( - defaultDisableRenewal = false - defaultEnableSSHCA = true - globalProvisionerClaims = Claims{ - MinTLSDur: &Duration{5 * time.Minute}, - MaxTLSDur: &Duration{24 * time.Hour}, - DefaultTLSDur: &Duration{24 * time.Hour}, - DisableRenewal: &defaultDisableRenewal, - MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs - MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, - DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour}, - MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs - MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, - DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, - EnableSSHCA: &defaultEnableSSHCA, - } -) - func TestJWK_Getters(t *testing.T) { p, err := generateJWK() assert.FatalError(t, err) @@ -247,7 +230,7 @@ func TestJWK_AuthorizeSign(t *testing.T) { key1, err := decryptJSONWebKey(p1.EncryptedKey) assert.FatalError(t, err) - t1, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], key1) + t1, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "name@smallstep.com", []string{"127.0.0.1", "max@smallstep.com", "foo"}, time.Now(), key1) assert.FatalError(t, err) t2, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "name@smallstep.com", []string{}, time.Now(), key1) @@ -260,14 +243,17 @@ func TestJWK_AuthorizeSign(t *testing.T) { token string } tests := []struct { - name string - prov *JWK - args args - err error + name string + prov *JWK + args args + err error + dns []string + emails []string + ips []net.IP }{ - {"fail-signature", p1, args{failSig}, errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")}, - {"ok-sans", p1, args{t1}, nil}, - {"ok-no-sans", p1, args{t2}, nil}, + {name: "fail-signature", prov: p1, args: args{failSig}, err: errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")}, + {"ok-sans", p1, args{t1}, nil, []string{"foo"}, []string{"max@smallstep.com"}, []net.IP{net.ParseIP("127.0.0.1")}}, + {"ok-no-sans", p1, args{t2}, nil, []string{"subject"}, []string{}, []net.IP{}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -279,19 +265,30 @@ func TestJWK_AuthorizeSign(t *testing.T) { } else { if assert.NotNil(t, got) { assert.Len(t, 8, got) - - _cnv := got[1] - cnv, ok := _cnv.(commonNameValidator) - assert.True(t, ok) - assert.Equals(t, string(cnv), "subject") - - _dnv := got[2] - dnv, ok := _dnv.(dnsNamesValidator) - assert.True(t, ok) - if tt.name == "ok-sans" { - assert.Equals(t, []string(dnv), []string{"test.smallstep.com"}) - } else { - assert.Equals(t, []string(dnv), []string{"subject"}) + for _, o := range got { + switch v := o.(type) { + case *provisionerExtensionOption: + assert.Equals(t, v.Type, int(TypeJWK)) + assert.Equals(t, v.Name, tt.prov.GetName()) + assert.Equals(t, v.CredentialID, tt.prov.Key.KeyID) + assert.Len(t, 0, v.KeyValuePairs) + case profileDefaultDuration: + assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration()) + case commonNameValidator: + assert.Equals(t, string(v), "subject") + case defaultPublicKeyValidator: + case dnsNamesValidator: + assert.Equals(t, []string(v), tt.dns) + case emailAddressesValidator: + assert.Equals(t, []string(v), tt.emails) + case ipAddressesValidator: + assert.Equals(t, []net.IP(v), tt.ips) + case *validityValidator: + assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration()) + assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration()) + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } } } } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index d4937470..b65d9b6f 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -285,26 +285,19 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e } // Check for the sign ssh method, default to sign X.509 - if m := MethodFromContext(ctx); m == SignSSHMethod { - if o.claimer.IsSSHCAEnabled() == false { + if MethodFromContext(ctx) == SignSSHMethod { + if !o.claimer.IsSSHCAEnabled() { return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID()) } return o.authorizeSSHSign(claims) } - // Admins should be able to authorize any SAN - if o.IsAdmin(claims.Email) { - return []SignOption{ - profileDefaultDuration(o.claimer.DefaultTLSCertDuration()), - newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID), - newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()), - }, nil - } - so := []SignOption{ - defaultPublicKeyValidator{}, - profileDefaultDuration(o.claimer.DefaultTLSCertDuration()), + // modifiers / withOptions newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID), + profileDefaultDuration(o.claimer.DefaultTLSCertDuration()), + // validators + defaultPublicKeyValidator{}, newValidityValidator(o.claimer.MinTLSCertDuration(), o.claimer.MaxTLSCertDuration()), } // Admins should be able to authorize any SAN @@ -350,13 +343,15 @@ func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) { signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults)) return append(signOptions, - // set the default extensions + // Set the default extensions &sshDefaultExtensionModifier{}, - // checks the validity bounds, and set the validity if has not been set - &sshCertificateValidityModifier{o.claimer}, - // validate public key + // Set the validity bounds if not set. + sshDefaultValidityModifier(o.claimer), + // Validate public key &sshDefaultPublicKeyValidator{}, - // require all the fields in the SSH certificate + // Validate the validity period. + &sshCertificateValidityValidator{o.claimer}, + // Require all the fields in the SSH certificate &sshCertificateDefaultValidator{}, ), nil } diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index 6e99a0fb..516e0f0e 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/cli/jose" ) @@ -298,11 +299,31 @@ func TestOIDC_AuthorizeSign(t *testing.T) { if err != nil { assert.Nil(t, got) } else { - assert.NotNil(t, got) - if tt.name == "admin" { - assert.Len(t, 3, got) - } else { - assert.Len(t, 5, got) + if assert.NotNil(t, got) { + if tt.name == "admin" { + assert.Len(t, 4, got) + } else { + assert.Len(t, 5, got) + } + for _, o := range got { + switch v := o.(type) { + case *provisionerExtensionOption: + assert.Equals(t, v.Type, int(TypeOIDC)) + assert.Equals(t, v.Name, tt.prov.GetName()) + assert.Equals(t, v.CredentialID, tt.prov.ClientID) + assert.Len(t, 0, v.KeyValuePairs) + case profileDefaultDuration: + assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration()) + case defaultPublicKeyValidator: + case *validityValidator: + assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration()) + assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration()) + case emailOnlyIdentity: + assert.Equals(t, string(v), "name@smallstep.com") + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } + } } } }) diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 2a63161c..155e34de 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -86,6 +86,8 @@ const ( TypeAzure Type = 5 // TypeACME is used to indicate the ACME provisioners. TypeACME Type = 6 + // TypeX5C is used to indicate the X5C provisioners. + TypeX5C Type = 7 // RevokeAudienceKey is the key for the 'revoke' audiences in the audiences map. RevokeAudienceKey = "revoke" @@ -108,6 +110,8 @@ func (t Type) String() string { return "Azure" case TypeACME: return "ACME" + case TypeX5C: + return "X5C" default: return "" } @@ -157,6 +161,8 @@ func (l *List) UnmarshalJSON(data []byte) error { p = &Azure{} case "acme": p = &ACME{} + case "x5c": + p = &X5C{} default: // Skip unsupported provisioners. A client using this method may be // compiled with a version of smallstep/certificates that does not diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index d535dfb6..53921a3c 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -53,19 +53,6 @@ func (v profileWithOption) Option(Options) x509util.WithOption { return x509util.WithOption(v) } -// profileDefaultDuration is a wrapper against x509util.WithOption to conform the -// interface. -type profileDefaultDuration time.Duration - -func (v profileDefaultDuration) Option(so Options) x509util.WithOption { - notBefore := so.NotBefore.Time() - if notBefore.IsZero() { - notBefore = time.Now() - } - notAfter := so.NotAfter.RelativeTime(notBefore) - return x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(v)) -} - // emailOnlyIdentity is a CertificateRequestValidator that checks that the only // SAN provided is the given email address. type emailOnlyIdentity string @@ -197,7 +184,61 @@ func (v emailAddressesValidator) Valid(req *x509.CertificateRequest) error { return nil } -// validityValidator validates the certificate temporal validity settings. +// profileDefaultDuration is a wrapper against x509util.WithOption to conform +// the SignOption interface. +type profileDefaultDuration time.Duration + +func (v profileDefaultDuration) Option(so Options) x509util.WithOption { + notBefore := so.NotBefore.Time() + if notBefore.IsZero() { + notBefore = time.Now() + } + notAfter := so.NotAfter.RelativeTime(notBefore) + return x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(v)) +} + +// profileLimitDuration is an x509 profile option that modifies an x509 validity +// period according to an imposed expiration time. +type profileLimitDuration struct { + def time.Duration + notAfter time.Time +} + +// Option returns an x509util option that limits the validity period of a +// certificate to one that is superficially imposed. +func (v profileLimitDuration) Option(so Options) x509util.WithOption { + return func(p x509util.Profile) error { + n := now() + notBefore := so.NotBefore.Time() + if notBefore.IsZero() { + notBefore = n + } + if notBefore.After(v.notAfter) { + return errors.Errorf("provisioning credential expiration (%s) is before "+ + "requested certificate notBefore (%s)", v.notAfter, notBefore) + } + + notAfter := so.NotAfter.RelativeTime(notBefore) + if notAfter.After(v.notAfter) { + return errors.Errorf("provisioning credential expiration (%s) is before "+ + "requested certificate notAfter (%s)", v.notAfter, notBefore) + } + if notAfter.IsZero() { + t := notBefore.Add(v.def) + if t.After(v.notAfter) { + notAfter = v.notAfter + } else { + notAfter = t + } + } + crt := p.Subject() + crt.NotBefore = notBefore + crt.NotAfter = notAfter + return nil + } +} + +// validityValidator validates the certificate validity settings. type validityValidator struct { min time.Duration max time.Duration @@ -208,7 +249,8 @@ func newValidityValidator(min, max time.Duration) *validityValidator { return &validityValidator{min: min, max: max} } -// Validate validates the certificate temporal validity settings. +// Valid validates the certificate validity settings (notBefore/notAfter) and +// and total duration. func (v *validityValidator) Valid(crt *x509.Certificate) error { var ( na = crt.NotAfter diff --git a/authority/provisioner/sign_options_test.go b/authority/provisioner/sign_options_test.go index 38f1d5e6..8a452dab 100644 --- a/authority/provisioner/sign_options_test.go +++ b/authority/provisioner/sign_options_test.go @@ -273,3 +273,87 @@ func Test_validityValidator_Valid(t *testing.T) { }) } } + +func Test_profileLimitDuration_Option(t *testing.T) { + n := now() + type test struct { + pld profileLimitDuration + so Options + cert *x509.Certificate + valid func(*x509.Certificate) + err error + } + tests := map[string]func() test{ + "fail/notBefore-after-limit": func() test { + d, err := ParseTimeDuration("8h") + assert.FatalError(t, err) + return test{ + pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{NotBefore: d}, + cert: new(x509.Certificate), + err: errors.New("provisioning credential expiration ("), + } + }, + "fail/requested-notAfter-after-limit": func() test { + d, err := ParseTimeDuration("4h") + assert.FatalError(t, err) + return test{ + pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d}, + cert: new(x509.Certificate), + err: errors.New("provisioning credential expiration ("), + } + }, + "ok/valid-notAfter-requested": func() test { + d, err := ParseTimeDuration("2h") + assert.FatalError(t, err) + return test{ + pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour)) + assert.Equals(t, cert.NotAfter, n.Add(5*time.Hour)) + }, + } + }, + "ok/valid-notAfter-nil-limit-over-default": func() test { + return test{ + pld: profileLimitDuration{def: 1 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour))}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour)) + assert.Equals(t, cert.NotAfter, n.Add(4*time.Hour)) + }, + } + }, + "ok/valid-notAfter-nil-limit-under-default": func() test { + return test{ + pld: profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)}, + so: Options{NotBefore: NewTimeDuration(n.Add(3 * time.Hour))}, + cert: new(x509.Certificate), + valid: func(cert *x509.Certificate) { + assert.Equals(t, cert.NotBefore, n.Add(3*time.Hour)) + assert.Equals(t, cert.NotAfter, n.Add(6*time.Hour)) + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tt := run() + prof := &x509util.Leaf{} + prof.SetSubject(tt.cert) + if err := tt.pld.Option(tt.so)(prof); err != nil { + if assert.NotNil(t, tt.err) { + assert.HasPrefix(t, err.Error(), tt.err.Error()) + } + } else { + if assert.Nil(t, tt.err) { + tt.valid(prof.Subject()) + } + } + }) + } +} diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index 94a77c50..a8f63cd5 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -191,47 +191,67 @@ func (m *sshDefaultExtensionModifier) Modify(cert *ssh.Certificate) error { } } -// sshCertificateValidityModifier is a SSHCertificateModifier checks the +// sshValidityModifier is an SSHCertificateModifier that checks the // validity bounds, setting them if they are not provided. It will fail if a // CertType has not been set or is not valid. -type sshCertificateValidityModifier struct { +type sshValidityModifier struct { *Claimer + validBefore time.Time } -func (m *sshCertificateValidityModifier) Modify(cert *ssh.Certificate) error { - var d, min, max time.Duration +func (m *sshValidityModifier) Modify(cert *ssh.Certificate) error { + var d time.Duration + switch cert.CertType { case ssh.UserCert: d = m.DefaultUserSSHCertDuration() - min = m.MinUserSSHCertDuration() - max = m.MaxUserSSHCertDuration() case ssh.HostCert: d = m.DefaultHostSSHCertDuration() - min = m.MinHostSSHCertDuration() - max = m.MaxHostSSHCertDuration() case 0: return errors.New("ssh certificate type has not been set") default: return errors.Errorf("unknown ssh certificate type %d", cert.CertType) } + hasLimit := !m.validBefore.IsZero() + + n := now() if cert.ValidAfter == 0 { - cert.ValidAfter = uint64(now().Truncate(time.Second).Unix()) + cert.ValidAfter = uint64(n.Truncate(time.Second).Unix()) } - if cert.ValidBefore == 0 { - t := time.Unix(int64(cert.ValidAfter), 0) - cert.ValidBefore = uint64(t.Add(d).Unix()) + certValidAfter := time.Unix(int64(cert.ValidAfter), 0) + if hasLimit && certValidAfter.After(m.validBefore) { + return errors.Errorf("provisioning credential expiration (%s) is before "+ + "requested certificate validAfter (%s)", m.validBefore, certValidAfter) } - diff := time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second - switch { - case diff < min: - return errors.Errorf("ssh certificate duration cannot be lower than %s", min) - case diff > max: - return errors.Errorf("ssh certificate duration cannot be greater than %s", max) - default: - return nil + if cert.ValidBefore == 0 { + certValidBefore := certValidAfter.Add(d) + if hasLimit && m.validBefore.Before(certValidBefore) { + certValidBefore = m.validBefore + } + cert.ValidBefore = uint64(certValidBefore.Unix()) + } else if hasLimit { + certValidBefore := time.Unix(int64(cert.ValidBefore), 0) + if m.validBefore.Before(certValidBefore) { + return errors.Errorf("provisioning credential expiration (%s) is before "+ + "requested certificate validBefore (%s)", m.validBefore, certValidBefore) + } } + + return nil +} + +func sshDefaultValidityModifier(c *Claimer) SSHCertificateModifier { + return &sshValidityModifier{c, time.Time{}} +} + +// sshLimitValidityModifier adjusts the duration to +// min(default, remaining provisioning credential duration). +// E.g. if the default is 12hrs but the remaining validity of the provisioning +// credential is only 4hrs, this option will set the value to 4hrs (the min of the two values). +func sshLimitValidityModifier(c *Claimer, validBefore time.Time) SSHCertificateModifier { + return &sshValidityModifier{c, validBefore} } // sshCertificateOptionsValidator validates the user SSHOptions with the ones @@ -245,6 +265,48 @@ func (v sshCertificateOptionsValidator) Valid(got SSHOptions) error { return want.match(got) } +type sshCertificateValidityValidator struct { + *Claimer +} + +func (v *sshCertificateValidityValidator) Valid(cert *ssh.Certificate) error { + switch { + case cert.ValidAfter == 0: + return errors.New("ssh certificate validAfter cannot be 0") + case cert.ValidBefore < uint64(now().Unix()): + return errors.New("ssh certificate validBefore cannot be in the past") + case cert.ValidBefore < cert.ValidAfter: + return errors.New("ssh certificate validBefore cannot be before validAfter") + } + + var min, max time.Duration + switch cert.CertType { + case ssh.UserCert: + min = v.MinUserSSHCertDuration() + max = v.MaxUserSSHCertDuration() + case ssh.HostCert: + min = v.MinHostSSHCertDuration() + max = v.MaxHostSSHCertDuration() + case 0: + return errors.New("ssh certificate type has not been set") + default: + return errors.Errorf("unknown ssh certificate type %d", cert.CertType) + } + + // seconds + dur := time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second + switch { + case dur < min: + return errors.Errorf("requested duration of %s is less than minimum "+ + "accepted duration for selected provisioner of %s", dur, min) + case dur > max: + return errors.Errorf("requested duration of %s is greater than maximum "+ + "accepted duration for selected provisioner of %s", dur, max) + default: + return nil + } +} + // sshCertificateDefaultValidator implements a simple validator for all the // fields in the SSH certificate. type sshCertificateDefaultValidator struct{} diff --git a/authority/provisioner/sign_ssh_options_test.go b/authority/provisioner/sign_ssh_options_test.go index 95c6913c..25a44121 100644 --- a/authority/provisioner/sign_ssh_options_test.go +++ b/authority/provisioner/sign_ssh_options_test.go @@ -190,3 +190,197 @@ func Test_sshCertificateDefaultValidator_Valid(t *testing.T) { }) } } + +func Test_sshCertificateValidityValidator(t *testing.T) { + p, err := generateX5C(nil) + assert.FatalError(t, err) + v := sshCertificateValidityValidator{p.claimer} + n := now() + tests := []struct { + name string + cert *ssh.Certificate + err error + }{ + { + "fail/validAfter-0", + &ssh.Certificate{CertType: ssh.UserCert}, + errors.New("ssh certificate validAfter cannot be 0"), + }, + { + "fail/validBefore-in-past", + &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(-time.Minute).Unix())}, + errors.New("ssh certificate validBefore cannot be in the past"), + }, + { + "fail/validBefore-before-validAfter", + &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Add(5 * time.Minute).Unix()), ValidBefore: uint64(now().Add(3 * time.Minute).Unix())}, + errors.New("ssh certificate validBefore cannot be before validAfter"), + }, + { + "fail/cert-type-not-set", + &ssh.Certificate{ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(10 * time.Minute).Unix())}, + errors.New("ssh certificate type has not been set"), + }, + { + "fail/unexpected-cert-type", + &ssh.Certificate{ + CertType: 3, + ValidAfter: uint64(now().Unix()), + ValidBefore: uint64(now().Add(10 * time.Minute).Unix()), + }, + errors.New("unknown ssh certificate type 3"), + }, + { + "fail/durationmax", + &ssh.Certificate{ + CertType: 1, + ValidAfter: uint64(n.Unix()), + ValidBefore: uint64(n.Add(48 * time.Hour).Unix()), + }, + errors.New("requested duration of 48h0m0s is greater than maximum accepted duration for selected provisioner of 24h0m0s"), + }, + { + "ok", + &ssh.Certificate{ + CertType: 1, + ValidAfter: uint64(now().Unix()), + ValidBefore: uint64(now().Add(8 * time.Hour).Unix()), + }, + nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := v.Valid(tt.cert); err != nil { + if assert.NotNil(t, tt.err) { + assert.HasPrefix(t, err.Error(), tt.err.Error()) + } + } else { + assert.Nil(t, tt.err) + } + }) + } +} + +func Test_sshValidityModifier(t *testing.T) { + n := now() + p, err := generateX5C(nil) + assert.FatalError(t, err) + type test struct { + svm *sshValidityModifier + cert *ssh.Certificate + valid func(*ssh.Certificate) + err error + } + tests := map[string]func() test{ + "fail/type-not-set": func() test { + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(6 * time.Hour)}, + cert: &ssh.Certificate{ + ValidAfter: uint64(n.Unix()), + ValidBefore: uint64(n.Add(8 * time.Hour).Unix()), + }, + err: errors.New("ssh certificate type has not been set"), + } + }, + "fail/type-not-recognized": func() test { + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(6 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 4, + ValidAfter: uint64(n.Unix()), + ValidBefore: uint64(n.Add(8 * time.Hour).Unix()), + }, + err: errors.New("unknown ssh certificate type 4"), + } + }, + "fail/requested-validAfter-after-limit": func() test { + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(1 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 1, + ValidAfter: uint64(n.Add(2 * time.Hour).Unix()), + ValidBefore: uint64(n.Add(8 * time.Hour).Unix()), + }, + err: errors.Errorf("provisioning credential expiration ("), + } + }, + "fail/requested-validBefore-after-limit": func() test { + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(1 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 1, + ValidAfter: uint64(n.Unix()), + ValidBefore: uint64(n.Add(2 * time.Hour).Unix()), + }, + err: errors.New("provisioning credential expiration ("), + } + }, + "ok/valid-requested-validBefore": func() test { + va, vb := uint64(n.Unix()), uint64(n.Add(2*time.Hour).Unix()) + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(3 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 1, + ValidAfter: va, + ValidBefore: vb, + }, + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.ValidAfter, va) + assert.Equals(t, cert.ValidBefore, vb) + }, + } + }, + "ok/empty-requested-validBefore-limit-after-default": func() test { + va := uint64(n.Unix()) + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(5 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 1, + ValidAfter: va, + }, + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.ValidAfter, va) + assert.Equals(t, cert.ValidBefore, uint64(n.Add(4*time.Hour).Unix())) + }, + } + }, + "ok/empty-requested-validBefore-limit-before-default": func() test { + va := uint64(n.Unix()) + return test{ + svm: &sshValidityModifier{Claimer: p.claimer, validBefore: n.Add(3 * time.Hour)}, + cert: &ssh.Certificate{ + CertType: 1, + ValidAfter: va, + }, + valid: func(cert *ssh.Certificate) { + assert.Equals(t, cert.ValidAfter, va) + assert.Equals(t, cert.ValidBefore, uint64(n.Add(3*time.Hour).Unix())) + }, + } + }, + } + for name, run := range tests { + t.Run(name, func(t *testing.T) { + tt := run() + if err := tt.svm.Modify(tt.cert); err != nil { + if assert.NotNil(t, tt.err) { + assert.HasPrefix(t, err.Error(), tt.err.Error()) + } + } else { + if assert.Nil(t, tt.err) { + tt.valid(tt.cert) + } + } + }) + } +} diff --git a/authority/provisioner/testdata/x5c-leaf.crt b/authority/provisioner/testdata/x5c-leaf.crt new file mode 100644 index 00000000..2d674c11 --- /dev/null +++ b/authority/provisioner/testdata/x5c-leaf.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIBuDCCAV+gAwIBAgIQFdu723gqgGaTaqjf6ny88zAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMzE4NTNaGA8yMTE5 +MDkwODAzMTg1MVowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvr +VpgSPXYruNRFduPWX564Abz/TDmb276JbKGeQqOBiDCBhTAOBgNVHQ8BAf8EBAMC +BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBReMkPW +f4MNWdg7KN4xI4ZLJd0IJDAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp +czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDRwAwRAIgKYLKXpTN +wtvZZaIvDzq1p8MO/SZ8yI42Ot69dNk/QtkCIBSvg5PozYcfbvwkgX5SwsjfYu0Z +AvUgkUQ2G25NBRmX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw +EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw +MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl +qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC +AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99 +oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw +E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1 +2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC +lgsqsR63is+0YQ== +-----END CERTIFICATE----- diff --git a/authority/provisioner/testdata/x5c-leaf.key b/authority/provisioner/testdata/x5c-leaf.key new file mode 100644 index 00000000..f77d015e --- /dev/null +++ b/authority/provisioner/testdata/x5c-leaf.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIALytC4LyTTAagMLMv+rzq2vtfhFkhuyBz4kqsnRs6zioAoGCCqGSM49 +AwEHoUQDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvrVpgS +PXYruNRFduPWX564Abz/TDmb276JbKGeQg== +-----END EC PRIVATE KEY----- diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 2760a16b..8f4fbaad 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -19,9 +19,44 @@ import ( "github.com/smallstep/cli/jose" ) -var testAudiences = Audiences{ - Sign: []string{"https://ca.smallstep.com/sign", "https://ca.smallstep.com/1.0/sign"}, - Revoke: []string{"https://ca.smallstep.com/revoke", "https://ca.smallstep.com/1.0/revoke"}, +var ( + defaultDisableRenewal = false + defaultEnableSSHCA = true + globalProvisionerClaims = Claims{ + MinTLSDur: &Duration{5 * time.Minute}, + MaxTLSDur: &Duration{24 * time.Hour}, + DefaultTLSDur: &Duration{24 * time.Hour}, + DisableRenewal: &defaultDisableRenewal, + MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs + MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, + DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour}, + MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs + MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, + DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, + EnableSSHCA: &defaultEnableSSHCA, + } + testAudiences = Audiences{ + Sign: []string{"https://ca.smallstep.com/sign", "https://ca.smallstep.com/1.0/sign"}, + Revoke: []string{"https://ca.smallstep.com/revoke", "https://ca.smallstep.com/1.0/revoke"}, + } +) + +func provisionerClaims() *Claims { + ddr := false + des := true + return &Claims{ + MinTLSDur: &Duration{5 * time.Minute}, + MaxTLSDur: &Duration{24 * time.Hour}, + DefaultTLSDur: &Duration{24 * time.Hour}, + DisableRenewal: &ddr, + MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs + MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, + DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour}, + MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs + MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, + DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, + EnableSSHCA: &des, + } } const awsTestCertificate = `-----BEGIN CERTIFICATE----- @@ -162,6 +197,58 @@ func generateJWK() (*JWK, error) { }, nil } +func generateX5C(root []byte) (*X5C, error) { + if root == nil { + root = []byte(`-----BEGIN CERTIFICATE----- +MIIBhTCCASqgAwIBAgIRAMalM7pKi0GCdKjO6u88OyowCgYIKoZIzj0EAwIwFDES +MBAGA1UEAxMJcm9vdC10ZXN0MCAXDTE5MTAwMjAyMzk0OFoYDzIxMTkwOTA4MDIz +OTQ4WjAUMRIwEAYDVQQDEwlyb290LXRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAAS29QTCXUu7cx9sa9wZPpRSFq/zXaw8Ai3EIygayrBsKnX42U2atBUjcBZO +BWL6A+PpLzU9ja867U5SYNHERS+Oo1swWTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T +AQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowFAYD +VR0RBA0wC4IJcm9vdC10ZXN0MAoGCCqGSM49BAMCA0kAMEYCIQC2vgqwla0u8LHH +1MHob14qvS5o76HautbIBW7fcHzz5gIhAIx5A2+wkJYX4026kqaZCk/1sAwTxSGY +M46l92gdOozT +-----END CERTIFICATE-----`) + } + + name, err := randutil.Alphanumeric(10) + if err != nil { + return nil, err + } + claimer, err := NewClaimer(nil, globalProvisionerClaims) + if err != nil { + return nil, err + } + + rootPool := x509.NewCertPool() + + var ( + block *pem.Block + rest = root + ) + for rest != nil { + block, rest = pem.Decode(rest) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing x509 certificate from PEM block") + } + rootPool.AddCert(cert) + } + return &X5C{ + Name: name, + Type: "X5C", + Roots: root, + Claims: &globalProvisionerClaims, + audiences: testAudiences, + claimer: claimer, + rootPool: rootPool, + }, nil +} + func generateOIDC() (*OIDC, error) { name, err := randutil.Alphanumeric(10) if err != nil { @@ -446,11 +533,31 @@ func generateSimpleToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) return generateToken("subject", iss, aud, "name@smallstep.com", []string{"test.smallstep.com"}, time.Now(), jwk) } -func generateToken(sub, iss, aud string, email string, sans []string, iat time.Time, jwk *jose.JSONWebKey) (string, error) { - sig, err := jose.NewSigner( - jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, - new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID), - ) +type tokOption func(*jose.SignerOptions) error + +func withX5CHdr(certs []*x509.Certificate) tokOption { + return func(so *jose.SignerOptions) error { + strs := make([]string, len(certs)) + for i, cert := range certs { + strs[i] = base64.StdEncoding.EncodeToString(cert.Raw) + } + so.WithHeader("x5c", strs) + return nil + } +} + +func generateToken(sub, iss, aud string, email string, sans []string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) { + so := new(jose.SignerOptions) + so.WithType("JWT") + so.WithHeader("kid", jwk.KeyID) + + for _, o := range tokOpts { + if err := o(so); err != nil { + return "", err + } + } + + sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so) if err != nil { return "", err } @@ -742,3 +849,24 @@ func generateACME() (*ACME, error) { } return p, nil } + +func parseCerts(b []byte) ([]*x509.Certificate, error) { + var ( + block *pem.Block + rest = b + certs = []*x509.Certificate{} + ) + for rest != nil { + block, rest = pem.Decode(rest) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "error parsing x509 certificate from PEM block") + } + + certs = append(certs, cert) + } + return certs, nil +} diff --git a/authority/provisioner/x5c.go b/authority/provisioner/x5c.go new file mode 100644 index 00000000..55725982 --- /dev/null +++ b/authority/provisioner/x5c.go @@ -0,0 +1,267 @@ +package provisioner + +import ( + "context" + "crypto/x509" + "encoding/pem" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/cli/crypto/x509util" + "github.com/smallstep/cli/jose" +) + +// x5cPayload extends jwt.Claims with step attributes. +type x5cPayload struct { + jose.Claims + SANs []string `json:"sans,omitempty"` + Step *stepPayload `json:"step,omitempty"` + chains [][]*x509.Certificate +} + +// X5C is the default provisioner, an entity that can sign tokens necessary for +// signature requests. +type X5C struct { + Type string `json:"type"` + Name string `json:"name"` + Roots []byte `json:"roots"` + Claims *Claims `json:"claims,omitempty"` + claimer *Claimer + audiences Audiences + rootPool *x509.CertPool +} + +// GetID returns the provisioner unique identifier. The name and credential id +// should uniquely identify any X5C provisioner. +func (p *X5C) GetID() string { + return "x5c/" + p.Name +} + +// GetTokenID returns the identifier of the token. +func (p *X5C) GetTokenID(ott string) (string, error) { + // Validate payload + token, err := jose.ParseSigned(ott) + if err != nil { + return "", errors.Wrap(err, "error parsing token") + } + + // Get claims w/out verification. We need to look up the provisioner + // key in order to verify the claims and we need the issuer from the claims + // before we can look up the provisioner. + var claims jose.Claims + if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil { + return "", errors.Wrap(err, "error verifying claims") + } + return claims.ID, nil +} + +// GetName returns the name of the provisioner. +func (p *X5C) GetName() string { + return p.Name +} + +// GetType returns the type of provisioner. +func (p *X5C) GetType() Type { + return TypeX5C +} + +// GetEncryptedKey returns the base provisioner encrypted key if it's defined. +func (p *X5C) GetEncryptedKey() (string, string, bool) { + return "", "", false +} + +// Init initializes and validates the fields of a X5C type. +func (p *X5C) Init(config Config) error { + switch { + case p.Type == "": + return errors.New("provisioner type cannot be empty") + case p.Name == "": + return errors.New("provisioner name cannot be empty") + case len(p.Roots) == 0: + return errors.New("provisioner root(s) cannot be empty") + } + + p.rootPool = x509.NewCertPool() + + var ( + block *pem.Block + rest = p.Roots + ) + for rest != nil { + block, rest = pem.Decode(rest) + if block == nil { + break + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return errors.Wrap(err, "error parsing x509 certificate from PEM block") + } + p.rootPool.AddCert(cert) + } + + // Verify that at least one root was found. + if len(p.rootPool.Subjects()) == 0 { + return errors.Errorf("no x509 certificates found in roots attribute for provisioner %s", p.GetName()) + } + + // Update claims with global ones + var err error + if p.claimer, err = NewClaimer(p.Claims, config.Claims); err != nil { + return err + } + + p.audiences = config.Audiences.WithFragment(p.GetID()) + return nil +} + +// authorizeToken performs common jwt authorization actions and returns the +// claims for case specific downstream parsing. +// e.g. a Sign request will auth/validate different fields than a Revoke request. +func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, error) { + jwt, err := jose.ParseSigned(token) + if err != nil { + return nil, errors.Wrapf(err, "error parsing token") + } + + verifiedChains, err := jwt.Headers[0].Certificates(x509.VerifyOptions{ + Roots: p.rootPool, + }) + if err != nil { + return nil, errors.Wrap(err, "error verifying x5c certificate chain") + } + leaf := verifiedChains[0][0] + + if leaf.KeyUsage&x509.KeyUsageDigitalSignature == 0 { + return nil, errors.New("certificate used to sign x5c token cannot be used for digital signature") + } + + // Using the leaf certificates key to validate the claims accomplishes two + // things: + // 1. Asserts that the private key used to sign the token corresponds + // to the public certificate in the `x5c` header of the token. + // 2. Asserts that the claims are valid - have not been tampered with. + var claims x5cPayload + if err = jwt.Claims(leaf.PublicKey, &claims); err != nil { + return nil, errors.Wrap(err, "error parsing claims") + } + + // According to "rfc7519 JSON Web Token" acceptable skew should be no + // more than a few minutes. + if err = claims.ValidateWithLeeway(jose.Expected{ + Issuer: p.Name, + Time: time.Now().UTC(), + }, time.Minute); err != nil { + return nil, errors.Wrapf(err, "invalid token") + } + + // validate audiences with the defaults + if !matchesAudience(claims.Audience, audiences) { + return nil, errors.New("invalid token: invalid audience claim (aud)") + } + + if claims.Subject == "" { + return nil, errors.New("token subject cannot be empty") + } + + // Save the verified chains on the x5c payload object. + claims.chains = verifiedChains + return &claims, nil +} + +// AuthorizeRevoke returns an error if the provisioner does not have rights to +// revoke the certificate with serial number in the `sub` property. +func (p *X5C) AuthorizeRevoke(token string) error { + _, err := p.authorizeToken(token, p.audiences.Revoke) + return err +} + +// AuthorizeSign validates the given token. +func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) { + claims, err := p.authorizeToken(token, p.audiences.Sign) + if err != nil { + return nil, err + } + + // Check for SSH sign-ing request. + if MethodFromContext(ctx) == SignSSHMethod { + if !p.claimer.IsSSHCAEnabled() { + return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID()) + } + return p.authorizeSSHSign(claims) + } + + // NOTE: This is for backwards compatibility with older versions of cli + // and certificates. Older versions added the token subject as the only SAN + // in a CSR by default. + if len(claims.SANs) == 0 { + claims.SANs = []string{claims.Subject} + } + + dnsNames, ips, emails := x509util.SplitSANs(claims.SANs) + + return []SignOption{ + // modifiers / withOptions + newProvisionerExtensionOption(TypeX5C, p.Name, ""), + profileLimitDuration{p.claimer.DefaultTLSCertDuration(), claims.chains[0][0].NotAfter}, + // validators + commonNameValidator(claims.Subject), + defaultPublicKeyValidator{}, + dnsNamesValidator(dnsNames), + emailAddressesValidator(emails), + ipAddressesValidator(ips), + newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()), + }, nil +} + +// AuthorizeRenewal returns an error if the renewal is disabled. +func (p *X5C) AuthorizeRenewal(cert *x509.Certificate) error { + if p.claimer.IsDisableRenewal() { + return errors.Errorf("renew is disabled for provisioner %s", p.GetID()) + } + return nil +} + +// authorizeSSHSign returns the list of SignOption for a SignSSH request. +func (p *X5C) authorizeSSHSign(claims *x5cPayload) ([]SignOption, error) { + if claims.Step == nil || claims.Step.SSH == nil { + return nil, errors.New("authorization token must be an SSH provisioning token") + } + opts := claims.Step.SSH + signOptions := []SignOption{ + // validates user's SSHOptions with the ones in the token + sshCertificateOptionsValidator(*opts), + // set the key id to the token subject + sshCertificateKeyIDModifier(claims.Subject), + } + + // Add modifiers from custom claims + if opts.CertType != "" { + signOptions = append(signOptions, sshCertificateCertTypeModifier(opts.CertType)) + } + if len(opts.Principals) > 0 { + signOptions = append(signOptions, sshCertificatePrincipalsModifier(opts.Principals)) + } + t := now() + if !opts.ValidAfter.IsZero() { + signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix())) + } + if !opts.ValidBefore.IsZero() { + signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix())) + } + + // Default to a user certificate with no principals if not set + signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert}) + + return append(signOptions, + // Set the default extensions. + &sshDefaultExtensionModifier{}, + // Checks the validity bounds, and set the validity if has not been set. + sshLimitValidityModifier(p.claimer, claims.chains[0][0].NotAfter), + // Validate public key. + &sshDefaultPublicKeyValidator{}, + // Validate the validity period. + &sshCertificateValidityValidator{p.claimer}, + // Require all the fields in the SSH certificate + &sshCertificateDefaultValidator{}, + ), nil +} diff --git a/authority/provisioner/x5c_test.go b/authority/provisioner/x5c_test.go new file mode 100644 index 00000000..477e3267 --- /dev/null +++ b/authority/provisioner/x5c_test.go @@ -0,0 +1,751 @@ +package provisioner + +import ( + "context" + "crypto/x509" + "net" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/assert" + "github.com/smallstep/cli/crypto/pemutil" + "github.com/smallstep/cli/jose" +) + +func TestX5C_Getters(t *testing.T) { + p, err := generateX5C(nil) + assert.FatalError(t, err) + id := "x5c/" + p.Name + if got := p.GetID(); got != id { + t.Errorf("X5C.GetID() = %v, want %v:%v", got, p.Name, id) + } + if got := p.GetName(); got != p.Name { + t.Errorf("X5C.GetName() = %v, want %v", got, p.Name) + } + if got := p.GetType(); got != TypeX5C { + t.Errorf("X5C.GetType() = %v, want %v", got, TypeX5C) + } + kid, key, ok := p.GetEncryptedKey() + if kid != "" || key != "" || ok == true { + t.Errorf("X5C.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)", + kid, key, ok, "", "", false) + } +} + +func TestX5C_Init(t *testing.T) { + type ProvisionerValidateTest struct { + p *X5C + err error + extraValid func(*X5C) error + } + tests := map[string]func(*testing.T) ProvisionerValidateTest{ + "fail/empty": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &X5C{}, + err: errors.New("provisioner type cannot be empty"), + } + }, + "fail/empty-name": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &X5C{ + Type: "X5C", + }, + err: errors.New("provisioner name cannot be empty"), + } + }, + "fail/empty-type": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &X5C{Name: "foo"}, + err: errors.New("provisioner type cannot be empty"), + } + }, + "fail/empty-key": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &X5C{Name: "foo", Type: "bar"}, + err: errors.New("provisioner root(s) cannot be empty"), + } + }, + "fail/no-valid-root-certs": func(t *testing.T) ProvisionerValidateTest { + return ProvisionerValidateTest{ + p: &X5C{Name: "foo", Type: "bar", Roots: []byte("foo"), audiences: testAudiences}, + err: errors.Errorf("no x509 certificates found in roots attribute for provisioner foo"), + } + }, + "fail/invalid-duration": func(t *testing.T) ProvisionerValidateTest { + p, err := generateX5C(nil) + assert.FatalError(t, err) + p.Claims = &Claims{DefaultTLSDur: &Duration{0}} + return ProvisionerValidateTest{ + p: p, + err: errors.New("claims: DefaultTLSCertDuration must be greater than 0"), + } + }, + "ok": func(t *testing.T) ProvisionerValidateTest { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return ProvisionerValidateTest{ + p: p, + } + }, + "ok/root-chain": func(t *testing.T) ProvisionerValidateTest { + p, err := generateX5C([]byte(`-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw +EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw +MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl +qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC +AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99 +oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw +E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1 +2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC +lgsqsR63is+0YQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBhTCCASqgAwIBAgIRAMalM7pKi0GCdKjO6u88OyowCgYIKoZIzj0EAwIwFDES +MBAGA1UEAxMJcm9vdC10ZXN0MCAXDTE5MTAwMjAyMzk0OFoYDzIxMTkwOTA4MDIz +OTQ4WjAUMRIwEAYDVQQDEwlyb290LXRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB +BwNCAAS29QTCXUu7cx9sa9wZPpRSFq/zXaw8Ai3EIygayrBsKnX42U2atBUjcBZO +BWL6A+PpLzU9ja867U5SYNHERS+Oo1swWTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T +AQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowFAYD +VR0RBA0wC4IJcm9vdC10ZXN0MAoGCCqGSM49BAMCA0kAMEYCIQC2vgqwla0u8LHH +1MHob14qvS5o76HautbIBW7fcHzz5gIhAIx5A2+wkJYX4026kqaZCk/1sAwTxSGY +M46l92gdOozT +-----END CERTIFICATE-----`)) + assert.FatalError(t, err) + return ProvisionerValidateTest{ + p: p, + extraValid: func(p *X5C) error { + numCerts := len(p.rootPool.Subjects()) + if numCerts != 2 { + return errors.Errorf("unexpected number of certs: want 2, but got %d", numCerts) + } + return nil + }, + } + }, + } + + config := Config{ + Claims: globalProvisionerClaims, + Audiences: testAudiences, + } + for name, get := range tests { + t.Run(name, func(t *testing.T) { + tc := get(t) + err := tc.p.Init(config) + if err != nil { + if assert.NotNil(t, tc.err) { + assert.Equals(t, tc.err.Error(), err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, tc.p.audiences, config.Audiences.WithFragment(tc.p.GetID())) + if tc.extraValid != nil { + assert.Nil(t, tc.extraValid(tc.p)) + } + } + } + }) + } +} + +func TestX5C_authorizeToken(t *testing.T) { + type test struct { + p *X5C + token string + err error + } + tests := map[string]func(*testing.T) test{ + "fail/bad-token": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + err: errors.New("error parsing token"), + } + }, + "fail/invalid-cert-chain": func(t *testing.T) test { + certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE----- +MIIBpTCCAUugAwIBAgIRAOn2LHXjYyTXQ7PNjDTSKiIwCgYIKoZIzj0EAwIwHDEa +MBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMTkwOTE0MDk1NTM2WhcNMjkw +OTExMDk1NTM2WjAkMSIwIAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENB +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2Cs0TY0dLM4b2s+z8+cc3JJp/W5H +zQRvICX/1aJ4MuObNLcvoSguJwJEkYpGB5fhb0KvoL+ebHfEOywGNwrWkaNmMGQw +DgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNLJ +4ZXoX9cI6YkGPxgs2US3ssVzMB8GA1UdIwQYMBaAFGIwpqz85wL29aF47Vj9XSVM +P9K7MAoGCCqGSM49BAMCA0gAMEUCIQC5c1ldDcesDb31GlO5cEJvOcRrIrNtkk8m +a5wpg+9s6QIgHIW6L60F8klQX+EO3o0SBqLeNcaskA4oSZsKjEdpSGo= +-----END CERTIFICATE-----`)) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", p.Name, testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("error verifying x5c certificate chain: x509: certificate signed by unknown authority"), + } + }, + "fail/doubled-up-self-signed-cert": func(t *testing.T) test { + certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE----- +MIIBgjCCASigAwIBAgIQIZiE9wpmSj6SMMDfHD17qjAKBggqhkjOPQQDAjAQMQ4w +DAYDVQQDEwVsZWFmMjAgFw0xOTEwMDIwMzEzNTlaGA8yMTE5MDkwODAzMTM1OVow +EDEOMAwGA1UEAxMFbGVhZjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuajJI +3YgDaj+jorioJzGJc2+V1hUM7XzN9tIHoUeItgny9GW08TrTc23h1cCZteNZvayG +M0wGpGeXOnE4IlH9o2IwYDAOBgNVHQ8BAf8EBAMCBSAwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBT99+JChTh3LWOHaqlSwNiwND18/zAQ +BgNVHREECTAHggVsZWFmMjAKBggqhkjOPQQDAgNIADBFAiB7gMRy3t81HpcnoRAS +ELZmDFaEnoLCsVfbmanFykazQQIhAI0sZjoE9t6gvzQp7XQp6CoxzCc3Jv3FwZ8G +EXAHTA9L +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBgjCCASigAwIBAgIQIZiE9wpmSj6SMMDfHD17qjAKBggqhkjOPQQDAjAQMQ4w +DAYDVQQDEwVsZWFmMjAgFw0xOTEwMDIwMzEzNTlaGA8yMTE5MDkwODAzMTM1OVow +EDEOMAwGA1UEAxMFbGVhZjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuajJI +3YgDaj+jorioJzGJc2+V1hUM7XzN9tIHoUeItgny9GW08TrTc23h1cCZteNZvayG +M0wGpGeXOnE4IlH9o2IwYDAOBgNVHQ8BAf8EBAMCBSAwHQYDVR0lBBYwFAYIKwYB +BQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBT99+JChTh3LWOHaqlSwNiwND18/zAQ +BgNVHREECTAHggVsZWFmMjAKBggqhkjOPQQDAgNIADBFAiB7gMRy3t81HpcnoRAS +ELZmDFaEnoLCsVfbmanFykazQQIhAI0sZjoE9t6gvzQp7XQp6CoxzCc3Jv3FwZ8G +EXAHTA9L +-----END CERTIFICATE-----`)) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", p.Name, testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("error verifying x5c certificate chain: x509: certificate signed by unknown authority"), + } + }, + "fail/digital-signature-ext-required": func(t *testing.T) test { + certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE----- +MIIBuTCCAV+gAwIBAgIQeRJLdDMIdn/T2ORKxYABezAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMjQxMTRaGA8yMTE5 +MDkwODAyNDExMlowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEDA1nGTOujobkcBWklyvymhWE5gQlvNLarVzhhhvPDw+MK2LX +yqkXrYZM10GrwQZuQ7ykHnjz00U/KXpPRQ7+0qOBiDCBhTAOBgNVHQ8BAf8EBAMC +BSAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQYv0AK +3GUOvC+m8ZTfyhn7tKQOazAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp +czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIhAPmertx0 +lchRU3kAu647exvlhEr1xosPOu6P8kVYbtTEAiAA51w9EYIT/Zb26M3eQV817T2g +Dnhl0ElPQsA92pkqbA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw +EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw +MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl +qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC +AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99 +oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw +E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1 +2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC +lgsqsR63is+0YQ== +-----END CERTIFICATE-----`)) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + p, err := generateX5C(nil) + assert.FatalError(t, err) + + tok, err := generateToken("", p.Name, testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("certificate used to sign x5c token cannot be used for digital signature"), + } + }, + "fail/signature-does-not-match-x5c-pub-key": func(t *testing.T) test { + certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE----- +MIIBuDCCAV+gAwIBAgIQFdu723gqgGaTaqjf6ny88zAKBggqhkjOPQQDAjAcMRow +GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMzE4NTNaGA8yMTE5 +MDkwODAzMTg1MVowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvr +VpgSPXYruNRFduPWX564Abz/TDmb276JbKGeQqOBiDCBhTAOBgNVHQ8BAf8EBAMC +BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBReMkPW +f4MNWdg7KN4xI4ZLJd0IJDAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp +czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDRwAwRAIgKYLKXpTN +wtvZZaIvDzq1p8MO/SZ8yI42Ot69dNk/QtkCIBSvg5PozYcfbvwkgX5SwsjfYu0Z +AvUgkUQ2G25NBRmX +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw +EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw +MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl +qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC +AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99 +oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw +E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1 +2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC +lgsqsR63is+0YQ== +-----END CERTIFICATE-----`)) + assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", "foobar", testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("error parsing claims: square/go-jose: error in cryptographic primitive"), + } + }, + "fail/invalid-issuer": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", "foobar", testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("invalid token: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"), + } + }, + "fail/invalid-audience": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", p.GetName(), "foobar", "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("invalid token: invalid audience claim (aud)"), + } + }, + "fail/empty-subject": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("", p.GetName(), testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + err: errors.New("token subject cannot be empty"), + } + }, + "ok": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.NotNil(t, claims) + assert.NotNil(t, claims.chains) + } + } + }) + } +} + +func TestX5C_AuthorizeSign(t *testing.T) { + type test struct { + p *X5C + token string + ctx context.Context + err error + dns []string + emails []string + ips []net.IP + } + tests := map[string]func(*testing.T) test{ + "fail/invalid-token": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + ctx: NewContextWithMethod(context.Background(), SignMethod), + err: errors.New("error parsing token"), + } + }, + "fail/ssh/disabled": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + p.claimer.claims = provisionerClaims() + *p.claimer.claims.EnableSSHCA = false + tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + ctx: NewContextWithMethod(context.Background(), SignSSHMethod), + token: tok, + err: errors.Errorf("ssh ca is disabled for provisioner x5c/%s", p.GetName()), + } + }, + "fail/ssh/invalid-token": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + ctx: NewContextWithMethod(context.Background(), SignSSHMethod), + token: tok, + err: errors.New("authorization token must be an SSH provisioning token"), + } + }, + "ok/empty-sans": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + ctx: NewContextWithMethod(context.Background(), SignMethod), + token: tok, + dns: []string{"foo"}, + emails: []string{}, + ips: []net.IP{}, + } + }, + "ok/multi-sans": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "", + []string{"127.0.0.1", "foo", "max@smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + ctx: NewContextWithMethod(context.Background(), SignMethod), + token: tok, + dns: []string{"foo"}, + emails: []string{"max@smallstep.com"}, + ips: []net.IP{net.ParseIP("127.0.0.1")}, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if opts, err := tc.p.AuthorizeSign(tc.ctx, tc.token); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + if assert.NotNil(t, opts) { + tot := 0 + for _, o := range opts { + switch v := o.(type) { + case *provisionerExtensionOption: + assert.Equals(t, v.Type, int(TypeX5C)) + assert.Equals(t, v.Name, tc.p.GetName()) + assert.Equals(t, v.CredentialID, "") + assert.Len(t, 0, v.KeyValuePairs) + case profileLimitDuration: + assert.Equals(t, v.def, tc.p.claimer.DefaultTLSCertDuration()) + + claims, err := tc.p.authorizeToken(tc.token, tc.p.audiences.Sign) + assert.FatalError(t, err) + assert.Equals(t, v.notAfter, claims.chains[0][0].NotAfter) + case commonNameValidator: + assert.Equals(t, string(v), "foo") + case defaultPublicKeyValidator: + case dnsNamesValidator: + assert.Equals(t, []string(v), tc.dns) + case emailAddressesValidator: + assert.Equals(t, []string(v), tc.emails) + case ipAddressesValidator: + assert.Equals(t, []net.IP(v), tc.ips) + case *validityValidator: + assert.Equals(t, v.min, tc.p.claimer.MinTLSCertDuration()) + assert.Equals(t, v.max, tc.p.claimer.MaxTLSCertDuration()) + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } + tot++ + } + assert.Equals(t, tot, 8) + } + } + } + }) + } +} + +func TestX5C_authorizeSSHSign(t *testing.T) { + _, fn := mockNow() + defer fn() + type test struct { + p *X5C + claims *x5cPayload + err error + } + tests := map[string]func(*testing.T) test{ + "fail/no-Step-claim": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + claims: new(x5cPayload), + err: errors.New("authorization token must be an SSH provisioning token"), + } + }, + "fail/no-SSH-subattribute-in-claims": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + claims: &x5cPayload{Step: new(stepPayload)}, + err: errors.New("authorization token must be an SSH provisioning token"), + } + }, + "ok/with-claims": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + return test{ + p: p, + claims: &x5cPayload{ + Step: &stepPayload{SSH: &SSHOptions{ + CertType: SSHHostCert, + Principals: []string{"max", "mariano", "alan"}, + ValidAfter: TimeDuration{d: 5 * time.Minute}, + ValidBefore: TimeDuration{d: 10 * time.Minute}, + }}, + Claims: jose.Claims{Subject: "foo"}, + chains: [][]*x509.Certificate{certs}, + }, + } + }, + "ok/without-claims": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + return test{ + p: p, + claims: &x5cPayload{ + Step: &stepPayload{SSH: &SSHOptions{}}, + Claims: jose.Claims{Subject: "foo"}, + chains: [][]*x509.Certificate{certs}, + }, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if opts, err := tc.p.authorizeSSHSign(tc.claims); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + if assert.NotNil(t, opts) { + tot := 0 + nw := now() + for _, o := range opts { + switch v := o.(type) { + case sshCertificateOptionsValidator: + tc.claims.Step.SSH.ValidAfter.t = time.Time{} + tc.claims.Step.SSH.ValidBefore.t = time.Time{} + assert.Equals(t, SSHOptions(v), *tc.claims.Step.SSH) + case sshCertificateKeyIDModifier: + assert.Equals(t, string(v), "foo") + case sshCertificateCertTypeModifier: + assert.Equals(t, string(v), tc.claims.Step.SSH.CertType) + case sshCertificatePrincipalsModifier: + assert.Equals(t, []string(v), tc.claims.Step.SSH.Principals) + case sshCertificateValidAfterModifier: + assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix()) + case sshCertificateValidBeforeModifier: + assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix()) + case sshCertificateDefaultsModifier: + assert.Equals(t, SSHOptions(v), SSHOptions{CertType: SSHUserCert}) + case *sshValidityModifier: + assert.Equals(t, v.Claimer, tc.p.claimer) + assert.Equals(t, v.validBefore, tc.claims.chains[0][0].NotAfter) + case *sshCertificateValidityValidator: + assert.Equals(t, v.Claimer, tc.p.claimer) + case *sshDefaultExtensionModifier, *sshDefaultPublicKeyValidator, + *sshCertificateDefaultValidator: + default: + assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v)) + } + tot++ + } + if len(tc.claims.Step.SSH.CertType) > 0 { + assert.Equals(t, tot, 12) + } else { + assert.Equals(t, tot, 8) + } + } + } + } + }) + } +} + +func TestX5C_AuthorizeRevoke(t *testing.T) { + type test struct { + p *X5C + token string + err error + } + tests := map[string]func(*testing.T) test{ + "fail/invalid-token": func(t *testing.T) test { + p, err := generateX5C(nil) + assert.FatalError(t, err) + return test{ + p: p, + token: "foo", + err: errors.New("error parsing token"), + } + }, + "ok": func(t *testing.T) test { + certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt") + assert.FatalError(t, err) + jwk, err := jose.ParseKey("./testdata/x5c-leaf.key") + assert.FatalError(t, err) + + p, err := generateX5C(nil) + assert.FatalError(t, err) + tok, err := generateToken("foo", p.GetName(), testAudiences.Revoke[0], "", + []string{"test.smallstep.com"}, time.Now(), jwk, + withX5CHdr(certs)) + assert.FatalError(t, err) + return test{ + p: p, + token: tok, + } + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc := tt(t) + if err := tc.p.AuthorizeRevoke(tc.token); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestX5C_AuthorizeRenewal(t *testing.T) { + p1, err := generateX5C(nil) + assert.FatalError(t, err) + p2, err := generateX5C(nil) + assert.FatalError(t, err) + + // disable renewal + disable := true + p2.Claims = &Claims{DisableRenewal: &disable} + p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims) + assert.FatalError(t, err) + + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + prov *X5C + args args + wantErr bool + }{ + {"ok", p1, args{nil}, false}, + {"fail", p2, args{nil}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.prov.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr { + t.Errorf("X5C.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/authority/tls.go b/authority/tls.go index 84227667..eb20639e 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -56,7 +56,7 @@ func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption { } // Sign creates a signed certificate from a certificate signing request. -func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) (*x509.Certificate, *x509.Certificate, error) { +func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) { var ( errContext = apiCtx{"csr": csr, "signOptions": signOpts} mods = []x509util.WithOption{withDefaultASN1DN(a.config.AuthorityConfig.Template)} @@ -69,66 +69,66 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti certValidators = append(certValidators, k) case provisioner.CertificateRequestValidator: if err := k.Valid(csr); err != nil { - return nil, nil, &apiError{errors.Wrap(err, "sign"), http.StatusUnauthorized, errContext} + return nil, &apiError{errors.Wrap(err, "sign"), http.StatusUnauthorized, errContext} } case provisioner.ProfileModifier: mods = append(mods, k.Option(signOpts)) default: - return nil, nil, &apiError{errors.Errorf("sign: invalid extra option type %T", k), + return nil, &apiError{errors.Errorf("sign: invalid extra option type %T", k), http.StatusInternalServerError, errContext} } } if err := csr.CheckSignature(); err != nil { - return nil, nil, &apiError{errors.Wrap(err, "sign: invalid certificate request"), + return nil, &apiError{errors.Wrap(err, "sign: invalid certificate request"), http.StatusBadRequest, errContext} } leaf, err := x509util.NewLeafProfileWithCSR(csr, issIdentity.Crt, issIdentity.Key, mods...) if err != nil { - return nil, nil, &apiError{errors.Wrapf(err, "sign"), http.StatusInternalServerError, errContext} + return nil, &apiError{errors.Wrapf(err, "sign"), http.StatusInternalServerError, errContext} } for _, v := range certValidators { if err := v.Valid(leaf.Subject()); err != nil { - return nil, nil, &apiError{errors.Wrap(err, "sign"), http.StatusUnauthorized, errContext} + return nil, &apiError{errors.Wrap(err, "sign"), http.StatusUnauthorized, errContext} } } crtBytes, err := leaf.CreateCertificate() if err != nil { - return nil, nil, &apiError{errors.Wrap(err, "sign: error creating new leaf certificate"), + return nil, &apiError{errors.Wrap(err, "sign: error creating new leaf certificate"), http.StatusInternalServerError, errContext} } serverCert, err := x509.ParseCertificate(crtBytes) if err != nil { - return nil, nil, &apiError{errors.Wrap(err, "sign: error parsing new leaf certificate"), + return nil, &apiError{errors.Wrap(err, "sign: error parsing new leaf certificate"), http.StatusInternalServerError, errContext} } caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw) if err != nil { - return nil, nil, &apiError{errors.Wrap(err, "sign: error parsing intermediate certificate"), + return nil, &apiError{errors.Wrap(err, "sign: error parsing intermediate certificate"), http.StatusInternalServerError, errContext} } if err = a.db.StoreCertificate(serverCert); err != nil { if err != db.ErrNotImplemented { - return nil, nil, &apiError{errors.Wrap(err, "sign: error storing certificate in db"), + return nil, &apiError{errors.Wrap(err, "sign: error storing certificate in db"), http.StatusInternalServerError, errContext} } } - return serverCert, caCert, nil + return []*x509.Certificate{serverCert, caCert}, nil } // Renew creates a new Certificate identical to the old certificate, except // with a validity window that begins 'now'. -func (a *Authority) Renew(oldCert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) { +func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) { // Check step provisioner extensions if err := a.authorizeRenewal(oldCert); err != nil { - return nil, nil, err + return nil, err } // Issuer @@ -181,26 +181,26 @@ func (a *Authority) Renew(oldCert *x509.Certificate) (*x509.Certificate, *x509.C leaf, err := x509util.NewLeafProfileWithTemplate(newCert, issIdentity.Crt, issIdentity.Key) if err != nil { - return nil, nil, &apiError{err, http.StatusInternalServerError, apiCtx{}} + return nil, &apiError{err, http.StatusInternalServerError, apiCtx{}} } crtBytes, err := leaf.CreateCertificate() if err != nil { - return nil, nil, &apiError{errors.Wrap(err, "error renewing certificate from existing server certificate"), + return nil, &apiError{errors.Wrap(err, "error renewing certificate from existing server certificate"), http.StatusInternalServerError, apiCtx{}} } serverCert, err := x509.ParseCertificate(crtBytes) if err != nil { - return nil, nil, &apiError{errors.Wrap(err, "error parsing new server certificate"), + return nil, &apiError{errors.Wrap(err, "error parsing new server certificate"), http.StatusInternalServerError, apiCtx{}} } caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw) if err != nil { - return nil, nil, &apiError{errors.Wrap(err, "error parsing intermediate certificate"), + return nil, &apiError{errors.Wrap(err, "error parsing intermediate certificate"), http.StatusInternalServerError, apiCtx{}} } - return serverCert, caCert, nil + return []*x509.Certificate{serverCert, caCert}, nil } // RevokeOptions are the options for the Revoke API. diff --git a/authority/tls_test.go b/authority/tls_test.go index 8d443fd4..cee44535 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -208,14 +208,15 @@ func TestSign(t *testing.T) { }, "fail rsa key too short": func(t *testing.T) *signTest { shortRSAKeyPEM := `-----BEGIN CERTIFICATE REQUEST----- -MIIBdDCB2wIBADAOMQwwCgYDVQQDEwNmb28wgaIwDQYJKoZIhvcNAQEBBQADgZAA -MIGMAoGEAK8dks7oV6kcIFEaWna7CDGYPAE8IL7rNi+ruQ1dIYz+JtxT7OPjbCn/ -t5iqni96+35iS/8CvMtEuquOMTMSWOWwlurrbTbLqCazuz/g233o8udxSxhny3cY -wHogp4cXCX6cFll6DeUnoCEuTTSIu8IBHbK48VfNw4V4gGz6cp/H93HrAgMBAAGg -ITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQHMAWCA2ZvbzANBgkqhkiG9w0BAQsF -AAOBhABCZsYM+Kgje68Z9Fjl2+cBwtQHvZDarh+cz6W1SchinZ1T0aNQvSj/otOe -ttnEF4Rq8zqzr4fbv+AF451Mx36AkfgZr9XWGzxidrH+fBCNWXWNR+ymhrL6UFTG -2FbarLt9jN2aJLAYQPwtSeGTAZ74tLOPRPnTP6aMfFNg4XCR0uveHA== +MIIBhDCB7gIBADAZMRcwFQYDVQQDEw5zbWFsbHN0ZXAgdGVzdDCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEA5JlgH99HvHHsCD6XTqqYj3bXU2oIlnYGoLVs7IJ4 +k205rv5/YWky2gjdpIv0Tnaf3o57IJ891lB7GiyO5iHIEUv5N9dVzrdUboyzk2uZ +7JMMNB43CSLB2oNuwJjLeAM/yBzlhRnvpKjrNSfSV+cH54FXdnbFbcTFMStnjqKG +MeECAwEAAaAsMCoGCSqGSIb3DQEJDjEdMBswGQYDVR0RBBIwEIIOc21hbGxzdGVw +IHRlc3QwDQYJKoZIhvcNAQELBQADgYEAKwsbr8Zfcq05DgOoJ//cXMFK1SP8ktRU +N2++E8Ww0Tet9oyNRArqxxS/UyVio63D3wynzRAB25PFGpYG1cN4b81Gv/foFUT6 +W5kR63lNVHBHgQmv5mA8YFsfrJHstaz5k727v2LMHEYIf5/3i16d5zhuxUoaPTYr +ZYtQ9Ot36qc= -----END CERTIFICATE REQUEST-----` block, _ := pem.Decode([]byte(shortRSAKeyPEM)) assert.FatalError(t, err) @@ -276,7 +277,7 @@ ttnEF4Rq8zqzr4fbv+AF451Mx36AkfgZr9XWGzxidrH+fBCNWXWNR+ymhrL6UFTG t.Run(name, func(t *testing.T) { tc := genTestCase(t) - leaf, intermediate, err := tc.auth.Sign(tc.csr, tc.signOpts, tc.extraOpts...) + certChain, err := tc.auth.Sign(tc.csr, tc.signOpts, tc.extraOpts...) if err != nil { if assert.NotNil(t, tc.err) { switch v := err.(type) { @@ -289,6 +290,8 @@ ttnEF4Rq8zqzr4fbv+AF451Mx36AkfgZr9XWGzxidrH+fBCNWXWNR+ymhrL6UFTG } } } else { + leaf := certChain[0] + intermediate := certChain[1] if assert.Nil(t, tc.err) { assert.Equals(t, leaf.NotBefore, signOpts.NotBefore.Time().Truncate(time.Second)) assert.Equals(t, leaf.NotAfter, signOpts.NotAfter.Time().Truncate(time.Second)) @@ -453,11 +456,11 @@ func TestRenew(t *testing.T) { tc, err := genTestCase() assert.FatalError(t, err) - var leaf, intermediate *x509.Certificate + var certChain []*x509.Certificate if tc.auth != nil { - leaf, intermediate, err = tc.auth.Renew(tc.crt) + certChain, err = tc.auth.Renew(tc.crt) } else { - leaf, intermediate, err = a.Renew(tc.crt) + certChain, err = a.Renew(tc.crt) } if err != nil { if assert.NotNil(t, tc.err) { @@ -471,6 +474,8 @@ func TestRenew(t *testing.T) { } } } else { + leaf := certChain[0] + intermediate := certChain[1] if assert.Nil(t, tc.err) { assert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.crt.NotAfter.Sub(crt.NotBefore)) diff --git a/ca/bootstrap_test.go b/ca/bootstrap_test.go index baa23d7e..3449b45a 100644 --- a/ca/bootstrap_test.go +++ b/ca/bootstrap_test.go @@ -303,7 +303,7 @@ func TestBootstrapClient(t *testing.T) { t.Errorf("BootstrapClient() error reading response: %v", err) return } - if renewal.CaPEM.Certificate == nil || renewal.ServerPEM.Certificate == nil { + if renewal.CaPEM.Certificate == nil || renewal.ServerPEM.Certificate == nil || len(renewal.CertChainPEM) == 0 { t.Errorf("BootstrapClient() invalid renewal response: %v", renewal) } } @@ -375,7 +375,7 @@ func TestBootstrapClientServerRotation(t *testing.T) { if err := readJSON(resp.Body, &renew); err != nil { return errors.Wrap(err, "client.Post() error reading response") } - if renew.ServerPEM.Certificate == nil || renew.CaPEM.Certificate == nil { + if renew.ServerPEM.Certificate == nil || renew.CaPEM.Certificate == nil || len(renew.CertChainPEM) == 0 { return errors.New("client.Post() unexpected response found") } // test with bootstrap server @@ -492,7 +492,7 @@ func TestBootstrapClientServerFederation(t *testing.T) { if err := readJSON(resp.Body, &renew); err != nil { return errors.Wrap(err, "client.Post() error reading response") } - if renew.ServerPEM.Certificate == nil || renew.CaPEM.Certificate == nil { + if renew.ServerPEM.Certificate == nil || renew.CaPEM.Certificate == nil || len(renew.CertChainPEM) == 0 { return errors.New("client.Post() unexpected response found") } // test with bootstrap server diff --git a/ca/ca.go b/ca/ca.go index a8af2f23..96bebba4 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -124,7 +124,10 @@ func (ca *CA) Init(config *authority.Config) (*CA, error) { } prefix := "acme" - acmeAuth := acme.NewAuthority(auth.GetDatabase().(nosql.DB), dns, prefix, auth) + acmeAuth, err := acme.NewAuthority(auth.GetDatabase().(nosql.DB), dns, prefix, auth) + if err != nil { + return nil, errors.Wrap(err, "error creating ACME authority") + } acmeRouterHandler := acmeAPI.New(acmeAuth) mux.Route("/"+prefix, func(r chi.Router) { acmeRouterHandler.Route(r) diff --git a/ca/client_test.go b/ca/client_test.go index a655dce0..fc3a5049 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -265,6 +265,10 @@ func TestClient_Sign(t *testing.T) { ok := &api.SignResponse{ ServerPEM: api.Certificate{Certificate: parseCertificate(certPEM)}, CaPEM: api.Certificate{Certificate: parseCertificate(rootPEM)}, + CertChainPEM: []api.Certificate{ + {Certificate: parseCertificate(certPEM)}, + {Certificate: parseCertificate(rootPEM)}, + }, } request := &api.SignRequest{ CsrPEM: api.CertificateRequest{CertificateRequest: parseCertificateRequest(csrPEM)}, @@ -418,6 +422,10 @@ func TestClient_Renew(t *testing.T) { ok := &api.SignResponse{ ServerPEM: api.Certificate{Certificate: parseCertificate(certPEM)}, CaPEM: api.Certificate{Certificate: parseCertificate(rootPEM)}, + CertChainPEM: []api.Certificate{ + {Certificate: parseCertificate(certPEM)}, + {Certificate: parseCertificate(rootPEM)}, + }, } unauthorized := api.Unauthorized(fmt.Errorf("Unauthorized")) badRequest := api.BadRequest(fmt.Errorf("Bad Request")) diff --git a/ca/renew.go b/ca/renew.go index 44234781..6a4fd22b 100644 --- a/ca/renew.go +++ b/ca/renew.go @@ -178,7 +178,7 @@ func (r *TLSRenewer) renewCertificate() { } func (r *TLSRenewer) nextRenewDuration(notAfter time.Time) time.Duration { - d := notAfter.Sub(time.Now()) - r.renewBefore + d := time.Until(notAfter) - r.renewBefore n := rand.Int63n(int64(r.renewJitter)) d -= time.Duration(n) if d < 0 { diff --git a/ca/signal.go b/ca/signal.go index 0d950435..598cc6f6 100644 --- a/ca/signal.go +++ b/ca/signal.go @@ -28,20 +28,17 @@ func StopHandler(servers ...Stopper) { signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) defer signal.Stop(signals) - for { - select { - case sig := <-signals: - switch sig { - case syscall.SIGINT, syscall.SIGTERM: - log.Println("shutting down ...") - for _, server := range servers { - err := server.Stop() - if err != nil { - log.Printf("error stopping server: %s", err.Error()) - } + for sig := range signals { + switch sig { + case syscall.SIGINT, syscall.SIGTERM: + log.Println("shutting down ...") + for _, server := range servers { + err := server.Stop() + if err != nil { + log.Printf("error stopping server: %s", err.Error()) } - return } + return } } } @@ -54,28 +51,25 @@ func StopReloaderHandler(servers ...StopReloader) { signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) defer signal.Stop(signals) - for { - select { - case sig := <-signals: - switch sig { - case syscall.SIGHUP: - log.Println("reloading ...") - for _, server := range servers { - err := server.Reload() - if err != nil { - log.Printf("error reloading server: %+v", err) - } + for sig := range signals { + switch sig { + case syscall.SIGHUP: + log.Println("reloading ...") + for _, server := range servers { + err := server.Reload() + if err != nil { + log.Printf("error reloading server: %+v", err) } - case syscall.SIGINT, syscall.SIGTERM: - log.Println("shutting down ...") - for _, server := range servers { - err := server.Stop() - if err != nil { - log.Printf("error stopping server: %s", err.Error()) - } - } - return } + case syscall.SIGINT, syscall.SIGTERM: + log.Println("shutting down ...") + for _, server := range servers { + err := server.Stop() + if err != nil { + log.Printf("error stopping server: %s", err.Error()) + } + } + return } } } diff --git a/ca/tls_options_test.go b/ca/tls_options_test.go index a422799e..e2ed4234 100644 --- a/ca/tls_options_test.go +++ b/ca/tls_options_test.go @@ -553,7 +553,7 @@ func equalPools(a, b *x509.CertPool) bool { for i := range subjects { sB[i] = string(subjects[i]) } - sort.Sort(sort.StringSlice(sA)) - sort.Sort(sort.StringSlice(sB)) + sort.Strings(sA) + sort.Strings(sB) return reflect.DeepEqual(sA, sB) } diff --git a/ca/tls_test.go b/ca/tls_test.go index b88e825a..bf29e9a6 100644 --- a/ca/tls_test.go +++ b/ca/tls_test.go @@ -417,6 +417,10 @@ func TestCertificate(t *testing.T) { ok := &api.SignResponse{ ServerPEM: api.Certificate{Certificate: cert}, CaPEM: api.Certificate{Certificate: parseCertificate(rootPEM)}, + CertChainPEM: []api.Certificate{ + {Certificate: cert}, + {Certificate: parseCertificate(rootPEM)}, + }, } tests := []struct { name string @@ -446,6 +450,10 @@ func TestIntermediateCertificate(t *testing.T) { ok := &api.SignResponse{ ServerPEM: api.Certificate{Certificate: parseCertificate(certPEM)}, CaPEM: api.Certificate{Certificate: intermediate}, + CertChainPEM: []api.Certificate{ + {Certificate: parseCertificate(certPEM)}, + {Certificate: intermediate}, + }, } tests := []struct { name string @@ -475,6 +483,10 @@ func TestRootCertificateCertificate(t *testing.T) { ok := &api.SignResponse{ ServerPEM: api.Certificate{Certificate: parseCertificate(certPEM)}, CaPEM: api.Certificate{Certificate: parseCertificate(rootPEM)}, + CertChainPEM: []api.Certificate{ + {Certificate: parseCertificate(certPEM)}, + {Certificate: parseCertificate(rootPEM)}, + }, TLS: &tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{ {root, root}, }}, @@ -482,6 +494,10 @@ func TestRootCertificateCertificate(t *testing.T) { noTLS := &api.SignResponse{ ServerPEM: api.Certificate{Certificate: parseCertificate(certPEM)}, CaPEM: api.Certificate{Certificate: parseCertificate(rootPEM)}, + CertChainPEM: []api.Certificate{ + {Certificate: parseCertificate(certPEM)}, + {Certificate: parseCertificate(rootPEM)}, + }, } tests := []struct { name string diff --git a/distribution.md b/distribution.md index b28f49ee..f0b58423 100644 --- a/distribution.md +++ b/distribution.md @@ -26,7 +26,7 @@ e.g. `v1.0.2` 2. **Update the version of step/cli**

-    $ dep ensure -update github.com/smallstep/cli
+    $ go get -u github.com/smallstep/cli
     
3. **Commit all changes.** diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 219c564e..9e399993 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -176,7 +176,7 @@ created when you initilialized your PKI. In order to properly validate this certificate clients need access to the public root of trust, aka the public root certificate. If you are using the `step cli` on the same host where you initialized your PKI (the `root_ca.crt` is stored on disk locally), then you can -continue to [setting up your environment](setting-up-environment-variables), +continue to [setting up your environment](#setup-env), otherwise we will show you how to easily download your root certificate in the following step. @@ -215,9 +215,10 @@ In the examples below we will use `https://ca.smallstep.com:8080`. 3. Test. ``` - * step ca health + $ step ca health ``` + #### Setting up Environment Defaults This is optional, but we recommend you populate a `defaults.json` file with a diff --git a/docs/acme.md b/docs/acme.md index 072aa0fa..5aec7206 100644 --- a/docs/acme.md +++ b/docs/acme.md @@ -71,8 +71,8 @@ There are two ways to address this problem: 1. Explicitly configure your ACME client to trust `step-ca`'s root certificate, or 2. Add `step-ca`'s root certificate to your system’s default trust store (e.g., - using `[step certificate - install](https://smallstep.com/docs/cli/certificate/install/)`) + using [`step certificate + install`](https://smallstep.com/docs/cli/certificate/install/)) If you’re using your CA for TLS in production, explicitly configuring your ACME client to only trust your root certificate is a better option. We’ll diff --git a/docs/provisioners.md b/docs/provisioners.md index 100ddb58..070b819b 100644 --- a/docs/provisioners.md +++ b/docs/provisioners.md @@ -132,7 +132,7 @@ is G-Suite. provider used to get the id token. Some identity providers might use an empty string as a secret. -* `configurationEndpoing` (mandatory): is the HTTP address used by the CA to get +* `configurationEndpoint` (mandatory): is the HTTP address used by the CA to get the OpenID Connect configuration and public keys used to validate the tokens. * `admins` (optional): is the list of emails that will be able to get diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..ef48c3f6 --- /dev/null +++ b/go.mod @@ -0,0 +1,44 @@ +module github.com/smallstep/certificates + +go 1.13 + +require ( + github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 // indirect + github.com/Masterminds/goutils v1.1.0 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible + github.com/chzyer/logex v1.1.10 // indirect + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect + github.com/dgraph-io/badger v1.5.3 // indirect + github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect + github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible + github.com/go-sql-driver/mysql v1.4.1 // indirect + github.com/golangci/golangci-lint v1.18.0 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/huandu/xstrings v1.2.0 // indirect + github.com/imdario/mergo v0.3.8 // indirect + github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect + github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect + github.com/manifoldco/promptui v0.3.1 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/newrelic/go-agent v1.11.0 + github.com/pkg/errors v0.8.1 + github.com/rs/xid v1.2.1 + github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 // indirect + github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect + github.com/sirupsen/logrus v1.1.1 + github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 + github.com/smallstep/cli v0.13.4-0.20191014220846-775cfe98ef76 + github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 + github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a + go.etcd.io/bbolt v1.3.2 // indirect + golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a + golang.org/x/net v0.0.0-20190620200207-3b0461eec859 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect + google.golang.org/appengine v1.5.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/square/go-jose.v2 v2.3.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..2030c6d9 --- /dev/null +++ b/go.sum @@ -0,0 +1,285 @@ +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9 h1:HD8gA2tkByhMAwYaFAX9w2l7vxvBQ5NMoxDrkhqhtn4= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/OpenPeeDeeP/depguard v1.0.0 h1:k9QF73nrHT3nPLz3lu6G5s+3Hi8Je36ODr1F5gjAXXM= +github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger v1.5.3 h1:5oWIuRvwn93cie+OSt1zSnkaIQ1JFQM8bGlIv6O6Sts= +github.com/dgraph-io/badger v1.5.3/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible h1:QkUV3XfIQZlGH/Y84jpL20do5cooBfUMzPRNRZvVkZ0= +github.com/go-chi/chi v3.3.4-0.20181024101233-0ebf7795c516+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540 h1:djv/qAomOVj8voCHt0M0OYwR/4vfDq1zNKSPKjJCexs= +github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= +github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0= +github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= +github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= +github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8= +github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= +github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ= +github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= +github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= +github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k= +github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= +github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= +github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= +github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg= +github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= +github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= +github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= +github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA= +github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/mock v1.0.0 h1:HzcpUG60pfl43n9d2qbdi/3l1uKpAmxlfWEPWtV/QxM= +github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 h1:YYWNAGTKWhKpcLLt7aSj/odlKrSrelQwlovBpDuf19w= +github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw= +github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= +github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c h1:/7detzz5stiXWPzkTlPTzkBEIIE4WGpppBJYjKqBiPI= +github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= +github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8= +github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= +github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8= +github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= +github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 h1:0OkFarm1Zy2CjCiDKfK9XHgmc2wbDlRMD2hD8anAJHU= +github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= +github.com/golangci/golangci-lint v1.18.0 h1:XmQgfcLofSG/6AsQuQqmLizB+3GggD+o6ObBG9L+VMM= +github.com/golangci/golangci-lint v1.18.0/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg= +github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547 h1:fUdgm/BdKvwOHxg5AhNbkNRp2mSy8sxTXyBVs/laQHo= +github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU= +github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc h1:gLLhTLMk2/SutryVJ6D4VZCU3CUqr8YloG7FPIBWFpI= +github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= +github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217 h1:En/tZdwhAn0JNwLuXzP3k2RVtMqMmOEK7Yu/g3tmtJE= +github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk= +github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= +github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us= +github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= +github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0 h1:HVfrLniijszjS1aiNg8JbBMO2+E1WIQ+j/gL4SQqGPg= +github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw= +github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= +github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= +github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= +github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.7.6 h1:U+1DqNen04MdEPgFiIwdOUiqZ8qPa37xgogX/sd3+54= +github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/manifoldco/promptui v0.3.1 h1:BxqNa7q1hVHXIXy3iupJMkXYS3aHhbubJWv2Jmg6x64= +github.com/manifoldco/promptui v0.3.1/go.mod h1:zoCNXiJnyM03LlBgTsWv8mq28s7aTC71UgKasqRJHww= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw= +github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/newrelic/go-agent v1.11.0 h1:jnd8+H6dB+93UTJHFT1wJoij5spKNN/xZ0nkw0kvt7o= +github.com/newrelic/go-agent v1.11.0/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM= +github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189 h1:CmSpbxmewNQbzqztaY0bke1qzHhyNyC29wYgh17Gxfo= +github.com/samfoo/ansi v0.0.0-20160124022901-b6bd2ded7189/go.mod h1:UUwuHEJ9zkkPDxspIHOa59PUeSkGFljESGzbxntLmIg= +github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg= +github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= +github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5 h1:lX6ybsQW9Agn3qK/W1Z39Z4a6RyEMGem/gXUYW0axYk= +github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= +github.com/smallstep/cli v0.13.4-0.20191014220846-775cfe98ef76 h1:iNh6x3czCb/01npI8/o4UdvfmTXJkjwVsBOfcXWvmAs= +github.com/smallstep/cli v0.13.4-0.20191014220846-775cfe98ef76/go.mod h1:zGPm8vWCqzvDqkdC1laFJNdIOjNSB8V4qDp68Ny538o= +github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61 h1:XM3mkHNBc6bEQhrZNEma+iz63xrmRFfCocmAEObeg/s= +github.com/smallstep/nosql v0.1.1-0.20191009043502-4b26d8029e61/go.mod h1:MFhYHIE/0V7OOHjYzjnWHqySJ40PVbwhjy24UBkJI2g= +github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs= +github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= +github.com/spf13/afero v1.1.0 h1:bopulORc2JeYaxfHLvJa5NzxviA9PoWhpiiJkru7Ji4= +github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= +github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE= +github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= +github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= +github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec h1:AmoEvWAO3nDx1MEcMzPh+GzOOIA5Znpv6++c7bePPY0= +github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +github.com/ultraware/funlen v0.0.1 h1:UeC9tpM4wNWzUJfan8z9sFE4QCzjjzlCZmuJN+aOkH0= +github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= +github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a h1:qbTm+Zobir+JOKt4xjwK7rwNJXWVfHtV0zGf4TVJ1tQ= +github.com/urfave/cli v1.20.1-0.20181029213200-b67dcf995b6a/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816 h1:mVFkLpejdFLXVUv9E42f3XJVfMdqd0IVLVIVLjZWn5o= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc h1:SdCq5U4J+PpbSDIl9bM0V1e1Ug1jsnBkAFvTs1htn7U= +golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190909030654-5b82db07426d h1:PhtdWYteEBebOX7KXm4qkIAVSUTHQ883/2hRB92r9lk= +golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34 h1:duVSyluuJA+u0BnkcLR01smoLrGgDTfWt5c8ODYG8fU= +mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/logging/handler.go b/logging/handler.go index 7a8ae0bb..c59736d9 100644 --- a/logging/handler.go +++ b/logging/handler.go @@ -32,7 +32,7 @@ func (l *LoggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t := time.Now() rw := NewResponseLogger(w) l.next.ServeHTTP(rw, r) - d := time.Now().Sub(t) + d := time.Since(t) l.writeEntry(rw, r, t, d) }