diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 819a470e..6da2aa27 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - go: [ '1.15', '1.16' ] + go: [ '1.15', '1.16', '1.17' ] outputs: is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} steps: @@ -62,8 +62,15 @@ jobs: needs: test runs-on: ubuntu-20.04 outputs: + debversion: ${{ steps.extract-tag.outputs.DEB_VERSION }} is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} steps: + - + name: Extract Tag Names + id: extract-tag + run: | + DEB_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | sed 's/-/./') + echo "::set-output name=DEB_VERSION::${DEB_VERSION}" - name: Is Pre-release id: is_prerelease @@ -99,62 +106,71 @@ jobs: name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.16 - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@56f5b77f7fa4a8fe068bf22b732ec036cc9bc13f # v2.4.1 - with: - version: latest - args: release --rm-dist - env: - GITHUB_TOKEN: ${{ secrets.PAT }} - - release_deb: - name: Build & Upload Debian Package To Github - runs-on: ubuntu-20.04 - needs: create_release - steps: - - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: '1.16' + go-version: 1.17 - name: APT Install id: aptInstall run: sudo apt-get -y install build-essential debhelper fakeroot - name: Build Debian package - id: build + id: make_debian run: | PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin make debian + # need to restore the git state otherwise goreleaser fails due to dirty state + git restore debian/changelog + git clean -fd - - name: Upload Debian Package - id: upload_deb + name: Install cosign + uses: sigstore/cosign-installer@v1.1.0 + with: + cosign-release: 'v1.1.0' + - + name: Write cosign key to disk + id: write_key + run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key" + - + name: Get Release Date + id: release_date run: | - tag_name="${GITHUB_REF##*/}" - hub release edit $(find ./.releases -type f -printf "-a %p ") -m "" "$tag_name" + RELEASE_DATE=$(date +"%y-%m-%d") + echo "::set-output name=RELEASE_DATE::${RELEASE_DATE}" + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@5a54d7e660bda43b405e8463261b3d25631ffe86 # v2.7.0 + with: + version: latest + args: release --rm-dist env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.PAT }} + COSIGN_PWD: ${{ secrets.COSIGN_PWD }} + DEB_VERSION: ${{ needs.create_release.outputs.debversion }} + RELEASE_DATE: ${{ steps.release_date.outputs.RELEASE_DATE }} build_upload_docker: name: Build & Upload Docker Images runs-on: ubuntu-20.04 needs: test steps: - - name: Checkout + - + name: Checkout uses: actions/checkout@v2 - - name: Setup Go + - + name: Setup Go uses: actions/setup-go@v2 with: - go-version: '1.16' - - name: Build + go-version: '1.17' + - + name: Install cosign + uses: sigstore/cosign-installer@v1.1.0 + with: + cosign-release: 'v1.1.0' + - + name: Write cosign key to disk + id: write_key + run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key" + - + name: Build id: build run: | PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin @@ -162,3 +178,4 @@ jobs: env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + COSIGN_PWD: ${{ secrets.COSIGN_PWD }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c73cfbd..96655664 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - go: [ '1.15', '1.16' ] + go: [ '1.15', '1.16', '1.17' ] steps: - name: Checkout diff --git a/.gitignore b/.gitignore index 7cba0d08..d87786b0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,8 @@ # Others *.swp -.travis-releases +.releases coverage.txt -vendor output +vendor .idea diff --git a/.golangci.yml b/.golangci.yml index 92af7723..cf389517 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -36,22 +36,30 @@ linters-settings: - performance - style - experimental + - diagnostic disabled-checks: - - wrapperFunc - - dupImport # https://github.com/go-critic/go-critic/issues/845 + - commentFormatting + - commentedOutCode + - evalOrder + - hugeParam + - octalLiteral + - rangeValCopy + - tooManyResultsChecker + - unnamedResult linters: disable-all: true enable: - - gofmt - - revive - - govet - - misspell - - ineffassign - deadcode + - gocritic + - gofmt + - gosimple + - govet + - ineffassign + - misspell + - revive - staticcheck - unused - - gosimple run: skip-dirs: diff --git a/.goreleaser.yml b/.goreleaser.yml index 7a7e20d3..207c75bd 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,34 +1,27 @@ # This is an example .goreleaser.yml file with some sane defaults. # Make sure to check the documentation at http://goreleaser.com project_name: step-ca + before: hooks: # You may remove this if you don't use go modules. - go mod download + builds: - id: step-ca env: - CGO_ENABLED=0 - goos: - - linux - - darwin - - windows - goarch: - - amd64 - - arm - - arm64 - - 386 - goarm: - - 6 - - 7 - ignore: - - goos: windows - goarch: 386 - - goos: windows - goarm: 6 - - goos: windows - goarm: 7 + targets: + - darwin_amd64 + - darwin_arm64 + - freebsd_amd64 + - linux_386 + - linux_amd64 + - linux_arm64 + - linux_arm_6 + - linux_arm_7 + - windows_amd64 flags: - -trimpath main: ./cmd/step-ca/main.go @@ -39,25 +32,16 @@ builds: id: step-cloudkms-init env: - CGO_ENABLED=0 - goos: - - linux - - darwin - - windows - goarch: - - amd64 - - arm - - arm64 - - 386 - goarm: - - 6 - - 7 - ignore: - - goos: windows - goarch: 386 - - goos: windows - goarm: 6 - - goos: windows - goarm: 7 + targets: + - darwin_amd64 + - darwin_arm64 + - freebsd_amd64 + - linux_386 + - linux_amd64 + - linux_arm64 + - linux_arm_6 + - linux_arm_7 + - windows_amd64 flags: - -trimpath main: ./cmd/step-cloudkms-init/main.go @@ -68,31 +52,23 @@ builds: id: step-awskms-init env: - CGO_ENABLED=0 - goos: - - linux - - darwin - - windows - goarch: - - amd64 - - arm - - arm64 - - 386 - goarm: - - 6 - - 7 - ignore: - - goos: windows - goarch: 386 - - goos: windows - goarm: 6 - - goos: windows - goarm: 7 + targets: + - darwin_amd64 + - darwin_arm64 + - freebsd_amd64 + - linux_386 + - linux_amd64 + - linux_arm64 + - linux_arm_6 + - linux_arm_7 + - windows_amd64 flags: - -trimpath main: ./cmd/step-awskms-init/main.go binary: bin/step-awskms-init ldflags: - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} + archives: - # Can be used to change the archive formats for specific GOOSs. @@ -106,13 +82,25 @@ archives: files: - README.md - LICENSE + source: enabled: true name_template: '{{ .ProjectName }}_{{ .Version }}' + checksum: name_template: 'checksums.txt' + extra_files: + - glob: ./.releases/* + +signs: +- cmd: cosign + stdin: '{{ .Env.COSIGN_PWD }}' + args: ["sign-blob", "-key=/tmp/cosign.key", "-output=${signature}", "${artifact}"] + artifacts: all + snapshot: name_template: "{{ .Tag }}-next" + release: # Repo in which the release will be created. # Default is extracted from the origin remote URL or empty if its private hosted. @@ -139,7 +127,55 @@ release: # You can change the name of the release. # Default is `{{.Tag}}` - #name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" + name_template: "Step CA {{ .Tag }} ({{ .Env.RELEASE_DATE }})" + + # Header template for the release body. + # Defaults to empty. + header: | + ## Official Release Artifacts + + #### Linux + + - πŸ“¦ [step-ca_linux_{{ .Version }}_amd64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_linux_{{ .Version }}_amd64.tar.gz) + - πŸ“¦ [step-ca_{{ .Env.DEB_VERSION }}_amd64.deb](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ .Env.DEB_VERSION }}_amd64.deb) + + #### OSX Darwin + + - πŸ“¦ [step-ca_darwin_{{ .Version }}_amd64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_amd64.tar.gz) + - πŸ“¦ [step-ca_darwin_{{ .Version }}_arm64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_arm64.tar.gz) + + #### Windows + + - πŸ“¦ [step-ca_windows_{{ .Version }}_arm64.zip](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_windows_{{ .Version }}_amd64.zip) + + For more builds across platforms and architectures, see the `Assets` section below. + And for packaged versions (Docker, k8s, Homebrew), see our [installation docs](https://smallstep.com/docs/step-ca/installation). + + Don't see the artifact you need? Open an issue [here](https://github.com/smallstep/certificates/issues/new/choose). + + ## Signatures and Checksums + + `step-ca` uses [sigstore/cosign](https://github.com/sigstore/cosign) for signing and verifying release artifacts. + + Below is an example using `cosign` to verify a release artifact: + + ``` + cosign verify-blob \ + -key https://raw.githubusercontent.com/smallstep/certificates/master/cosign.pub \ + -signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig + ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz + ``` + + The `checksums.txt` file (in the `Assets` section below) contains a checksum for every artifact in the release. + + # Footer template for the release body. + # Defaults to empty. + footer: | + ## Thanks! + + Those were the changes on {{ .Tag }}! + + Come join us on [Discord](https://discord.gg/X2RKGwEbV9) to ask questions, chat about PKI, or get a sneak peak at the freshest PKI memes. # You can disable this pipe in order to not upload any artifacts. # Defaults to false. @@ -149,6 +185,8 @@ release: # The filename on the release will be the last part of the path (base). If # another file with the same name exists, the latest one found will be used. # Defaults to empty. + extra_files: + - glob: ./.releases/* #extra_files: # - glob: ./path/to/file.txt # - glob: ./glob/**/to/**/file/**/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2b3e25..a902e001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,62 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased - 0.0.1] - DATE +## [Unreleased - 0.17.7] - DATE ### Added +- Support for generate extractable keys and certificates on a pkcs#11 module. ### Changed ### Deprecated ### Removed ### Fixed ### Security + +## [0.17.6] - 2021-10-20 +### Notes +- 0.17.5 failed in CI/CD + +## [0.17.5] - 2021-10-20 +### Added +- Support for Azure Key Vault as a KMS. +- Adapt `pki` package to support key managers. +- gocritic linter +### Fixed +- gocritic warnings + +## [0.17.4] - 2021-09-28 +### Fixed +- Support host-only or user-only SSH CA. + +## [0.17.3] - 2021-09-24 +### Added +- go 1.17 to github action test matrix +- Support for CloudKMS RSA-PSS signers without using templates. +- Add flags to support individual passwords for the intermediate and SSH keys. +- Global support for group admins in the OIDC provisioner. +### Changed +- Using go 1.17 for binaries +### Fixed +- Upgrade go-jose.v2 to fix a bug in the JWK fingerprint of Ed25519 keys. +### Security +- Use cosign to sign and upload signatures for multi-arch Docker container. +- Add debian checksum + +## [0.17.2] - 2021-08-30 +### Added +- Additional way to distinguish Azure IID and Azure OIDC tokens. +### Security +- Sign over all goreleaser github artifacts using cosign + +## [0.17.1] - 2021-08-26 + +## [0.17.0] - 2021-08-25 +### Added +- Add support for Linked CAs using protocol buffers and gRPC +- `step-ca init` adds support for + - configuring a StepCAS RA + - configuring a Linked CA + - congifuring a `step-ca` using Helm +### Changed +- Update badger driver to use v2 by default +- Update TLS cipher suites to include 1.3 +### Security +- Fix key version when SHA512WithRSA is used. There was a typo creating RSA keys with SHA256 digests instead of SHA512. diff --git a/Makefile b/Makefile index 235517b2..09e342df 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ ci: testcgo build bootstra%: # Using a released version of golangci-lint to take into account custom replacements in their go.mod - $Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.39.0 + $Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.42.0 .PHONY: bootstra% @@ -68,7 +68,7 @@ PUSHTYPE := branch endif VERSION := $(shell echo $(VERSION) | sed 's/^v//') -DEB_VERSION := $(shell echo $(VERSION) | sed 's/-/~/g') +DEB_VERSION := $(shell echo $(VERSION) | sed 's/-/./g') ifdef V $(info TRAVIS_TAG is $(TRAVIS_TAG)) @@ -154,7 +154,7 @@ fmt: $Q gofmt -l -w $(SRC) lint: - $Q $(GOFLAGS) LOG_LEVEL=error golangci-lint run --timeout=30m + $Q golangci-lint run --timeout=30m lintcgo: $Q LOG_LEVEL=error golangci-lint run --timeout=30m diff --git a/README.md b/README.md index f0649175..65116b38 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,14 @@ You can use it to: Whatever your use case, `step-ca` is easy to use and hard to misuse, thanks to [safe, sane defaults](https://smallstep.com/docs/step-ca/certificate-authority-server-production#sane-cryptographic-defaults). -**Questions? Find us in [Discussions](https://github.com/smallstep/certificates/discussions).** +--- + +**Don't want to run your own CA?** +To get up and running quickly, or as an alternative to running your own `step-ca` server, consider creating a [free hosted smallstep Certificate Manager authority](https://info.smallstep.com/certificate-manager-early-access-mvp/). + +--- + +**Questions? Find us in [Discussions](https://github.com/smallstep/certificates/discussions) or [Join our Discord](https://u.step.sm/discord).** [Website](https://smallstep.com/certificates) | [Documentation](https://smallstep.com/docs) | @@ -27,7 +34,6 @@ Whatever your use case, `step-ca` is easy to use and hard to misuse, thanks to [ [Contributor's Guide](./docs/CONTRIBUTING.md) [![GitHub release](https://img.shields.io/github/release/smallstep/certificates.svg)](https://github.com/smallstep/certificates/releases/latest) -[![CA Image](https://images.microbadger.com/badges/image/smallstep/step-ca.svg)](https://microbadger.com/images/smallstep/step-ca) [![Go Report Card](https://goreportcard.com/badge/github.com/smallstep/certificates)](https://goreportcard.com/report/github.com/smallstep/certificates) [![Build Status](https://travis-ci.com/smallstep/certificates.svg?branch=master)](https://travis-ci.com/smallstep/certificates) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) @@ -58,10 +64,11 @@ You can issue certificates in exchange for: - ID tokens from Okta, GSuite, Azure AD, Auth0. - ID tokens from an OAuth OIDC service that you host, like [Keycloak](https://www.keycloak.org/) or [Dex](https://github.com/dexidp/dex) - [Cloud instance identity documents](https://smallstep.com/blog/embarrassingly-easy-certificates-on-aws-azure-gcp/), for VMs on AWS, GCP, and Azure -- [Single-use, short-lived JWK tokens]() issued by your CD tool β€” Puppet, Chef, Ansible, Terraform, etc. +- [Single-use, short-lived JWK tokens](https://smallstep.com/docs/step-ca/provisioners#jwk) issued by your CD tool β€” Puppet, Chef, Ansible, Terraform, etc. - A trusted X.509 certificate (X5C provisioner) -- Expiring SSH host certificates needing rotation (the SSHPOP provisioner) -- Learn more in our [provisioner documentation](https://smallstep.com/docs/step-ca/configuration#jwk) +- A SCEP challenge (SCEP provisioner) +- An SSH host certificates needing renewal (the SSHPOP provisioner) +- Learn more in our [provisioner documentation](https://smallstep.com/docs/step-ca/provisioners) ### πŸ” Your own private ACME server @@ -74,16 +81,17 @@ ACME is the protocol used by Let's Encrypt to automate the issuance of HTTPS cer - For `tls-alpn-01`, respond to the challenge at the TLS layer ([as Caddy does](https://caddy.community/t/caddy-supports-the-acme-tls-alpn-challenge/4860)) to prove that you control the web server - Works with any ACME client. We've written examples for: - - [certbot](https://smallstep.com/blog/private-acme-server/#certbotuploadsacme-certbotpng-certbot-example) - - [acme.sh](https://smallstep.com/blog/private-acme-server/#acmeshuploadsacme-acme-shpng-acmesh-example) - - [Caddy](https://smallstep.com/blog/private-acme-server/#caddyuploadsacme-caddypng-caddy-example) - - [Traefik](https://smallstep.com/blog/private-acme-server/#traefikuploadsacme-traefikpng-traefik-example) - - [Apache](https://smallstep.com/blog/private-acme-server/#apacheuploadsacme-apachepng-apache-example) - - [nginx](https://smallstep.com/blog/private-acme-server/#nginxuploadsacme-nginxpng-nginx-example) + - [certbot](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#certbot) + - [acme.sh](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#acmesh) + - [win-acme](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#win-acme) + - [Caddy](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#caddy-v2) + - [Traefik](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#traefik) + - [Apache](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#apache) + - [nginx](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#nginx) - Get certificates programmatically using ACME, using these libraries: - - [`lego`](https://github.com/go-acme/lego) for Golang ([example usage](https://smallstep.com/blog/private-acme-server/#golanguploadsacme-golangpng-go-example)) - - certbot's [`acme` module](https://github.com/certbot/certbot/tree/master/acme) for Python ([example usage](https://smallstep.com/blog/private-acme-server/#pythonuploadsacme-pythonpng-python-example)) - - [`acme-client`](https://github.com/publishlab/node-acme-client) for Node.js ([example usage](https://smallstep.com/blog/private-acme-server/#nodejsuploadsacme-node-jspng-nodejs-example)) + - [`lego`](https://github.com/go-acme/lego) for Golang ([example usage](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#golang)) + - certbot's [`acme` module](https://github.com/certbot/certbot/tree/master/acme) for Python ([example usage](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#python)) + - [`acme-client`](https://github.com/publishlab/node-acme-client) for Node.js ([example usage](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#node)) - Our own [`step` CLI tool](https://github.com/smallstep/cli) is also an ACME client! - See our [ACME tutorial](https://smallstep.com/docs/tutorials/acme-challenge) for more diff --git a/acme/api/account.go b/acme/api/account.go index b733c679..259cb2a2 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -19,7 +19,7 @@ type NewAccountRequest struct { func validateContacts(cs []string) error { for _, c := range cs { - if len(c) == 0 { + if c == "" { return acme.NewError(acme.ErrorMalformedType, "contact cannot be empty string") } } diff --git a/acme/api/account_test.go b/acme/api/account_test.go index c4d7a812..a45751a0 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -178,7 +178,7 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) { provName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - url := fmt.Sprintf("http://ca.smallstep.com/acme/%s/account/%s/orders", provName, accID) + u := fmt.Sprintf("http://ca.smallstep.com/acme/%s/account/%s/orders", provName, accID) oids := []string{"foo", "bar"} oidURLs := []string{ @@ -255,7 +255,7 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.GetOrdersByAccountID(w, req) diff --git a/acme/api/handler.go b/acme/api/handler.go index 8ec12a93..44a1dac5 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -64,8 +64,14 @@ type HandlerOptions struct { // NewHandler returns a new ACME API handler. func NewHandler(ops HandlerOptions) api.RouterHandler { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } client := http.Client{ - Timeout: 30 * time.Second, + Timeout: 30 * time.Second, + Transport: transport, } dialer := &net.Dialer{ Timeout: 30 * time.Second, diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index f354bbac..8112ad4c 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -148,7 +148,7 @@ func TestHandler_GetAuthorization(t *testing.T) { // Request with chi context chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("authzID", az.ID) - url := fmt.Sprintf("%s/acme/%s/authz/%s", + u := fmt.Sprintf("%s/acme/%s/authz/%s", baseURL.String(), provName, az.ID) type test struct { @@ -280,7 +280,7 @@ func TestHandler_GetAuthorization(t *testing.T) { expB, err := json.Marshal(az) assert.FatalError(t, err) assert.Equals(t, bytes.TrimSpace(body), expB) - assert.Equals(t, res.Header["Location"], []string{url}) + assert.Equals(t, res.Header["Location"], []string{u}) assert.Equals(t, res.Header["Content-Type"], []string{"application/json"}) } }) @@ -314,7 +314,7 @@ func TestHandler_GetCertificate(t *testing.T) { // Request with chi context chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("certID", certID) - url := fmt.Sprintf("%s/acme/%s/certificate/%s", + u := fmt.Sprintf("%s/acme/%s/certificate/%s", baseURL.String(), provName, certID) type test struct { @@ -396,7 +396,7 @@ func TestHandler_GetCertificate(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{db: tc.db} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.GetCertificate(w, req) @@ -434,7 +434,7 @@ func TestHandler_GetChallenge(t *testing.T) { baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - url := fmt.Sprintf("%s/acme/%s/challenge/%s/%s", + u := fmt.Sprintf("%s/acme/%s/challenge/%s/%s", baseURL.String(), provName, "authzID", "chID") type test struct { @@ -635,7 +635,7 @@ func TestHandler_GetChallenge(t *testing.T) { AuthorizationID: "authzID", Type: acme.HTTP01, AccountID: "accID", - URL: url, + URL: u, Error: acme.NewError(acme.ErrorConnectionType, "force"), }, vco: &acme.ValidateChallengeOptions{ @@ -652,7 +652,7 @@ func TestHandler_GetChallenge(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{db: tc.db, linker: NewLinker("dns", "acme"), validateChallengeOptions: tc.vco} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.GetChallenge(w, req) @@ -678,7 +678,7 @@ func TestHandler_GetChallenge(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, bytes.TrimSpace(body), expB) assert.Equals(t, res.Header["Link"], []string{fmt.Sprintf("<%s/acme/%s/authz/%s>;rel=\"up\"", baseURL, provName, "authzID")}) - assert.Equals(t, res.Header["Location"], []string{url}) + assert.Equals(t, res.Header["Location"], []string{u}) assert.Equals(t, res.Header["Content-Type"], []string{"application/json"}) } }) diff --git a/acme/api/middleware.go b/acme/api/middleware.go index cb1df487..55bc96bf 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -223,7 +223,7 @@ func (h *Handler) validateJWS(next nextHTTP) nextHTTP { api.WriteError(w, acme.NewError(acme.ErrorMalformedType, "jwk and kid are mutually exclusive")) return } - if hdr.JSONWebKey == nil && len(hdr.KeyID) == 0 { + if hdr.JSONWebKey == nil && hdr.KeyID == "" { api.WriteError(w, acme.NewError(acme.ErrorMalformedType, "either jwk or kid must be defined in jws protected header")) return } @@ -399,7 +399,7 @@ func (h *Handler) verifyAndExtractJWSPayload(next nextHTTP) nextHTTP { api.WriteError(w, err) return } - if len(jwk.Algorithm) != 0 && jwk.Algorithm != jws.Signatures[0].Protected.Algorithm { + if jwk.Algorithm != "" && jwk.Algorithm != jws.Signatures[0].Protected.Algorithm { api.WriteError(w, acme.NewError(acme.ErrorMalformedType, "verifier and signature algorithm do not match")) return } diff --git a/acme/api/middleware_test.go b/acme/api/middleware_test.go index 40090e83..e8d22d53 100644 --- a/acme/api/middleware_test.go +++ b/acme/api/middleware_test.go @@ -108,7 +108,7 @@ func TestHandler_baseURLFromRequest(t *testing.T) { } func TestHandler_addNonce(t *testing.T) { - url := "https://ca.smallstep.com/acme/new-nonce" + u := "https://ca.smallstep.com/acme/new-nonce" type test struct { db acme.DB err *acme.Error @@ -141,7 +141,7 @@ func TestHandler_addNonce(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{db: tc.db} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) w := httptest.NewRecorder() h.addNonce(testNext)(w, req) res := w.Result() @@ -230,7 +230,7 @@ func TestHandler_verifyContentType(t *testing.T) { prov := newProv() escProvName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - url := fmt.Sprintf("%s/acme/%s/certificate/abc123", baseURL.String(), escProvName) + u := fmt.Sprintf("%s/acme/%s/certificate/abc123", baseURL.String(), escProvName) type test struct { h Handler ctx context.Context @@ -245,7 +245,7 @@ func TestHandler_verifyContentType(t *testing.T) { h: Handler{ linker: NewLinker("dns", "acme"), }, - url: url, + url: u, ctx: context.Background(), contentType: "foo", statusCode: 500, @@ -257,7 +257,7 @@ func TestHandler_verifyContentType(t *testing.T) { h: Handler{ linker: NewLinker("dns", "acme"), }, - url: url, + url: u, ctx: context.WithValue(context.Background(), provisionerContextKey, prov), contentType: "foo", statusCode: 400, @@ -319,11 +319,11 @@ func TestHandler_verifyContentType(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - _url := url + _u := u if tc.url != "" { - _url = tc.url + _u = tc.url } - req := httptest.NewRequest("GET", _url, nil) + req := httptest.NewRequest("GET", _u, nil) req = req.WithContext(tc.ctx) req.Header.Add("Content-Type", tc.contentType) w := httptest.NewRecorder() @@ -353,7 +353,7 @@ func TestHandler_verifyContentType(t *testing.T) { } func TestHandler_isPostAsGet(t *testing.T) { - url := "https://ca.smallstep.com/acme/new-account" + u := "https://ca.smallstep.com/acme/new-account" type test struct { ctx context.Context err *acme.Error @@ -392,7 +392,7 @@ func TestHandler_isPostAsGet(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.isPostAsGet(testNext)(w, req) @@ -430,7 +430,7 @@ func (errReader) Close() error { } func TestHandler_parseJWS(t *testing.T) { - url := "https://ca.smallstep.com/acme/new-account" + u := "https://ca.smallstep.com/acme/new-account" type test struct { next nextHTTP body io.Reader @@ -483,7 +483,7 @@ func TestHandler_parseJWS(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{} - req := httptest.NewRequest("GET", url, tc.body) + req := httptest.NewRequest("GET", u, tc.body) w := httptest.NewRecorder() h.parseJWS(tc.next)(w, req) res := w.Result() @@ -528,7 +528,7 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) { assert.FatalError(t, err) parsedJWS, err := jose.ParseJWS(raw) assert.FatalError(t, err) - url := "https://ca.smallstep.com/acme/account/1234" + u := "https://ca.smallstep.com/acme/account/1234" type test struct { ctx context.Context next func(http.ResponseWriter, *http.Request) @@ -681,7 +681,7 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.verifyAndExtractJWSPayload(tc.next)(w, req) @@ -713,7 +713,7 @@ func TestHandler_lookupJWK(t *testing.T) { prov := newProv() provName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - url := fmt.Sprintf("%s/acme/%s/account/1234", + u := fmt.Sprintf("%s/acme/%s/account/1234", baseURL, provName) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) @@ -883,7 +883,7 @@ func TestHandler_lookupJWK(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{db: tc.db, linker: tc.linker} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.lookupJWK(tc.next)(w, req) @@ -934,7 +934,7 @@ func TestHandler_extractJWK(t *testing.T) { assert.FatalError(t, err) parsedJWS, err := jose.ParseJWS(raw) assert.FatalError(t, err) - url := fmt.Sprintf("https://ca.smallstep.com/acme/%s/account/1234", + u := fmt.Sprintf("https://ca.smallstep.com/acme/%s/account/1234", provName) type test struct { db acme.DB @@ -1079,7 +1079,7 @@ func TestHandler_extractJWK(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{db: tc.db} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.extractJWK(tc.next)(w, req) @@ -1108,7 +1108,7 @@ func TestHandler_extractJWK(t *testing.T) { } func TestHandler_validateJWS(t *testing.T) { - url := "https://ca.smallstep.com/acme/account/1234" + u := "https://ca.smallstep.com/acme/account/1234" type test struct { db acme.DB ctx context.Context @@ -1198,7 +1198,7 @@ func TestHandler_validateJWS(t *testing.T) { Algorithm: jose.RS256, JSONWebKey: &pub, ExtraHeaders: map[jose.HeaderKey]interface{}{ - "url": url, + "url": u, }, }, }, @@ -1226,7 +1226,7 @@ func TestHandler_validateJWS(t *testing.T) { Algorithm: jose.RS256, JSONWebKey: &pub, ExtraHeaders: map[jose.HeaderKey]interface{}{ - "url": url, + "url": u, }, }, }, @@ -1298,7 +1298,7 @@ func TestHandler_validateJWS(t *testing.T) { }, ctx: context.WithValue(context.Background(), jwsContextKey, jws), statusCode: 400, - err: acme.NewError(acme.ErrorMalformedType, "url header in JWS (foo) does not match request url (%s)", url), + err: acme.NewError(acme.ErrorMalformedType, "url header in JWS (foo) does not match request url (%s)", u), } }, "fail/both-jwk-kid": func(t *testing.T) test { @@ -1313,7 +1313,7 @@ func TestHandler_validateJWS(t *testing.T) { KeyID: "bar", JSONWebKey: &pub, ExtraHeaders: map[jose.HeaderKey]interface{}{ - "url": url, + "url": u, }, }, }, @@ -1337,7 +1337,7 @@ func TestHandler_validateJWS(t *testing.T) { Protected: jose.Header{ Algorithm: jose.ES256, ExtraHeaders: map[jose.HeaderKey]interface{}{ - "url": url, + "url": u, }, }, }, @@ -1362,7 +1362,7 @@ func TestHandler_validateJWS(t *testing.T) { Algorithm: jose.ES256, KeyID: "bar", ExtraHeaders: map[jose.HeaderKey]interface{}{ - "url": url, + "url": u, }, }, }, @@ -1392,7 +1392,7 @@ func TestHandler_validateJWS(t *testing.T) { Algorithm: jose.ES256, JSONWebKey: &pub, ExtraHeaders: map[jose.HeaderKey]interface{}{ - "url": url, + "url": u, }, }, }, @@ -1422,7 +1422,7 @@ func TestHandler_validateJWS(t *testing.T) { Algorithm: jose.RS256, JSONWebKey: &pub, ExtraHeaders: map[jose.HeaderKey]interface{}{ - "url": url, + "url": u, }, }, }, @@ -1446,7 +1446,7 @@ func TestHandler_validateJWS(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{db: tc.db} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.validateJWS(tc.next)(w, req) diff --git a/acme/api/order_test.go b/acme/api/order_test.go index afb23c3f..3c6d768f 100644 --- a/acme/api/order_test.go +++ b/acme/api/order_test.go @@ -264,7 +264,7 @@ func TestHandler_GetOrder(t *testing.T) { // Request with chi context chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("ordID", o.ID) - url := fmt.Sprintf("%s/acme/%s/order/%s", + u := fmt.Sprintf("%s/acme/%s/order/%s", baseURL.String(), escProvName, o.ID) type test struct { @@ -422,7 +422,7 @@ func TestHandler_GetOrder(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{linker: NewLinker("dns", "acme"), db: tc.db} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.GetOrder(w, req) @@ -448,7 +448,7 @@ func TestHandler_GetOrder(t *testing.T) { assert.FatalError(t, err) assert.Equals(t, bytes.TrimSpace(body), expB) - assert.Equals(t, res.Header["Location"], []string{url}) + assert.Equals(t, res.Header["Location"], []string{u}) assert.Equals(t, res.Header["Content-Type"], []string{"application/json"}) } }) @@ -663,7 +663,7 @@ func TestHandler_NewOrder(t *testing.T) { prov := newProv() escProvName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - url := fmt.Sprintf("%s/acme/%s/order/ordID", + u := fmt.Sprintf("%s/acme/%s/order/ordID", baseURL.String(), escProvName) type test struct { @@ -1335,7 +1335,7 @@ func TestHandler_NewOrder(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{linker: NewLinker("dns", "acme"), db: tc.db} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.NewOrder(w, req) @@ -1363,7 +1363,7 @@ func TestHandler_NewOrder(t *testing.T) { tc.vr(t, ro) } - assert.Equals(t, res.Header["Location"], []string{url}) + assert.Equals(t, res.Header["Location"], []string{u}) assert.Equals(t, res.Header["Content-Type"], []string{"application/json"}) } }) @@ -1406,7 +1406,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { // Request with chi context chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("ordID", o.ID) - url := fmt.Sprintf("%s/acme/%s/order/%s", + u := fmt.Sprintf("%s/acme/%s/order/%s", baseURL.String(), escProvName, o.ID) _csr, err := pemutil.Read("../../authority/testdata/certs/foo.csr") @@ -1625,7 +1625,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { tc := run(t) t.Run(name, func(t *testing.T) { h := &Handler{linker: NewLinker("dns", "acme"), db: tc.db} - req := httptest.NewRequest("GET", url, nil) + req := httptest.NewRequest("GET", u, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() h.FinalizeOrder(w, req) @@ -1654,7 +1654,7 @@ func TestHandler_FinalizeOrder(t *testing.T) { assert.FatalError(t, json.Unmarshal(body, ro)) assert.Equals(t, bytes.TrimSpace(body), expB) - assert.Equals(t, res.Header["Location"], []string{url}) + assert.Equals(t, res.Header["Location"], []string{u}) assert.Equals(t, res.Header["Content-Type"], []string{"application/json"}) } }) diff --git a/acme/challenge.go b/acme/challenge.go index 1d5f0ec9..b880708c 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -10,11 +10,13 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "errors" "fmt" "io/ioutil" "net" "net/http" "net/url" + "reflect" "strings" "time" @@ -74,23 +76,23 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, } func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { - url := &url.URL{Scheme: "http", Host: ch.Value, Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)} + u := &url.URL{Scheme: "http", Host: ch.Value, Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)} - resp, err := vo.HTTPGet(url.String()) + resp, err := vo.HTTPGet(u.String()) if err != nil { return storeError(ctx, db, ch, false, WrapError(ErrorConnectionType, err, - "error doing http GET for url %s", url)) + "error doing http GET for url %s", u)) } defer resp.Body.Close() if resp.StatusCode >= 400 { return storeError(ctx, db, ch, false, NewError(ErrorConnectionType, - "error doing http GET for url %s with status code %d", url, resp.StatusCode)) + "error doing http GET for url %s with status code %d", u, resp.StatusCode)) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return WrapErrorISE(err, "error reading "+ - "response body for url %s", url) + "response body for url %s", u) } keyAuth := strings.TrimSpace(string(body)) @@ -114,6 +116,17 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb return nil } +func tlsAlert(err error) uint8 { + var opErr *net.OpError + if errors.As(err, &opErr) { + v := reflect.ValueOf(opErr.Err) + if v.Kind() == reflect.Uint8 { + return uint8(v.Uint()) + } + } + return 0 +} + func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { config := &tls.Config{ NextProtos: []string{"acme-tls/1"}, @@ -129,6 +142,14 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON conn, err := vo.TLSDial("tcp", hostPort, config) if err != nil { + // With Go 1.17+ tls.Dial fails if there's no overlap between configured + // client and server protocols. When this happens the connection is + // closed with the error no_application_protocol(120) as required by + // RFC7301. See https://golang.org/doc/go1.17#ALPN + if tlsAlert(err) == 120 { + return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, + "cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge")) + } return storeError(ctx, db, ch, false, WrapError(ErrorConnectionType, err, "error doing TLS dial for %s", hostPort)) } diff --git a/acme/challenge_test.go b/acme/challenge_test.go index bb9a2507..a522790f 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -1276,7 +1276,7 @@ func newTLSALPNValidationCert(keyAuthHash []byte, obsoleteOID, critical bool, na oid = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} } - keyAuthHashEnc, _ := asn1.Marshal(keyAuthHash[:]) + keyAuthHashEnc, _ := asn1.Marshal(keyAuthHash) certTemplate.ExtraExtensions = []pkix.Extension{ { @@ -1395,7 +1395,7 @@ func TestTLSALPN01Validate(t *testing.T) { assert.Equals(t, updch.Type, ch.Type) assert.Equals(t, updch.Value, ch.Value) - err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443: tls: DialWithDialer timed out", ch.Value) + err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443:", ch.Value) assert.HasPrefix(t, updch.Error.Err.Error(), err.Err.Error()) assert.Equals(t, updch.Error.Type, err.Type) diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 5ba99a73..a02e93dc 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -93,8 +93,8 @@ func TestDB_getDBAccount(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if dbacc, err := db.getDBAccount(context.Background(), accID); err != nil { + d := DB{db: tc.db} + if dbacc, err := d.getDBAccount(context.Background(), accID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -109,15 +109,13 @@ func TestDB_getDBAccount(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, dbacc.ID, tc.dbacc.ID) - assert.Equals(t, dbacc.Status, tc.dbacc.Status) - assert.Equals(t, dbacc.CreatedAt, tc.dbacc.CreatedAt) - assert.Equals(t, dbacc.DeactivatedAt, tc.dbacc.DeactivatedAt) - assert.Equals(t, dbacc.Contact, tc.dbacc.Contact) - assert.Equals(t, dbacc.Key.KeyID, tc.dbacc.Key.KeyID) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbacc.ID, tc.dbacc.ID) + assert.Equals(t, dbacc.Status, tc.dbacc.Status) + assert.Equals(t, dbacc.CreatedAt, tc.dbacc.CreatedAt) + assert.Equals(t, dbacc.DeactivatedAt, tc.dbacc.DeactivatedAt) + assert.Equals(t, dbacc.Contact, tc.dbacc.Contact) + assert.Equals(t, dbacc.Key.KeyID, tc.dbacc.Key.KeyID) } }) } @@ -174,8 +172,8 @@ func TestDB_getAccountIDByKeyID(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if retAccID, err := db.getAccountIDByKeyID(context.Background(), kid); err != nil { + d := DB{db: tc.db} + if retAccID, err := d.getAccountIDByKeyID(context.Background(), kid); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -190,10 +188,8 @@ func TestDB_getAccountIDByKeyID(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, retAccID, accID) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, retAccID, accID) } }) } @@ -250,8 +246,8 @@ func TestDB_GetAccount(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if acc, err := db.GetAccount(context.Background(), accID); err != nil { + d := DB{db: tc.db} + if acc, err := d.GetAccount(context.Background(), accID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -266,13 +262,11 @@ func TestDB_GetAccount(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, acc.ID, tc.dbacc.ID) - assert.Equals(t, acc.Status, tc.dbacc.Status) - assert.Equals(t, acc.Contact, tc.dbacc.Contact) - assert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, acc.ID, tc.dbacc.ID) + assert.Equals(t, acc.Status, tc.dbacc.Status) + assert.Equals(t, acc.Contact, tc.dbacc.Contact) + assert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID) } }) } @@ -358,8 +352,8 @@ func TestDB_GetAccountByKeyID(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if acc, err := db.GetAccountByKeyID(context.Background(), kid); err != nil { + d := DB{db: tc.db} + if acc, err := d.GetAccountByKeyID(context.Background(), kid); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -374,13 +368,11 @@ func TestDB_GetAccountByKeyID(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, acc.ID, tc.dbacc.ID) - assert.Equals(t, acc.Status, tc.dbacc.Status) - assert.Equals(t, acc.Contact, tc.dbacc.Contact) - assert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, acc.ID, tc.dbacc.ID) + assert.Equals(t, acc.Status, tc.dbacc.Status) + assert.Equals(t, acc.Contact, tc.dbacc.Contact) + assert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID) } }) } @@ -527,8 +519,8 @@ func TestDB_CreateAccount(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.CreateAccount(context.Background(), tc.acc); err != nil { + d := DB{db: tc.db} + if err := d.CreateAccount(context.Background(), tc.acc); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -688,8 +680,8 @@ func TestDB_UpdateAccount(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.UpdateAccount(context.Background(), tc.acc); err != nil { + d := DB{db: tc.db} + if err := d.UpdateAccount(context.Background(), tc.acc); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } diff --git a/acme/db/nosql/authz_test.go b/acme/db/nosql/authz_test.go index 0c2cec50..01c255dc 100644 --- a/acme/db/nosql/authz_test.go +++ b/acme/db/nosql/authz_test.go @@ -97,8 +97,8 @@ func TestDB_getDBAuthz(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if dbaz, err := db.getDBAuthz(context.Background(), azID); err != nil { + d := DB{db: tc.db} + if dbaz, err := d.getDBAuthz(context.Background(), azID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -113,18 +113,16 @@ func TestDB_getDBAuthz(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, dbaz.ID, tc.dbaz.ID) - assert.Equals(t, dbaz.AccountID, tc.dbaz.AccountID) - assert.Equals(t, dbaz.Identifier, tc.dbaz.Identifier) - assert.Equals(t, dbaz.Status, tc.dbaz.Status) - assert.Equals(t, dbaz.Token, tc.dbaz.Token) - assert.Equals(t, dbaz.CreatedAt, tc.dbaz.CreatedAt) - assert.Equals(t, dbaz.ExpiresAt, tc.dbaz.ExpiresAt) - assert.Equals(t, dbaz.Error.Error(), tc.dbaz.Error.Error()) - assert.Equals(t, dbaz.Wildcard, tc.dbaz.Wildcard) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbaz.ID, tc.dbaz.ID) + assert.Equals(t, dbaz.AccountID, tc.dbaz.AccountID) + assert.Equals(t, dbaz.Identifier, tc.dbaz.Identifier) + assert.Equals(t, dbaz.Status, tc.dbaz.Status) + assert.Equals(t, dbaz.Token, tc.dbaz.Token) + assert.Equals(t, dbaz.CreatedAt, tc.dbaz.CreatedAt) + assert.Equals(t, dbaz.ExpiresAt, tc.dbaz.ExpiresAt) + assert.Equals(t, dbaz.Error.Error(), tc.dbaz.Error.Error()) + assert.Equals(t, dbaz.Wildcard, tc.dbaz.Wildcard) } }) } @@ -293,8 +291,8 @@ func TestDB_GetAuthorization(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if az, err := db.GetAuthorization(context.Background(), azID); err != nil { + d := DB{db: tc.db} + if az, err := d.GetAuthorization(context.Background(), azID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -309,21 +307,19 @@ func TestDB_GetAuthorization(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, az.ID, tc.dbaz.ID) - assert.Equals(t, az.AccountID, tc.dbaz.AccountID) - assert.Equals(t, az.Identifier, tc.dbaz.Identifier) - assert.Equals(t, az.Status, tc.dbaz.Status) - assert.Equals(t, az.Token, tc.dbaz.Token) - assert.Equals(t, az.Wildcard, tc.dbaz.Wildcard) - assert.Equals(t, az.ExpiresAt, tc.dbaz.ExpiresAt) - assert.Equals(t, az.Challenges, []*acme.Challenge{ - {ID: "foo"}, - {ID: "bar"}, - }) - assert.Equals(t, az.Error.Error(), tc.dbaz.Error.Error()) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, az.ID, tc.dbaz.ID) + assert.Equals(t, az.AccountID, tc.dbaz.AccountID) + assert.Equals(t, az.Identifier, tc.dbaz.Identifier) + assert.Equals(t, az.Status, tc.dbaz.Status) + assert.Equals(t, az.Token, tc.dbaz.Token) + assert.Equals(t, az.Wildcard, tc.dbaz.Wildcard) + assert.Equals(t, az.ExpiresAt, tc.dbaz.ExpiresAt) + assert.Equals(t, az.Challenges, []*acme.Challenge{ + {ID: "foo"}, + {ID: "bar"}, + }) + assert.Equals(t, az.Error.Error(), tc.dbaz.Error.Error()) } }) } @@ -445,8 +441,8 @@ func TestDB_CreateAuthorization(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.CreateAuthorization(context.Background(), tc.az); err != nil { + d := DB{db: tc.db} + if err := d.CreateAuthorization(context.Background(), tc.az); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -594,8 +590,8 @@ func TestDB_UpdateAuthorization(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.UpdateAuthorization(context.Background(), tc.az); err != nil { + d := DB{db: tc.db} + if err := d.UpdateAuthorization(context.Background(), tc.az); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } diff --git a/acme/db/nosql/certificate_test.go b/acme/db/nosql/certificate_test.go index baa19383..8b6b6ef3 100644 --- a/acme/db/nosql/certificate_test.go +++ b/acme/db/nosql/certificate_test.go @@ -116,8 +116,8 @@ func TestDB_CreateCertificate(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.CreateCertificate(context.Background(), tc.cert); err != nil { + d := DB{db: tc.db} + if err := d.CreateCertificate(context.Background(), tc.cert); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -246,8 +246,8 @@ func TestDB_GetCertificate(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - cert, err := db.GetCertificate(context.Background(), certID) + d := DB{db: tc.db} + cert, err := d.GetCertificate(context.Background(), certID) if err != nil { switch k := err.(type) { case *acme.Error: @@ -263,14 +263,12 @@ func TestDB_GetCertificate(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, cert.ID, certID) - assert.Equals(t, cert.AccountID, "accountID") - assert.Equals(t, cert.OrderID, "orderID") - assert.Equals(t, cert.Leaf, leaf) - assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root}) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, cert.ID, certID) + assert.Equals(t, cert.AccountID, "accountID") + assert.Equals(t, cert.OrderID, "orderID") + assert.Equals(t, cert.Leaf, leaf) + assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root}) } }) } diff --git a/acme/db/nosql/challenge_test.go b/acme/db/nosql/challenge_test.go index b39395e8..4da5679b 100644 --- a/acme/db/nosql/challenge_test.go +++ b/acme/db/nosql/challenge_test.go @@ -92,8 +92,8 @@ func TestDB_getDBChallenge(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if ch, err := db.getDBChallenge(context.Background(), chID); err != nil { + d := DB{db: tc.db} + if ch, err := d.getDBChallenge(context.Background(), chID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -108,17 +108,15 @@ func TestDB_getDBChallenge(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, ch.ID, tc.dbc.ID) - assert.Equals(t, ch.AccountID, tc.dbc.AccountID) - assert.Equals(t, ch.Type, tc.dbc.Type) - assert.Equals(t, ch.Status, tc.dbc.Status) - assert.Equals(t, ch.Token, tc.dbc.Token) - assert.Equals(t, ch.Value, tc.dbc.Value) - assert.Equals(t, ch.ValidatedAt, tc.dbc.ValidatedAt) - assert.Equals(t, ch.Error.Error(), tc.dbc.Error.Error()) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, ch.ID, tc.dbc.ID) + assert.Equals(t, ch.AccountID, tc.dbc.AccountID) + assert.Equals(t, ch.Type, tc.dbc.Type) + assert.Equals(t, ch.Status, tc.dbc.Status) + assert.Equals(t, ch.Token, tc.dbc.Token) + assert.Equals(t, ch.Value, tc.dbc.Value) + assert.Equals(t, ch.ValidatedAt, tc.dbc.ValidatedAt) + assert.Equals(t, ch.Error.Error(), tc.dbc.Error.Error()) } }) } @@ -206,8 +204,8 @@ func TestDB_CreateChallenge(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.CreateChallenge(context.Background(), tc.ch); err != nil { + d := DB{db: tc.db} + if err := d.CreateChallenge(context.Background(), tc.ch); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -286,8 +284,8 @@ func TestDB_GetChallenge(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if ch, err := db.GetChallenge(context.Background(), chID, azID); err != nil { + d := DB{db: tc.db} + if ch, err := d.GetChallenge(context.Background(), chID, azID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -302,17 +300,15 @@ func TestDB_GetChallenge(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, ch.ID, tc.dbc.ID) - assert.Equals(t, ch.AccountID, tc.dbc.AccountID) - assert.Equals(t, ch.Type, tc.dbc.Type) - assert.Equals(t, ch.Status, tc.dbc.Status) - assert.Equals(t, ch.Token, tc.dbc.Token) - assert.Equals(t, ch.Value, tc.dbc.Value) - assert.Equals(t, ch.ValidatedAt, tc.dbc.ValidatedAt) - assert.Equals(t, ch.Error.Error(), tc.dbc.Error.Error()) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, ch.ID, tc.dbc.ID) + assert.Equals(t, ch.AccountID, tc.dbc.AccountID) + assert.Equals(t, ch.Type, tc.dbc.Type) + assert.Equals(t, ch.Status, tc.dbc.Status) + assert.Equals(t, ch.Token, tc.dbc.Token) + assert.Equals(t, ch.Value, tc.dbc.Value) + assert.Equals(t, ch.ValidatedAt, tc.dbc.ValidatedAt) + assert.Equals(t, ch.Error.Error(), tc.dbc.Error.Error()) } }) } @@ -442,8 +438,8 @@ func TestDB_UpdateChallenge(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.UpdateChallenge(context.Background(), tc.ch); err != nil { + d := DB{db: tc.db} + if err := d.UpdateChallenge(context.Background(), tc.ch); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } diff --git a/acme/db/nosql/nonce.go b/acme/db/nosql/nonce.go index 9badae87..e438c9ed 100644 --- a/acme/db/nosql/nonce.go +++ b/acme/db/nosql/nonce.go @@ -31,7 +31,7 @@ func (db *DB) CreateNonce(ctx context.Context) (acme.Nonce, error) { ID: id, CreatedAt: clock.Now(), } - if err = db.save(ctx, id, n, nil, "nonce", nonceTable); err != nil { + if err := db.save(ctx, id, n, nil, "nonce", nonceTable); err != nil { return "", err } return acme.Nonce(id), nil diff --git a/acme/db/nosql/nonce_test.go b/acme/db/nosql/nonce_test.go index 05d73d52..7dc5cc91 100644 --- a/acme/db/nosql/nonce_test.go +++ b/acme/db/nosql/nonce_test.go @@ -67,8 +67,8 @@ func TestDB_CreateNonce(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if n, err := db.CreateNonce(context.Background()); err != nil { + d := DB{db: tc.db} + if n, err := d.CreateNonce(context.Background()); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -144,8 +144,8 @@ func TestDB_DeleteNonce(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.DeleteNonce(context.Background(), acme.Nonce(nonceID)); err != nil { + d := DB{db: tc.db} + if err := d.DeleteNonce(context.Background(), acme.Nonce(nonceID)); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { diff --git a/acme/db/nosql/nosql.go b/acme/db/nosql/nosql.go index c179d2ed..34932361 100644 --- a/acme/db/nosql/nosql.go +++ b/acme/db/nosql/nosql.go @@ -42,7 +42,7 @@ func New(db nosqlDB.DB) (*DB, error) { // save writes the new data to the database, overwriting the old data if it // existed. -func (db *DB) save(ctx context.Context, id string, nu interface{}, old interface{}, typ string, table []byte) error { +func (db *DB) save(ctx context.Context, id string, nu, old interface{}, typ string, table []byte) error { var ( err error newB []byte diff --git a/acme/db/nosql/nosql_test.go b/acme/db/nosql/nosql_test.go index 4396acc8..d9c0b484 100644 --- a/acme/db/nosql/nosql_test.go +++ b/acme/db/nosql/nosql_test.go @@ -126,8 +126,8 @@ func TestDB_save(t *testing.T) { } for name, tc := range tests { t.Run(name, func(t *testing.T) { - db := &DB{db: tc.db} - if err := db.save(context.Background(), "id", tc.nu, tc.old, "challenge", challengeTable); err != nil { + d := &DB{db: tc.db} + if err := d.save(context.Background(), "id", tc.nu, tc.old, "challenge", challengeTable); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } diff --git a/acme/db/nosql/order.go b/acme/db/nosql/order.go index ba3934af..0c6bf795 100644 --- a/acme/db/nosql/order.go +++ b/acme/db/nosql/order.go @@ -124,10 +124,8 @@ func (db *DB) updateAddOrderIDs(ctx context.Context, accID string, addOids ...st ordersByAccountMux.Lock() defer ordersByAccountMux.Unlock() + var oldOids []string b, err := db.db.Get(ordersByAccountIDTable, []byte(accID)) - var ( - oldOids []string - ) if err != nil { if !nosql.IsErrNotFound(err) { return nil, errors.Wrapf(err, "error loading orderIDs for account %s", accID) diff --git a/acme/db/nosql/order_test.go b/acme/db/nosql/order_test.go index 7248700f..e92eb684 100644 --- a/acme/db/nosql/order_test.go +++ b/acme/db/nosql/order_test.go @@ -12,7 +12,7 @@ import ( "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" - nosqldb "github.com/smallstep/nosql/database" + "github.com/smallstep/nosql/database" ) func TestDB_getDBOrder(t *testing.T) { @@ -31,7 +31,7 @@ func TestDB_getDBOrder(t *testing.T) { assert.Equals(t, bucket, orderTable) assert.Equals(t, string(key), orderID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, acmeErr: acme.NewError(acme.ErrorMalformedType, "order orderID not found"), @@ -100,8 +100,8 @@ func TestDB_getDBOrder(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if dbo, err := db.getDBOrder(context.Background(), orderID); err != nil { + d := DB{db: tc.db} + if dbo, err := d.getDBOrder(context.Background(), orderID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -116,20 +116,18 @@ func TestDB_getDBOrder(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, dbo.ID, tc.dbo.ID) - assert.Equals(t, dbo.ProvisionerID, tc.dbo.ProvisionerID) - assert.Equals(t, dbo.CertificateID, tc.dbo.CertificateID) - assert.Equals(t, dbo.Status, tc.dbo.Status) - assert.Equals(t, dbo.CreatedAt, tc.dbo.CreatedAt) - assert.Equals(t, dbo.ExpiresAt, tc.dbo.ExpiresAt) - assert.Equals(t, dbo.NotBefore, tc.dbo.NotBefore) - assert.Equals(t, dbo.NotAfter, tc.dbo.NotAfter) - assert.Equals(t, dbo.Identifiers, tc.dbo.Identifiers) - assert.Equals(t, dbo.AuthorizationIDs, tc.dbo.AuthorizationIDs) - assert.Equals(t, dbo.Error.Error(), tc.dbo.Error.Error()) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, dbo.ID, tc.dbo.ID) + assert.Equals(t, dbo.ProvisionerID, tc.dbo.ProvisionerID) + assert.Equals(t, dbo.CertificateID, tc.dbo.CertificateID) + assert.Equals(t, dbo.Status, tc.dbo.Status) + assert.Equals(t, dbo.CreatedAt, tc.dbo.CreatedAt) + assert.Equals(t, dbo.ExpiresAt, tc.dbo.ExpiresAt) + assert.Equals(t, dbo.NotBefore, tc.dbo.NotBefore) + assert.Equals(t, dbo.NotAfter, tc.dbo.NotAfter) + assert.Equals(t, dbo.Identifiers, tc.dbo.Identifiers) + assert.Equals(t, dbo.AuthorizationIDs, tc.dbo.AuthorizationIDs) + assert.Equals(t, dbo.Error.Error(), tc.dbo.Error.Error()) } }) } @@ -164,7 +162,7 @@ func TestDB_GetOrder(t *testing.T) { assert.Equals(t, bucket, orderTable) assert.Equals(t, string(key), orderID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, acmeErr: acme.NewError(acme.ErrorMalformedType, "order orderID not found"), @@ -206,8 +204,8 @@ func TestDB_GetOrder(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if o, err := db.GetOrder(context.Background(), orderID); err != nil { + d := DB{db: tc.db} + if o, err := d.GetOrder(context.Background(), orderID); err != nil { switch k := err.(type) { case *acme.Error: if assert.NotNil(t, tc.acmeErr) { @@ -222,20 +220,18 @@ func TestDB_GetOrder(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, o.ID, tc.dbo.ID) - assert.Equals(t, o.AccountID, tc.dbo.AccountID) - assert.Equals(t, o.ProvisionerID, tc.dbo.ProvisionerID) - assert.Equals(t, o.CertificateID, tc.dbo.CertificateID) - assert.Equals(t, o.Status, tc.dbo.Status) - assert.Equals(t, o.ExpiresAt, tc.dbo.ExpiresAt) - assert.Equals(t, o.NotBefore, tc.dbo.NotBefore) - assert.Equals(t, o.NotAfter, tc.dbo.NotAfter) - assert.Equals(t, o.Identifiers, tc.dbo.Identifiers) - assert.Equals(t, o.AuthorizationIDs, tc.dbo.AuthorizationIDs) - assert.Equals(t, o.Error.Error(), tc.dbo.Error.Error()) - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, o.ID, tc.dbo.ID) + assert.Equals(t, o.AccountID, tc.dbo.AccountID) + assert.Equals(t, o.ProvisionerID, tc.dbo.ProvisionerID) + assert.Equals(t, o.CertificateID, tc.dbo.CertificateID) + assert.Equals(t, o.Status, tc.dbo.Status) + assert.Equals(t, o.ExpiresAt, tc.dbo.ExpiresAt) + assert.Equals(t, o.NotBefore, tc.dbo.NotBefore) + assert.Equals(t, o.NotAfter, tc.dbo.NotAfter) + assert.Equals(t, o.Identifiers, tc.dbo.Identifiers) + assert.Equals(t, o.AuthorizationIDs, tc.dbo.AuthorizationIDs) + assert.Equals(t, o.Error.Error(), tc.dbo.Error.Error()) } }) } @@ -366,8 +362,8 @@ func TestDB_UpdateOrder(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.UpdateOrder(context.Background(), tc.o); err != nil { + d := DB{db: tc.db} + if err := d.UpdateOrder(context.Background(), tc.o); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -511,7 +507,7 @@ func TestDB_CreateOrder(t *testing.T) { MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, string(bucket), string(ordersByAccountIDTable)) assert.Equals(t, string(key), o.AccountID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { switch string(bucket) { @@ -557,8 +553,8 @@ func TestDB_CreateOrder(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if err := db.CreateOrder(context.Background(), tc.o); err != nil { + d := DB{db: tc.db} + if err := d.CreateOrder(context.Background(), tc.o); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } @@ -680,7 +676,7 @@ func TestDB_updateAddOrderIDs(t *testing.T) { MGet: func(bucket, key []byte) ([]byte, error) { assert.Equals(t, bucket, ordersByAccountIDTable) assert.Equals(t, key, []byte(accID)) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { assert.Equals(t, bucket, ordersByAccountIDTable) @@ -710,6 +706,34 @@ func TestDB_updateAddOrderIDs(t *testing.T) { err: errors.Errorf("error saving orderIDs index for account %s", accID), } }, + "ok/no-old": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(ordersByAccountIDTable): + return nil, database.ErrNotFound + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + switch string(bucket) { + case string(ordersByAccountIDTable): + assert.Equals(t, key, []byte(accID)) + assert.Equals(t, old, nil) + assert.Equals(t, nu, nil) + return nil, true, nil + default: + assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket))) + return nil, false, errors.New("force") + } + }, + }, + res: []string{}, + } + }, "ok/all-old-not-pending": func(t *testing.T) test { oldOids := []string{"foo", "bar"} bOldOids, err := json.Marshal(oldOids) @@ -967,15 +991,15 @@ func TestDB_updateAddOrderIDs(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} + d := DB{db: tc.db} var ( res []string err error ) if tc.addOids == nil { - res, err = db.updateAddOrderIDs(context.Background(), accID) + res, err = d.updateAddOrderIDs(context.Background(), accID) } else { - res, err = db.updateAddOrderIDs(context.Background(), accID, tc.addOids...) + res, err = d.updateAddOrderIDs(context.Background(), accID, tc.addOids...) } if err != nil { @@ -993,10 +1017,8 @@ func TestDB_updateAddOrderIDs(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.True(t, reflect.DeepEqual(res, tc.res)) - } + } else if assert.Nil(t, tc.err) { + assert.True(t, reflect.DeepEqual(res, tc.res)) } }) } diff --git a/acme/order.go b/acme/order.go index fd8956f7..237c6979 100644 --- a/acme/order.go +++ b/acme/order.go @@ -289,6 +289,7 @@ func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.Certificate // name or in an extensionRequest attribute [RFC2985] requesting a // subjectAltName extension, or both. if csr.Subject.CommonName != "" { + // nolint:gocritic canonicalized.DNSNames = append(csr.DNSNames, csr.Subject.CommonName) } canonicalized.DNSNames = uniqueSortedLowerNames(csr.DNSNames) diff --git a/api/api.go b/api/api.go index 5be9ecc1..30ba03f9 100644 --- a/api/api.go +++ b/api/api.go @@ -240,9 +240,9 @@ type caHandler struct { } // New creates a new RouterHandler with the CA endpoints. -func New(authority Authority) RouterHandler { +func New(auth Authority) RouterHandler { return &caHandler{ - Authority: authority, + Authority: auth, } } @@ -295,7 +295,7 @@ func (h *caHandler) Health(w http.ResponseWriter, r *http.Request) { // certificate for the given SHA256. func (h *caHandler) Root(w http.ResponseWriter, r *http.Request) { sha := chi.URLParam(r, "sha") - sum := strings.ToLower(strings.Replace(sha, "-", "", -1)) + sum := strings.ToLower(strings.ReplaceAll(sha, "-", "")) // Load root certificate with the cert, err := h.Authority.Root(sum) if err != nil { @@ -409,19 +409,20 @@ func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) { "certificate": base64.StdEncoding.EncodeToString(cert.Raw), } for _, ext := range cert.Extensions { - if ext.Id.Equal(oidStepProvisioner) { - val := &stepProvisioner{} - rest, err := asn1.Unmarshal(ext.Value, val) - if err != nil || len(rest) > 0 { - break - } - if len(val.CredentialID) > 0 { - m["provisioner"] = fmt.Sprintf("%s (%s)", val.Name, val.CredentialID) - } else { - m["provisioner"] = string(val.Name) - } + if !ext.Id.Equal(oidStepProvisioner) { + continue + } + val := &stepProvisioner{} + rest, err := asn1.Unmarshal(ext.Value, val) + if err != nil || len(rest) > 0 { break } + if len(val.CredentialID) > 0 { + m["provisioner"] = fmt.Sprintf("%s (%s)", val.Name, val.CredentialID) + } else { + m["provisioner"] = string(val.Name) + } + break } rl.WithFields(m) } diff --git a/api/api_test.go b/api/api_test.go index 33d2bae7..89596165 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -186,8 +186,8 @@ func TestCertificate_MarshalJSON(t *testing.T) { }{ {"nil", fields{Certificate: nil}, []byte("null"), false}, {"empty", fields{Certificate: &x509.Certificate{Raw: nil}}, []byte(`"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n"`), false}, - {"root", fields{Certificate: parseCertificate(rootPEM)}, []byte(`"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"`), false}, - {"cert", fields{Certificate: parseCertificate(certPEM)}, []byte(`"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n"`), false}, + {"root", fields{Certificate: parseCertificate(rootPEM)}, []byte(`"` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n"`), false}, + {"cert", fields{Certificate: parseCertificate(certPEM)}, []byte(`"` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n"`), false}, } for _, tt := range tests { @@ -219,11 +219,11 @@ func TestCertificate_UnmarshalJSON(t *testing.T) { {"invalid string", []byte(`"foobar"`), false, true}, {"invalid bytes 0", []byte{}, false, true}, {"invalid bytes 1", []byte{1}, false, true}, {"empty csr", []byte(`"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE----\n"`), false, true}, - {"invalid type", []byte(`"` + strings.Replace(csrPEM, "\n", `\n`, -1) + `"`), false, true}, + {"invalid type", []byte(`"` + strings.ReplaceAll(csrPEM, "\n", `\n`) + `"`), false, true}, {"empty string", []byte(`""`), false, false}, {"json null", []byte(`null`), false, false}, - {"valid root", []byte(`"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `"`), true, false}, - {"valid cert", []byte(`"` + strings.Replace(certPEM, "\n", `\n`, -1) + `"`), true, false}, + {"valid root", []byte(`"` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `"`), true, false}, + {"valid cert", []byte(`"` + strings.ReplaceAll(certPEM, "\n", `\n`) + `"`), true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -251,7 +251,7 @@ func TestCertificate_UnmarshalJSON_json(t *testing.T) { {"empty crt (null)", `{"crt":null}`, false, false}, {"empty crt (string)", `{"crt":""}`, false, false}, {"empty crt", `{"crt":"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE----\n"}`, false, true}, - {"valid crt", `{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `"}`, true, false}, + {"valid crt", `{"crt":"` + strings.ReplaceAll(certPEM, "\n", `\n`) + `"}`, true, false}, } type request struct { @@ -297,7 +297,7 @@ func TestCertificateRequest_MarshalJSON(t *testing.T) { }{ {"nil", fields{CertificateRequest: nil}, []byte("null"), false}, {"empty", fields{CertificateRequest: &x509.CertificateRequest{}}, []byte(`"-----BEGIN CERTIFICATE REQUEST-----\n-----END CERTIFICATE REQUEST-----\n"`), false}, - {"csr", fields{CertificateRequest: parseCertificateRequest(csrPEM)}, []byte(`"` + strings.Replace(csrPEM, "\n", `\n`, -1) + `\n"`), false}, + {"csr", fields{CertificateRequest: parseCertificateRequest(csrPEM)}, []byte(`"` + strings.ReplaceAll(csrPEM, "\n", `\n`) + `\n"`), false}, } for _, tt := range tests { @@ -329,10 +329,10 @@ func TestCertificateRequest_UnmarshalJSON(t *testing.T) { {"invalid string", []byte(`"foobar"`), false, true}, {"invalid bytes 0", []byte{}, false, true}, {"invalid bytes 1", []byte{1}, false, true}, {"empty csr", []byte(`"-----BEGIN CERTIFICATE REQUEST-----\n-----END CERTIFICATE REQUEST----\n"`), false, true}, - {"invalid type", []byte(`"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `"`), false, true}, + {"invalid type", []byte(`"` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `"`), false, true}, {"empty string", []byte(`""`), false, false}, {"json null", []byte(`null`), false, false}, - {"valid csr", []byte(`"` + strings.Replace(csrPEM, "\n", `\n`, -1) + `"`), true, false}, + {"valid csr", []byte(`"` + strings.ReplaceAll(csrPEM, "\n", `\n`) + `"`), true, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -360,7 +360,7 @@ func TestCertificateRequest_UnmarshalJSON_json(t *testing.T) { {"empty csr (null)", `{"csr":null}`, false, false}, {"empty csr (string)", `{"csr":""}`, false, false}, {"empty csr", `{"csr":"-----BEGIN CERTIFICATE REQUEST-----\n-----END CERTIFICATE REQUEST----\n"}`, false, true}, - {"valid csr", `{"csr":"` + strings.Replace(csrPEM, "\n", `\n`, -1) + `"}`, true, false}, + {"valid csr", `{"csr":"` + strings.ReplaceAll(csrPEM, "\n", `\n`) + `"}`, true, false}, } type request struct { @@ -739,7 +739,7 @@ func (m *mockAuthority) CheckSSHHost(ctx context.Context, principal, token strin return m.ret1.(bool), m.err } -func (m *mockAuthority) GetSSHBastion(ctx context.Context, user string, hostname string) (*authority.Bastion, error) { +func (m *mockAuthority) GetSSHBastion(ctx context.Context, user, hostname string) (*authority.Bastion, error) { if m.getSSHBastion != nil { return m.getSSHBastion(ctx, user, hostname) } @@ -816,7 +816,7 @@ func Test_caHandler_Root(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com/root/efc7d6b475a56fe587650bcdb999a4a308f815ba44db4bf0371ea68a786ccd36", nil) req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)) - expected := []byte(`{"ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"}`) + expected := []byte(`{"ca":"` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n"}`) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -860,8 +860,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","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"]}`) + expected1 := []byte(`{"crt":"` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n","ca":"` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n","certChain":["` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n","` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n"]}`) + expected2 := []byte(`{"crt":"` + strings.ReplaceAll(stepCertPEM, "\n", `\n`) + `\n","ca":"` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n","certChain":["` + strings.ReplaceAll(stepCertPEM, "\n", `\n`) + `\n","` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n"]}`) tests := []struct { name string @@ -934,7 +934,7 @@ func Test_caHandler_Renew(t *testing.T) { {"renew error", cs, nil, nil, errs.Forbidden("an error"), http.StatusForbidden}, } - 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"]}`) + expected := []byte(`{"crt":"` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n","ca":"` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n","certChain":["` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n","` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n"]}`) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -995,7 +995,7 @@ func Test_caHandler_Rekey(t *testing.T) { {"json read error", "{", cs, nil, nil, nil, http.StatusBadRequest}, } - 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"]}`) + expected := []byte(`{"crt":"` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n","ca":"` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n","certChain":["` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n","` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n"]}`) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1210,7 +1210,7 @@ func Test_caHandler_Roots(t *testing.T) { {"fail", cs, nil, nil, fmt.Errorf("an error"), http.StatusForbidden}, } - expected := []byte(`{"crts":["` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"]}`) + expected := []byte(`{"crts":["` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n"]}`) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1256,7 +1256,7 @@ func Test_caHandler_Federation(t *testing.T) { {"fail", cs, nil, nil, fmt.Errorf("an error"), http.StatusForbidden}, } - expected := []byte(`{"crts":["` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"]}`) + expected := []byte(`{"crts":["` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n"]}`) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/api/errors.go b/api/errors.go index db3bc3e2..bff46b55 100644 --- a/api/errors.go +++ b/api/errors.go @@ -50,12 +50,10 @@ func WriteError(w http.ResponseWriter, err error) { rl.WithFields(map[string]interface{}{ "stack-trace": fmt.Sprintf("%+v", e), }) - } else { - if e, ok := cause.(errs.StackTracer); ok { - rl.WithFields(map[string]interface{}{ - "stack-trace": fmt.Sprintf("%+v", e), - }) - } + } else if e, ok := cause.(errs.StackTracer); ok { + rl.WithFields(map[string]interface{}{ + "stack-trace": fmt.Sprintf("%+v", e), + }) } } } diff --git a/api/ssh.go b/api/ssh.go index 8c0c1aa3..7c7a5acd 100644 --- a/api/ssh.go +++ b/api/ssh.go @@ -52,7 +52,7 @@ func (s *SSHSignRequest) Validate() error { return errors.Errorf("unknown certType %s", s.CertType) case len(s.PublicKey) == 0: return errors.New("missing or empty publicKey") - case len(s.OTT) == 0: + case s.OTT == "": return errors.New("missing or empty ott") default: // Validate identity signature if provided @@ -408,18 +408,18 @@ func (h *caHandler) SSHConfig(w http.ResponseWriter, r *http.Request) { return } - var config SSHConfigResponse + var cfg SSHConfigResponse switch body.Type { case provisioner.SSHUserCert: - config.UserTemplates = ts + cfg.UserTemplates = ts case provisioner.SSHHostCert: - config.HostTemplates = ts + cfg.HostTemplates = ts default: WriteError(w, errs.InternalServer("it should hot get here")) return } - JSON(w, config) + JSON(w, cfg) } // SSHCheckHost is the HTTP handler that returns if a hosts certificate exists or not. diff --git a/api/sshRekey.go b/api/sshRekey.go index 285422f9..9d9e17cf 100644 --- a/api/sshRekey.go +++ b/api/sshRekey.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "time" "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" @@ -18,7 +19,7 @@ type SSHRekeyRequest struct { // Validate validates the SSHSignRekey. func (s *SSHRekeyRequest) Validate() error { switch { - case len(s.OTT) == 0: + case s.OTT == "": return errors.New("missing or empty ott") case len(s.PublicKey) == 0: return errors.New("missing or empty public key") @@ -72,7 +73,11 @@ func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) { return } - identity, err := h.renewIdentityCertificate(r) + // Match identity cert with the SSH cert + notBefore := time.Unix(int64(oldCert.ValidAfter), 0) + notAfter := time.Unix(int64(oldCert.ValidBefore), 0) + + identity, err := h.renewIdentityCertificate(r, notBefore, notAfter) if err != nil { WriteError(w, errs.ForbiddenErr(err)) return diff --git a/api/sshRenew.go b/api/sshRenew.go index 048c83a3..d0633ecf 100644 --- a/api/sshRenew.go +++ b/api/sshRenew.go @@ -1,7 +1,9 @@ package api import ( + "crypto/x509" "net/http" + "time" "github.com/pkg/errors" "github.com/smallstep/certificates/authority/provisioner" @@ -16,7 +18,7 @@ type SSHRenewRequest struct { // Validate validates the SSHSignRequest. func (s *SSHRenewRequest) Validate() error { switch { - case len(s.OTT) == 0: + case s.OTT == "": return errors.New("missing or empty ott") default: return nil @@ -62,7 +64,11 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { return } - identity, err := h.renewIdentityCertificate(r) + // Match identity cert with the SSH cert + notBefore := time.Unix(int64(oldCert.ValidAfter), 0) + notAfter := time.Unix(int64(oldCert.ValidBefore), 0) + + identity, err := h.renewIdentityCertificate(r, notBefore, notAfter) if err != nil { WriteError(w, errs.ForbiddenErr(err)) return @@ -74,13 +80,28 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) { }, http.StatusCreated) } -// renewIdentityCertificate request the client TLS certificate if present. -func (h *caHandler) renewIdentityCertificate(r *http.Request) ([]Certificate, error) { +// renewIdentityCertificate request the client TLS certificate if present. If notBefore and notAfter are passed the +func (h *caHandler) renewIdentityCertificate(r *http.Request, notBefore, notAfter time.Time) ([]Certificate, error) { if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { return nil, nil } - certChain, err := h.Authority.Renew(r.TLS.PeerCertificates[0]) + // Clone the certificate as we can modify it. + cert, err := x509.ParseCertificate(r.TLS.PeerCertificates[0].Raw) + if err != nil { + return nil, errors.Wrap(err, "error parsing client certificate") + } + + // Enforce the cert to match another certificate, for example an ssh + // certificate. + if !notBefore.IsZero() { + cert.NotBefore = notBefore + } + if !notAfter.IsZero() { + cert.NotAfter = notAfter + } + + certChain, err := h.Authority.Renew(cert) if err != nil { return nil, err } diff --git a/api/sshRevoke.go b/api/sshRevoke.go index 5a1c858c..c6ebe99d 100644 --- a/api/sshRevoke.go +++ b/api/sshRevoke.go @@ -36,7 +36,7 @@ func (r *SSHRevokeRequest) Validate() (err error) { if !r.Passive { return errs.NotImplemented("non-passive revocation not implemented") } - if len(r.OTT) == 0 { + if r.OTT == "" { return errs.BadRequest("missing ott") } return diff --git a/api/ssh_test.go b/api/ssh_test.go index 1873a96d..a2e8748f 100644 --- a/api/ssh_test.go +++ b/api/ssh_test.go @@ -284,7 +284,7 @@ func Test_caHandler_SSHSign(t *testing.T) { identityCerts := []*x509.Certificate{ parseCertificate(certPEM), } - identityCertsPEM := []byte(`"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n"`) + identityCertsPEM := []byte(`"` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n"`) tests := []struct { name string diff --git a/authority/admin/api/middleware.go b/authority/admin/api/middleware.go index 90289f85..19025a9d 100644 --- a/authority/admin/api/middleware.go +++ b/authority/admin/api/middleware.go @@ -27,7 +27,7 @@ func (h *Handler) requireAPIEnabled(next nextHTTP) nextHTTP { func (h *Handler) extractAuthorizeTokenAdmin(next nextHTTP) nextHTTP { return func(w http.ResponseWriter, r *http.Request) { tok := r.Header.Get("Authorization") - if len(tok) == 0 { + if tok == "" { api.WriteError(w, admin.NewError(admin.ErrorUnauthorizedType, "missing authorization header token")) return diff --git a/authority/admin/db.go b/authority/admin/db.go index 15fe6686..8a6339d9 100644 --- a/authority/admin/db.go +++ b/authority/admin/db.go @@ -54,7 +54,7 @@ func UnmarshalProvisionerDetails(typ linkedca.Provisioner_Type, data []byte) (*l return &linkedca.ProvisionerDetails{Data: v.Data}, nil } -// DB is the DB interface expected by the step-ca ACME API. +// DB is the DB interface expected by the step-ca Admin API. type DB interface { CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) diff --git a/authority/admin/db/nosql/admin_test.go b/authority/admin/db/nosql/admin_test.go index 092d72db..4234d526 100644 --- a/authority/admin/db/nosql/admin_test.go +++ b/authority/admin/db/nosql/admin_test.go @@ -12,7 +12,6 @@ import ( "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" "github.com/smallstep/nosql/database" - nosqldb "github.com/smallstep/nosql/database" "go.step.sm/linkedca" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -32,7 +31,7 @@ func TestDB_getDBAdminBytes(t *testing.T) { assert.Equals(t, bucket, adminsTable) assert.Equals(t, string(key), adminID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"), @@ -67,8 +66,8 @@ func TestDB_getDBAdminBytes(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if b, err := db.getDBAdminBytes(context.Background(), adminID); err != nil { + d := DB{db: tc.db} + if b, err := d.getDBAdminBytes(context.Background(), adminID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -83,10 +82,8 @@ func TestDB_getDBAdminBytes(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) { - assert.Equals(t, string(b), "foo") - } + } else if assert.Nil(t, tc.err) { + assert.Equals(t, string(b), "foo") } }) } @@ -108,7 +105,7 @@ func TestDB_getDBAdmin(t *testing.T) { assert.Equals(t, bucket, adminsTable) assert.Equals(t, string(key), adminID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"), @@ -193,8 +190,8 @@ func TestDB_getDBAdmin(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if dba, err := db.getDBAdmin(context.Background(), adminID); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if dba, err := d.getDBAdmin(context.Background(), adminID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -209,16 +206,14 @@ func TestDB_getDBAdmin(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - assert.Equals(t, dba.ID, adminID) - assert.Equals(t, dba.AuthorityID, tc.dba.AuthorityID) - assert.Equals(t, dba.ProvisionerID, tc.dba.ProvisionerID) - assert.Equals(t, dba.Subject, tc.dba.Subject) - assert.Equals(t, dba.Type, tc.dba.Type) - assert.Equals(t, dba.CreatedAt, tc.dba.CreatedAt) - assert.Fatal(t, dba.DeletedAt.IsZero()) - } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, dba.ID, adminID) + assert.Equals(t, dba.AuthorityID, tc.dba.AuthorityID) + assert.Equals(t, dba.ProvisionerID, tc.dba.ProvisionerID) + assert.Equals(t, dba.Subject, tc.dba.Subject) + assert.Equals(t, dba.Type, tc.dba.Type) + assert.Equals(t, dba.CreatedAt, tc.dba.CreatedAt) + assert.Fatal(t, dba.DeletedAt.IsZero()) } }) } @@ -283,8 +278,8 @@ func TestDB_unmarshalDBAdmin(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{authorityID: admin.DefaultAuthorityID} - if dba, err := db.unmarshalDBAdmin(tc.in, adminID); err != nil { + d := DB{authorityID: admin.DefaultAuthorityID} + if dba, err := d.unmarshalDBAdmin(tc.in, adminID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -299,16 +294,14 @@ func TestDB_unmarshalDBAdmin(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - assert.Equals(t, dba.ID, adminID) - assert.Equals(t, dba.AuthorityID, tc.dba.AuthorityID) - assert.Equals(t, dba.ProvisionerID, tc.dba.ProvisionerID) - assert.Equals(t, dba.Subject, tc.dba.Subject) - assert.Equals(t, dba.Type, tc.dba.Type) - assert.Equals(t, dba.CreatedAt, tc.dba.CreatedAt) - assert.Fatal(t, dba.DeletedAt.IsZero()) - } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, dba.ID, adminID) + assert.Equals(t, dba.AuthorityID, tc.dba.AuthorityID) + assert.Equals(t, dba.ProvisionerID, tc.dba.ProvisionerID) + assert.Equals(t, dba.Subject, tc.dba.Subject) + assert.Equals(t, dba.Type, tc.dba.Type) + assert.Equals(t, dba.CreatedAt, tc.dba.CreatedAt) + assert.Fatal(t, dba.DeletedAt.IsZero()) } }) } @@ -360,8 +353,8 @@ func TestDB_unmarshalAdmin(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{authorityID: admin.DefaultAuthorityID} - if adm, err := db.unmarshalAdmin(tc.in, adminID); err != nil { + d := DB{authorityID: admin.DefaultAuthorityID} + if adm, err := d.unmarshalAdmin(tc.in, adminID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -376,16 +369,14 @@ func TestDB_unmarshalAdmin(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - assert.Equals(t, adm.Id, adminID) - assert.Equals(t, adm.AuthorityId, tc.dba.AuthorityID) - assert.Equals(t, adm.ProvisionerId, tc.dba.ProvisionerID) - assert.Equals(t, adm.Subject, tc.dba.Subject) - assert.Equals(t, adm.Type, tc.dba.Type) - assert.Equals(t, adm.CreatedAt, timestamppb.New(tc.dba.CreatedAt)) - assert.Equals(t, adm.DeletedAt, timestamppb.New(tc.dba.DeletedAt)) - } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, adm.Id, adminID) + assert.Equals(t, adm.AuthorityId, tc.dba.AuthorityID) + assert.Equals(t, adm.ProvisionerId, tc.dba.ProvisionerID) + assert.Equals(t, adm.Subject, tc.dba.Subject) + assert.Equals(t, adm.Type, tc.dba.Type) + assert.Equals(t, adm.CreatedAt, timestamppb.New(tc.dba.CreatedAt)) + assert.Equals(t, adm.DeletedAt, timestamppb.New(tc.dba.DeletedAt)) } }) } @@ -407,7 +398,7 @@ func TestDB_GetAdmin(t *testing.T) { assert.Equals(t, bucket, adminsTable) assert.Equals(t, string(key), adminID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"), @@ -516,8 +507,8 @@ func TestDB_GetAdmin(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if adm, err := db.GetAdmin(context.Background(), adminID); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if adm, err := d.GetAdmin(context.Background(), adminID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -532,16 +523,14 @@ func TestDB_GetAdmin(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - assert.Equals(t, adm.Id, adminID) - assert.Equals(t, adm.AuthorityId, tc.dba.AuthorityID) - assert.Equals(t, adm.ProvisionerId, tc.dba.ProvisionerID) - assert.Equals(t, adm.Subject, tc.dba.Subject) - assert.Equals(t, adm.Type, tc.dba.Type) - assert.Equals(t, adm.CreatedAt, timestamppb.New(tc.dba.CreatedAt)) - assert.Equals(t, adm.DeletedAt, timestamppb.New(tc.dba.DeletedAt)) - } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, adm.Id, adminID) + assert.Equals(t, adm.AuthorityId, tc.dba.AuthorityID) + assert.Equals(t, adm.ProvisionerId, tc.dba.ProvisionerID) + assert.Equals(t, adm.Subject, tc.dba.Subject) + assert.Equals(t, adm.Type, tc.dba.Type) + assert.Equals(t, adm.CreatedAt, timestamppb.New(tc.dba.CreatedAt)) + assert.Equals(t, adm.DeletedAt, timestamppb.New(tc.dba.DeletedAt)) } }) } @@ -562,7 +551,7 @@ func TestDB_DeleteAdmin(t *testing.T) { assert.Equals(t, bucket, adminsTable) assert.Equals(t, string(key), adminID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"), @@ -670,8 +659,8 @@ func TestDB_DeleteAdmin(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if err := db.DeleteAdmin(context.Background(), adminID); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if err := d.DeleteAdmin(context.Background(), adminID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -708,7 +697,7 @@ func TestDB_UpdateAdmin(t *testing.T) { assert.Equals(t, bucket, adminsTable) assert.Equals(t, string(key), adminID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "admin adminID not found"), @@ -821,8 +810,8 @@ func TestDB_UpdateAdmin(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if err := db.UpdateAdmin(context.Background(), tc.adm); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if err := d.UpdateAdmin(context.Background(), tc.adm); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -919,8 +908,8 @@ func TestDB_CreateAdmin(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if err := db.CreateAdmin(context.Background(), tc.adm); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if err := d.CreateAdmin(context.Background(), tc.adm); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -1095,8 +1084,8 @@ func TestDB_GetAdmins(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if admins, err := db.GetAdmins(context.Background()); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if admins, err := d.GetAdmins(context.Background()); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -1111,10 +1100,8 @@ func TestDB_GetAdmins(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - tc.verify(t, admins) - } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + tc.verify(t, admins) } }) } diff --git a/authority/admin/db/nosql/nosql.go b/authority/admin/db/nosql/nosql.go index 18599b02..22b049f5 100644 --- a/authority/admin/db/nosql/nosql.go +++ b/authority/admin/db/nosql/nosql.go @@ -35,7 +35,7 @@ func New(db nosqlDB.DB, authorityID string) (*DB, error) { // save writes the new data to the database, overwriting the old data if it // existed. -func (db *DB) save(ctx context.Context, id string, nu interface{}, old interface{}, typ string, table []byte) error { +func (db *DB) save(ctx context.Context, id string, nu, old interface{}, typ string, table []byte) error { var ( err error newB []byte diff --git a/authority/admin/db/nosql/provisioner_test.go b/authority/admin/db/nosql/provisioner_test.go index 95811f26..e599ea04 100644 --- a/authority/admin/db/nosql/provisioner_test.go +++ b/authority/admin/db/nosql/provisioner_test.go @@ -12,7 +12,6 @@ import ( "github.com/smallstep/certificates/db" "github.com/smallstep/nosql" "github.com/smallstep/nosql/database" - nosqldb "github.com/smallstep/nosql/database" "go.step.sm/linkedca" ) @@ -31,7 +30,7 @@ func TestDB_getDBProvisionerBytes(t *testing.T) { assert.Equals(t, bucket, provisionersTable) assert.Equals(t, string(key), provID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"), @@ -66,8 +65,8 @@ func TestDB_getDBProvisionerBytes(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db} - if b, err := db.getDBProvisionerBytes(context.Background(), provID); err != nil { + d := DB{db: tc.db} + if b, err := d.getDBProvisionerBytes(context.Background(), provID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -82,10 +81,8 @@ func TestDB_getDBProvisionerBytes(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - assert.Equals(t, string(b), "foo") - } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, string(b), "foo") } }) } @@ -107,7 +104,7 @@ func TestDB_getDBProvisioner(t *testing.T) { assert.Equals(t, bucket, provisionersTable) assert.Equals(t, string(key), provID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"), @@ -190,8 +187,8 @@ func TestDB_getDBProvisioner(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if dbp, err := db.getDBProvisioner(context.Background(), provID); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if dbp, err := d.getDBProvisioner(context.Background(), provID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -206,15 +203,13 @@ func TestDB_getDBProvisioner(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - assert.Equals(t, dbp.ID, provID) - assert.Equals(t, dbp.AuthorityID, tc.dbp.AuthorityID) - assert.Equals(t, dbp.Type, tc.dbp.Type) - assert.Equals(t, dbp.Name, tc.dbp.Name) - assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt) - assert.Fatal(t, dbp.DeletedAt.IsZero()) - } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, dbp.ID, provID) + assert.Equals(t, dbp.AuthorityID, tc.dbp.AuthorityID) + assert.Equals(t, dbp.Type, tc.dbp.Type) + assert.Equals(t, dbp.Name, tc.dbp.Name) + assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt) + assert.Fatal(t, dbp.DeletedAt.IsZero()) } }) } @@ -278,8 +273,8 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{authorityID: admin.DefaultAuthorityID} - if dbp, err := db.unmarshalDBProvisioner(tc.in, provID); err != nil { + d := DB{authorityID: admin.DefaultAuthorityID} + if dbp, err := d.unmarshalDBProvisioner(tc.in, provID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -294,19 +289,17 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - assert.Equals(t, dbp.ID, provID) - assert.Equals(t, dbp.AuthorityID, tc.dbp.AuthorityID) - assert.Equals(t, dbp.Type, tc.dbp.Type) - assert.Equals(t, dbp.Name, tc.dbp.Name) - assert.Equals(t, dbp.Details, tc.dbp.Details) - assert.Equals(t, dbp.Claims, tc.dbp.Claims) - assert.Equals(t, dbp.X509Template, tc.dbp.X509Template) - assert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate) - assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt) - assert.Fatal(t, dbp.DeletedAt.IsZero()) - } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, dbp.ID, provID) + assert.Equals(t, dbp.AuthorityID, tc.dbp.AuthorityID) + assert.Equals(t, dbp.Type, tc.dbp.Type) + assert.Equals(t, dbp.Name, tc.dbp.Name) + assert.Equals(t, dbp.Details, tc.dbp.Details) + assert.Equals(t, dbp.Claims, tc.dbp.Claims) + assert.Equals(t, dbp.X509Template, tc.dbp.X509Template) + assert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate) + assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt) + assert.Fatal(t, dbp.DeletedAt.IsZero()) } }) } @@ -402,8 +395,8 @@ func TestDB_unmarshalProvisioner(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{authorityID: admin.DefaultAuthorityID} - if prov, err := db.unmarshalProvisioner(tc.in, provID); err != nil { + d := DB{authorityID: admin.DefaultAuthorityID} + if prov, err := d.unmarshalProvisioner(tc.in, provID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -418,20 +411,18 @@ func TestDB_unmarshalProvisioner(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - assert.Equals(t, prov.Id, provID) - assert.Equals(t, prov.AuthorityId, tc.dbp.AuthorityID) - assert.Equals(t, prov.Type, tc.dbp.Type) - assert.Equals(t, prov.Name, tc.dbp.Name) - assert.Equals(t, prov.Claims, tc.dbp.Claims) - assert.Equals(t, prov.X509Template, tc.dbp.X509Template) - assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate) + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, prov.Id, provID) + assert.Equals(t, prov.AuthorityId, tc.dbp.AuthorityID) + assert.Equals(t, prov.Type, tc.dbp.Type) + assert.Equals(t, prov.Name, tc.dbp.Name) + assert.Equals(t, prov.Claims, tc.dbp.Claims) + assert.Equals(t, prov.X509Template, tc.dbp.X509Template) + assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate) - retDetailsBytes, err := json.Marshal(prov.Details.GetData()) - assert.FatalError(t, err) - assert.Equals(t, retDetailsBytes, tc.dbp.Details) - } + retDetailsBytes, err := json.Marshal(prov.Details.GetData()) + assert.FatalError(t, err) + assert.Equals(t, retDetailsBytes, tc.dbp.Details) } }) } @@ -453,7 +444,7 @@ func TestDB_GetProvisioner(t *testing.T) { assert.Equals(t, bucket, provisionersTable) assert.Equals(t, string(key), provID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"), @@ -542,8 +533,8 @@ func TestDB_GetProvisioner(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if prov, err := db.GetProvisioner(context.Background(), provID); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if prov, err := d.GetProvisioner(context.Background(), provID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -558,20 +549,18 @@ func TestDB_GetProvisioner(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - assert.Equals(t, prov.Id, provID) - assert.Equals(t, prov.AuthorityId, tc.dbp.AuthorityID) - assert.Equals(t, prov.Type, tc.dbp.Type) - assert.Equals(t, prov.Name, tc.dbp.Name) - assert.Equals(t, prov.Claims, tc.dbp.Claims) - assert.Equals(t, prov.X509Template, tc.dbp.X509Template) - assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate) + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + assert.Equals(t, prov.Id, provID) + assert.Equals(t, prov.AuthorityId, tc.dbp.AuthorityID) + assert.Equals(t, prov.Type, tc.dbp.Type) + assert.Equals(t, prov.Name, tc.dbp.Name) + assert.Equals(t, prov.Claims, tc.dbp.Claims) + assert.Equals(t, prov.X509Template, tc.dbp.X509Template) + assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate) - retDetailsBytes, err := json.Marshal(prov.Details.GetData()) - assert.FatalError(t, err) - assert.Equals(t, retDetailsBytes, tc.dbp.Details) - } + retDetailsBytes, err := json.Marshal(prov.Details.GetData()) + assert.FatalError(t, err) + assert.Equals(t, retDetailsBytes, tc.dbp.Details) } }) } @@ -592,7 +581,7 @@ func TestDB_DeleteProvisioner(t *testing.T) { assert.Equals(t, bucket, provisionersTable) assert.Equals(t, string(key), provID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"), @@ -692,8 +681,8 @@ func TestDB_DeleteProvisioner(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if err := db.DeleteProvisioner(context.Background(), provID); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if err := d.DeleteProvisioner(context.Background(), provID); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -853,8 +842,8 @@ func TestDB_GetProvisioners(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if provs, err := db.GetProvisioners(context.Background()); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if provs, err := d.GetProvisioners(context.Background()); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -869,10 +858,8 @@ func TestDB_GetProvisioners(t *testing.T) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } - } else { - if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { - tc.verify(t, provs) - } + } else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) { + tc.verify(t, provs) } }) } @@ -963,8 +950,8 @@ func TestDB_CreateProvisioner(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if err := db.CreateProvisioner(context.Background(), tc.prov); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if err := d.CreateProvisioner(context.Background(), tc.prov); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { @@ -1001,7 +988,7 @@ func TestDB_UpdateProvisioner(t *testing.T) { assert.Equals(t, bucket, provisionersTable) assert.Equals(t, string(key), provID) - return nil, nosqldb.ErrNotFound + return nil, database.ErrNotFound }, }, adminErr: admin.NewError(admin.ErrorNotFoundType, "provisioner provID not found"), @@ -1199,8 +1186,8 @@ func TestDB_UpdateProvisioner(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - db := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} - if err := db.UpdateProvisioner(context.Background(), tc.prov); err != nil { + d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} + if err := d.UpdateProvisioner(context.Background(), tc.prov); err != nil { switch k := err.(type) { case *admin.Error: if assert.NotNil(t, tc.adminErr) { diff --git a/authority/administrator/collection.go b/authority/administrator/collection.go index ff04a41f..88d7bb2c 100644 --- a/authority/administrator/collection.go +++ b/authority/administrator/collection.go @@ -55,8 +55,8 @@ type subProv struct { provisioner string } -func newSubProv(subject, provisioner string) subProv { - return subProv{subject, provisioner} +func newSubProv(subject, prov string) subProv { + return subProv{subject, prov} } // LoadBySubProv a admin by the subject and provisioner name. diff --git a/authority/admins.go b/authority/admins.go index dcaf9b49..b975297a 100644 --- a/authority/admins.go +++ b/authority/admins.go @@ -16,10 +16,10 @@ func (a *Authority) LoadAdminByID(id string) (*linkedca.Admin, bool) { } // LoadAdminBySubProv returns an *linkedca.Admin with the given ID. -func (a *Authority) LoadAdminBySubProv(subject, provisioner string) (*linkedca.Admin, bool) { +func (a *Authority) LoadAdminBySubProv(subject, prov string) (*linkedca.Admin, bool) { a.adminMutex.RLock() defer a.adminMutex.RUnlock() - return a.admins.LoadBySubProv(subject, provisioner) + return a.admins.LoadBySubProv(subject, prov) } // GetAdmins returns a map listing each provisioner and the JWK Key Set diff --git a/authority/authority.go b/authority/authority.go index 0f171fa7..aa8698d7 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -7,41 +7,44 @@ import ( "crypto/x509" "encoding/hex" "log" + "strings" "sync" "time" - "github.com/smallstep/certificates/cas" - "github.com/smallstep/certificates/scep" - "go.step.sm/linkedca" - "github.com/pkg/errors" "github.com/smallstep/certificates/authority/admin" adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql" "github.com/smallstep/certificates/authority/administrator" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/cas" casapi "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/kms" kmsapi "github.com/smallstep/certificates/kms/apiv1" "github.com/smallstep/certificates/kms/sshagentkms" + "github.com/smallstep/certificates/scep" "github.com/smallstep/certificates/templates" "github.com/smallstep/nosql" "go.step.sm/crypto/pemutil" + "go.step.sm/linkedca" "golang.org/x/crypto/ssh" ) // Authority implements the Certificate Authority internal interface. type Authority struct { - config *config.Config - keyManager kms.KeyManager - provisioners *provisioner.Collection - admins *administrator.Collection - db db.AuthDB - adminDB admin.DB - templates *templates.Templates + config *config.Config + keyManager kms.KeyManager + provisioners *provisioner.Collection + admins *administrator.Collection + db db.AuthDB + adminDB admin.DB + templates *templates.Templates + linkedCAToken string // X509 CA + password []byte + issuerPassword []byte x509CAService cas.CertificateAuthorityService rootX509Certs []*x509.Certificate rootX509CertPool *x509.CertPool @@ -52,6 +55,8 @@ type Authority struct { scepService *scep.Service // SSH CA + sshHostPassword []byte + sshUserPassword []byte sshCAUserCertSignKey ssh.Signer sshCAHostCertSignKey ssh.Signer sshCAUserCerts []ssh.PublicKey @@ -73,14 +78,14 @@ type Authority struct { } // New creates and initiates a new Authority type. -func New(config *config.Config, opts ...Option) (*Authority, error) { - err := config.Validate() +func New(cfg *config.Config, opts ...Option) (*Authority, error) { + err := cfg.Validate() if err != nil { return nil, err } var a = &Authority{ - config: config, + config: cfg, certificates: new(sync.Map), } @@ -205,6 +210,26 @@ func (a *Authority) init() error { var err error + // Set password if they are not set. + var configPassword []byte + if a.config.Password != "" { + configPassword = []byte(a.config.Password) + } + if configPassword != nil && a.password == nil { + a.password = configPassword + } + if a.sshHostPassword == nil { + a.sshHostPassword = a.password + } + if a.sshUserPassword == nil { + a.sshUserPassword = a.password + } + + // Automatically enable admin for all linked cas. + if a.linkedCAToken != "" { + a.config.AuthorityConfig.EnableAdmin = true + } + // Initialize step-ca Database if it's not already initialized with WithDB. // If a.config.DB is nil then a simple, barebones in memory DB will be used. if a.db == nil { @@ -232,6 +257,11 @@ func (a *Authority) init() error { options = *a.config.AuthorityConfig.Options } + // Set the issuer password if passed in the flags. + if options.CertificateIssuer != nil && a.issuerPassword != nil { + options.CertificateIssuer.Password = string(a.issuerPassword) + } + // Read intermediate and create X509 signer for default CAS. if options.Is(casapi.SoftCAS) { options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert) @@ -240,7 +270,7 @@ func (a *Authority) init() error { } options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ SigningKey: a.config.IntermediateKey, - Password: []byte(a.config.Password), + Password: []byte(a.password), }) if err != nil { return err @@ -309,7 +339,7 @@ func (a *Authority) init() error { if a.config.SSH.HostKey != "" { signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ SigningKey: a.config.SSH.HostKey, - Password: []byte(a.config.Password), + Password: []byte(a.sshHostPassword), }) if err != nil { return err @@ -335,7 +365,7 @@ func (a *Authority) init() error { if a.config.SSH.UserKey != "" { signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ SigningKey: a.config.SSH.UserKey, - Password: []byte(a.config.Password), + Password: []byte(a.sshUserPassword), }) if err != nil { return err @@ -359,33 +389,45 @@ func (a *Authority) init() error { a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, a.sshCAUserCertSignKey.PublicKey()) } - // Append other public keys + // Append other public keys and add them to the template variables. for _, key := range a.config.SSH.Keys { + publicKey := key.PublicKey() switch key.Type { case provisioner.SSHHostCert: if key.Federated { - a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, key.PublicKey()) + a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, publicKey) } else { - a.sshCAHostCerts = append(a.sshCAHostCerts, key.PublicKey()) + a.sshCAHostCerts = append(a.sshCAHostCerts, publicKey) } case provisioner.SSHUserCert: if key.Federated { - a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, key.PublicKey()) + a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, publicKey) } else { - a.sshCAUserCerts = append(a.sshCAUserCerts, key.PublicKey()) + a.sshCAUserCerts = append(a.sshCAUserCerts, publicKey) } default: return errors.Errorf("unsupported type %s", key.Type) } } + } - // Configure template variables. + // Configure template variables. On the template variables HostFederatedKeys + // and UserFederatedKeys we will skip the actual CA that will be available + // in HostKey and UserKey. + // + // We cannot do it in the previous blocks because this configuration can be + // injected using options. + if a.sshCAHostCertSignKey != nil { tmplVars.SSH.HostKey = a.sshCAHostCertSignKey.PublicKey() - tmplVars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey() - // On the templates we skip the first one because there's a distinction - // between the main key and federated keys. tmplVars.SSH.HostFederatedKeys = append(tmplVars.SSH.HostFederatedKeys, a.sshCAHostFederatedCerts[1:]...) + } else { + tmplVars.SSH.HostFederatedKeys = append(tmplVars.SSH.HostFederatedKeys, a.sshCAHostFederatedCerts...) + } + if a.sshCAUserCertSignKey != nil { + tmplVars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey() tmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts[1:]...) + } else { + tmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts...) } // Check if a KMS with decryption capability is required and available @@ -414,7 +456,7 @@ func (a *Authority) init() error { } options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ SigningKey: a.config.IntermediateKey, - Password: []byte(a.config.Password), + Password: []byte(a.password), }) if err != nil { return err @@ -423,7 +465,7 @@ func (a *Authority) init() error { if km, ok := a.keyManager.(kmsapi.Decrypter); ok { options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{ DecryptionKey: a.config.IntermediateKey, - Password: []byte(a.config.Password), + Password: []byte(a.password), }) if err != nil { return err @@ -442,10 +484,24 @@ func (a *Authority) init() error { // Initialize step-ca Admin Database if it's not already initialized using // WithAdminDB. if a.adminDB == nil { - // Check if AuthConfig already exists - a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID) - if err != nil { - return err + if a.linkedCAToken == "" { + // Check if AuthConfig already exists + a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID) + if err != nil { + return err + } + } else { + // Use the linkedca client as the admindb. + client, err := newLinkedCAClient(a.linkedCAToken) + if err != nil { + return err + } + // If authorityId is configured make sure it matches the one in the token + if id := a.config.AuthorityConfig.AuthorityID; id != "" && !strings.EqualFold(id, client.authorityID) { + return errors.New("error initializing linkedca: token authority and configured authority do not match") + } + client.Run() + a.adminDB = client } } @@ -453,9 +509,9 @@ func (a *Authority) init() error { if err != nil { return admin.WrapErrorISE(err, "error loading provisioners to initialize authority") } - if len(provs) == 0 { + if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") { // Create First Provisioner - prov, err := CreateFirstProvisioner(context.Background(), a.adminDB, a.config.Password) + prov, err := CreateFirstProvisioner(context.Background(), a.adminDB, string(a.password)) if err != nil { return admin.WrapErrorISE(err, "error creating first provisioner") } @@ -527,6 +583,9 @@ func (a *Authority) CloseForReload() { if err := a.keyManager.Close(); err != nil { log.Printf("error closing the key manager: %v", err) } + if client, ok := a.adminDB.(*linkedCaClient); ok { + client.Stop() + } } // requiresDecrypter returns whether the Authority diff --git a/authority/authority_test.go b/authority/authority_test.go index 7604ec6b..1e18a24f 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -11,6 +11,7 @@ import ( "net" "reflect" "testing" + "time" "github.com/pkg/errors" "github.com/smallstep/assert" @@ -82,6 +83,10 @@ func testAuthority(t *testing.T, opts ...Option) *Authority { } a, err := New(c, opts...) assert.FatalError(t, err) + // Avoid errors when test tokens are created before the test authority. This + // happens in some tests where we re-create the same authority to test + // special cases without re-creating the token. + a.startTime = a.startTime.Add(-1 * time.Minute) return a } diff --git a/authority/authorize.go b/authority/authorize.go index 8d1f878a..a4e7e591 100644 --- a/authority/authorize.go +++ b/authority/authorize.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "encoding/hex" "net/http" + "strconv" "strings" "time" @@ -53,7 +54,7 @@ func (a *Authority) authorizeToken(ctx context.Context, token string) (provision // key in order to verify the claims and we need the issuer from the claims // before we can look up the provisioner. var claims Claims - if err = tok.UnsafeClaimsWithoutVerification(&claims); err != nil { + if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil { return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeToken") } @@ -76,7 +77,7 @@ func (a *Authority) authorizeToken(ctx context.Context, token string) (provision // Store the token to protect against reuse unless it's skipped. // If we cannot get a token id from the provisioner, just hash the token. if !SkipTokenReuseFromContext(ctx) { - if err = a.UseToken(token, p); err != nil { + if err := a.UseToken(token, p); err != nil { return nil, err } } @@ -111,7 +112,7 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc // 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 jose.Claims - if err = jwt.Claims(leaf.PublicKey, &claims); err != nil { + if err := jwt.Claims(leaf.PublicKey, &claims); err != nil { return nil, admin.WrapError(admin.ErrorUnauthorizedType, err, "adminHandler.authorizeToken; error parsing x5c claims") } @@ -121,13 +122,13 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc } // Check that the token has not been used. - if err = a.UseToken(token, prov); err != nil { + if err := a.UseToken(token, prov); err != nil { return nil, admin.WrapError(admin.ErrorUnauthorizedType, err, "adminHandler.authorizeToken; error with reuse token") } // According to "rfc7519 JSON Web Token" acceptable skew should be no // more than a few minutes. - if err = claims.ValidateWithLeeway(jose.Expected{ + if err := claims.ValidateWithLeeway(jose.Expected{ Issuer: prov.GetName(), Time: time.Now().UTC(), }, time.Minute); err != nil { @@ -173,6 +174,9 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc } // UseToken stores the token to protect against reuse. +// +// This method currently ignores any error coming from the GetTokenID, but it +// should specifically ignore the error provisioner.ErrAllowTokenReuse. func (a *Authority) UseToken(token string, prov provisioner.Interface) error { if reuseKey, err := prov.GetTokenID(token); err == nil { if reuseKey == "" { @@ -258,7 +262,7 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error { if err != nil { return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRevoke") } - if err = p.AuthorizeRevoke(ctx, token); err != nil { + if err := p.AuthorizeRevoke(ctx, token); err != nil { return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRevoke") } return nil @@ -270,10 +274,19 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error { // // TODO(mariano): should we authorize by default? func (a *Authority) authorizeRenew(cert *x509.Certificate) error { + var err error + var isRevoked bool var opts = []interface{}{errs.WithKeyVal("serialNumber", cert.SerialNumber.String())} // Check the passive revocation table. - isRevoked, err := a.db.IsRevoked(cert.SerialNumber.String()) + serial := cert.SerialNumber.String() + if lca, ok := a.adminDB.(interface { + IsRevoked(string) (bool, error) + }); ok { + isRevoked, err = lca.IsRevoked(serial) + } else { + isRevoked, err = a.db.IsRevoked(serial) + } if err != nil { return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) } @@ -291,6 +304,28 @@ func (a *Authority) authorizeRenew(cert *x509.Certificate) error { return nil } +// authorizeSSHCertificate returns an error if the given certificate is revoked. +func (a *Authority) authorizeSSHCertificate(ctx context.Context, cert *ssh.Certificate) error { + var err error + var isRevoked bool + + serial := strconv.FormatUint(cert.Serial, 10) + if lca, ok := a.adminDB.(interface { + IsSSHRevoked(string) (bool, error) + }); ok { + isRevoked, err = lca.IsSSHRevoked(serial) + } else { + isRevoked, err = a.db.IsSSHRevoked(serial) + } + if err != nil { + return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHCertificate", errs.WithKeyVal("serialNumber", serial)) + } + if isRevoked { + return errs.Unauthorized("authority.authorizeSSHCertificate: certificate has been revoked", errs.WithKeyVal("serialNumber", serial)) + } + return nil +} + // authorizeSSHSign loads the provisioner from the token, checks that it has not // been used again and calls the provisioner AuthorizeSSHSign method. Returns a // list of methods to apply to the signing flow. diff --git a/authority/authorize_test.go b/authority/authorize_test.go index f308ec28..6d524a25 100644 --- a/authority/authorize_test.go +++ b/authority/authorize_test.go @@ -917,7 +917,7 @@ func createSSHCert(cert *ssh.Certificate, signer ssh.Signer) (*ssh.Certificate, if err != nil { return nil, nil, err } - if err = cert.SignCert(rand.Reader, signer); err != nil { + if err := cert.SignCert(rand.Reader, signer); err != nil { return nil, nil, err } return cert, jwk, nil diff --git a/authority/config/config.go b/authority/config/config.go index 9ad1ff5f..75c32994 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -75,6 +75,7 @@ type ASN1DN struct { Locality string `json:"locality,omitempty"` Province string `json:"province,omitempty"` StreetAddress string `json:"streetAddress,omitempty"` + SerialNumber string `json:"serialNumber,omitempty"` CommonName string `json:"commonName,omitempty"` } @@ -83,8 +84,9 @@ type ASN1DN struct { // cas.Options. type AuthConfig struct { *cas.Options - AuthorityID string `json:"authorityID,omitempty"` - Provisioners provisioner.List `json:"provisioners"` + AuthorityID string `json:"authorityId,omitempty"` + DeploymentType string `json:"deploymentType,omitempty"` + Provisioners provisioner.List `json:"provisioners,omitempty"` Admins []*linkedca.Admin `json:"-"` Template *ASN1DN `json:"template,omitempty"` Claims *provisioner.Claims `json:"claims,omitempty"` @@ -188,9 +190,10 @@ func (c *Config) Validate() error { switch { case c.Address == "": return errors.New("address cannot be empty") - case len(c.DNSNames) == 0: return errors.New("dnsNames cannot be empty") + case c.AuthorityConfig == nil: + return errors.New("authority cannot be nil") } // Options holds the RA/CAS configuration. @@ -222,7 +225,7 @@ func (c *Config) Validate() error { c.TLS.MaxVersion = DefaultTLSOptions.MaxVersion } if c.TLS.MinVersion == 0 { - c.TLS.MinVersion = c.TLS.MaxVersion + c.TLS.MinVersion = DefaultTLSOptions.MinVersion } if c.TLS.MinVersion > c.TLS.MaxVersion { return errors.New("tls minVersion cannot exceed tls maxVersion") diff --git a/authority/config/tls_options.go b/authority/config/tls_options.go index 996b5834..0db202e5 100644 --- a/authority/config/tls_options.go +++ b/authority/config/tls_options.go @@ -15,8 +15,9 @@ var ( // DefaultTLSRenegotiation default TLS connection renegotiation policy. DefaultTLSRenegotiation = false // Never regnegotiate. // DefaultTLSCipherSuites specifies default step ciphersuite(s). + // These are TLS 1.0 - 1.2 cipher suites. DefaultTLSCipherSuites = CipherSuites{ - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", } // ApprovedTLSCipherSuites smallstep approved ciphersuites. @@ -26,25 +27,21 @@ var ( "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", } // DefaultTLSOptions represents the default TLS version as well as the cipher // suites used in the TLS certificates. DefaultTLSOptions = TLSOptions{ - CipherSuites: CipherSuites{ - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - }, - MinVersion: 1.2, - MaxVersion: 1.2, - Renegotiation: false, + CipherSuites: DefaultTLSCipherSuites, + MinVersion: DefaultTLSMinVersion, + MaxVersion: DefaultTLSMaxVersion, + Renegotiation: DefaultTLSRenegotiation, } ) @@ -119,27 +116,38 @@ func (c CipherSuites) Value() []uint16 { // cipherSuites has the list of supported cipher suites. var cipherSuites = map[string]uint16{ - "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, - "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, - "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, - "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, - "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, - "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, - "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + // TLS 1.0 - 1.2 cipher suites. + "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, + "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA, + "TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA, + "TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, + "TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + "TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + + // TLS 1.3 cipher sutes. + "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256, + "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384, + "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, + + // Legacy names. + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, } // TLSOptions represents the TLS options that can be specified on *tls.Config diff --git a/authority/config/types.go b/authority/config/types.go index 6d7b9389..5ca3b15f 100644 --- a/authority/config/types.go +++ b/authority/config/types.go @@ -25,7 +25,7 @@ func (s multiString) HasEmpties() bool { return true } for _, ss := range s { - if len(ss) == 0 { + if ss == "" { return true } } diff --git a/authority/export.go b/authority/export.go new file mode 100644 index 00000000..8a5a257f --- /dev/null +++ b/authority/export.go @@ -0,0 +1,284 @@ +package authority + +import ( + "encoding/json" + "io/ioutil" + "net/url" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/cli-utils/config" + "go.step.sm/linkedca" + "google.golang.org/protobuf/types/known/structpb" +) + +// Export creates a linkedca configuration form the current ca.json and loaded +// authorities. +// +// Note that export will not export neither the pki password nor the certificate +// issuer password. +func (a *Authority) Export() (c *linkedca.Configuration, err error) { + // Recover from panics + defer func() { + if r := recover(); r != nil { + err = r.(error) + } + }() + + files := make(map[string][]byte) + + // The exported configuration should not include the password in it. + c = &linkedca.Configuration{ + Version: "1.0", + Root: mustReadFilesOrURIs(a.config.Root, files), + FederatedRoots: mustReadFilesOrURIs(a.config.FederatedRoots, files), + Intermediate: mustReadFileOrURI(a.config.IntermediateCert, files), + IntermediateKey: mustReadFileOrURI(a.config.IntermediateKey, files), + Address: a.config.Address, + InsecureAddress: a.config.InsecureAddress, + DnsNames: a.config.DNSNames, + Db: mustMarshalToStruct(a.config.DB), + Logger: mustMarshalToStruct(a.config.Logger), + Monitoring: mustMarshalToStruct(a.config.Monitoring), + Authority: &linkedca.Authority{ + Id: a.config.AuthorityConfig.AuthorityID, + EnableAdmin: a.config.AuthorityConfig.EnableAdmin, + DisableIssuedAtCheck: a.config.AuthorityConfig.DisableIssuedAtCheck, + Backdate: mustDuration(a.config.AuthorityConfig.Backdate), + DeploymentType: a.config.AuthorityConfig.DeploymentType, + }, + Files: files, + } + + // SSH + if v := a.config.SSH; v != nil { + c.Ssh = &linkedca.SSH{ + HostKey: mustReadFileOrURI(v.HostKey, files), + UserKey: mustReadFileOrURI(v.UserKey, files), + AddUserPrincipal: v.AddUserPrincipal, + AddUserCommand: v.AddUserCommand, + } + for _, k := range v.Keys { + typ, ok := linkedca.SSHPublicKey_Type_value[strings.ToUpper(k.Type)] + if !ok { + return nil, errors.Errorf("unsupported ssh key type %s", k.Type) + } + c.Ssh.Keys = append(c.Ssh.Keys, &linkedca.SSHPublicKey{ + Type: linkedca.SSHPublicKey_Type(typ), + Federated: k.Federated, + Key: mustMarshalToStruct(k), + }) + } + if b := v.Bastion; b != nil { + c.Ssh.Bastion = &linkedca.Bastion{ + Hostname: b.Hostname, + User: b.User, + Port: b.Port, + Command: b.Command, + Flags: b.Flags, + } + } + } + + // KMS + if v := a.config.KMS; v != nil { + var typ int32 + var ok bool + if v.Type == "" { + typ = int32(linkedca.KMS_SOFTKMS) + } else { + typ, ok = linkedca.KMS_Type_value[strings.ToUpper(v.Type)] + if !ok { + return nil, errors.Errorf("unsupported kms type %s", v.Type) + } + } + c.Kms = &linkedca.KMS{ + Type: linkedca.KMS_Type(typ), + CredentialsFile: v.CredentialsFile, + Uri: v.URI, + Pin: v.Pin, + ManagementKey: v.ManagementKey, + Region: v.Region, + Profile: v.Profile, + } + } + + // Authority + // cas options + if v := a.config.AuthorityConfig.Options; v != nil { + c.Authority.Type = 0 + c.Authority.CertificateAuthority = v.CertificateAuthority + c.Authority.CertificateAuthorityFingerprint = v.CertificateAuthorityFingerprint + c.Authority.CredentialsFile = v.CredentialsFile + if iss := v.CertificateIssuer; iss != nil { + typ, ok := linkedca.CertificateIssuer_Type_value[strings.ToUpper(iss.Type)] + if !ok { + return nil, errors.Errorf("unknown certificate issuer type %s", iss.Type) + } + // The exported certificate issuer should not include the password. + c.Authority.CertificateIssuer = &linkedca.CertificateIssuer{ + Type: linkedca.CertificateIssuer_Type(typ), + Provisioner: iss.Provisioner, + Certificate: mustReadFileOrURI(iss.Certificate, files), + Key: mustReadFileOrURI(iss.Key, files), + } + } + } + // admins + for { + list, cursor := a.admins.Find("", 100) + c.Authority.Admins = append(c.Authority.Admins, list...) + if cursor == "" { + break + } + } + // provisioners + for { + list, cursor := a.provisioners.Find("", 100) + for _, p := range list { + lp, err := ProvisionerToLinkedca(p) + if err != nil { + return nil, err + } + c.Authority.Provisioners = append(c.Authority.Provisioners, lp) + } + if cursor == "" { + break + } + } + // global claims + c.Authority.Claims = claimsToLinkedca(a.config.AuthorityConfig.Claims) + // Distinguished names template + if v := a.config.AuthorityConfig.Template; v != nil { + c.Authority.Template = &linkedca.DistinguishedName{ + Country: v.Country, + Organization: v.Organization, + OrganizationalUnit: v.OrganizationalUnit, + Locality: v.Locality, + Province: v.Province, + StreetAddress: v.StreetAddress, + SerialNumber: v.SerialNumber, + CommonName: v.CommonName, + } + } + + // TLS + if v := a.config.TLS; v != nil { + c.Tls = &linkedca.TLS{ + MinVersion: v.MinVersion.String(), + MaxVersion: v.MaxVersion.String(), + Renegotiation: v.Renegotiation, + } + for _, cs := range v.CipherSuites.Value() { + c.Tls.CipherSuites = append(c.Tls.CipherSuites, linkedca.TLS_CiperSuite(cs)) + } + } + + // Templates + if v := a.config.Templates; v != nil { + c.Templates = &linkedca.ConfigTemplates{ + Ssh: &linkedca.SSHConfigTemplate{}, + Data: mustMarshalToStruct(v.Data), + } + // Remove automatically loaded vars + if c.Templates.Data != nil && c.Templates.Data.Fields != nil { + delete(c.Templates.Data.Fields, "Step") + } + for _, t := range v.SSH.Host { + typ, ok := linkedca.ConfigTemplate_Type_value[strings.ToUpper(string(t.Type))] + if !ok { + return nil, errors.Errorf("unsupported template type %s", t.Type) + } + c.Templates.Ssh.Hosts = append(c.Templates.Ssh.Hosts, &linkedca.ConfigTemplate{ + Type: linkedca.ConfigTemplate_Type(typ), + Name: t.Name, + Template: mustReadFileOrURI(t.TemplatePath, files), + Path: t.Path, + Comment: t.Comment, + Requires: t.RequiredData, + Content: t.Content, + }) + } + for _, t := range v.SSH.User { + typ, ok := linkedca.ConfigTemplate_Type_value[strings.ToUpper(string(t.Type))] + if !ok { + return nil, errors.Errorf("unsupported template type %s", t.Type) + } + c.Templates.Ssh.Users = append(c.Templates.Ssh.Users, &linkedca.ConfigTemplate{ + Type: linkedca.ConfigTemplate_Type(typ), + Name: t.Name, + Template: mustReadFileOrURI(t.TemplatePath, files), + Path: t.Path, + Comment: t.Comment, + Requires: t.RequiredData, + Content: t.Content, + }) + } + } + + return c, nil +} + +func mustDuration(d *provisioner.Duration) string { + if d == nil || d.Duration == 0 { + return "" + } + return d.String() +} + +func mustMarshalToStruct(v interface{}) *structpb.Struct { + b, err := json.Marshal(v) + if err != nil { + panic(errors.Wrapf(err, "error marshaling %T", v)) + } + var r *structpb.Struct + if err := json.Unmarshal(b, &r); err != nil { + panic(errors.Wrapf(err, "error unmarshaling %T", v)) + } + return r +} + +func mustReadFileOrURI(fn string, m map[string][]byte) string { + if fn == "" { + return "" + } + + stepPath := filepath.ToSlash(config.StepPath()) + if !strings.HasSuffix(stepPath, "/") { + stepPath += "/" + } + + fn = strings.TrimPrefix(filepath.ToSlash(fn), stepPath) + + ok, err := isFilename(fn) + if err != nil { + panic(err) + } + if ok { + b, err := ioutil.ReadFile(config.StepAbs(fn)) + if err != nil { + panic(errors.Wrapf(err, "error reading %s", fn)) + } + m[fn] = b + return fn + } + return fn +} + +func mustReadFilesOrURIs(fns []string, m map[string][]byte) []string { + var result []string + for _, fn := range fns { + result = append(result, mustReadFileOrURI(fn, m)) + } + return result +} + +func isFilename(fn string) (bool, error) { + u, err := url.Parse(fn) + if err != nil { + return false, errors.Wrapf(err, "error parsing %s", fn) + } + return u.Scheme == "" || u.Scheme == "file", nil +} diff --git a/authority/linkedca.go b/authority/linkedca.go new file mode 100644 index 00000000..b568dcbb --- /dev/null +++ b/authority/linkedca.go @@ -0,0 +1,490 @@ +package authority + +import ( + "context" + "crypto" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "net/url" + "regexp" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/db" + "go.step.sm/crypto/jose" + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/tlsutil" + "go.step.sm/crypto/x509util" + "go.step.sm/linkedca" + "golang.org/x/crypto/ssh" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +const uuidPattern = "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + +type linkedCaClient struct { + renewer *tlsutil.Renewer + client linkedca.MajordomoClient + authorityID string +} + +type linkedCAClaims struct { + jose.Claims + SANs []string `json:"sans"` + SHA string `json:"sha"` +} + +func newLinkedCAClient(token string) (*linkedCaClient, error) { + tok, err := jose.ParseSigned(token) + if err != nil { + return nil, errors.Wrap(err, "error parsing token") + } + + var claims linkedCAClaims + if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil { + return nil, errors.Wrap(err, "error parsing token") + } + // Validate claims + if len(claims.Audience) != 1 { + return nil, errors.New("error parsing token: invalid aud claim") + } + if claims.SHA == "" { + return nil, errors.New("error parsing token: invalid sha claim") + } + // Get linkedCA endpoint from audience. + u, err := url.Parse(claims.Audience[0]) + if err != nil { + return nil, errors.New("error parsing token: invalid aud claim") + } + // Get authority from SANs + authority, err := getAuthority(claims.SANs) + if err != nil { + return nil, err + } + + // Create csr to login with + signer, err := keyutil.GenerateDefaultSigner() + if err != nil { + return nil, err + } + csr, err := x509util.CreateCertificateRequest(claims.Subject, claims.SANs, signer) + if err != nil { + return nil, err + } + + // Get and verify root certificate + root, err := getRootCertificate(u.Host, claims.SHA) + if err != nil { + return nil, err + } + + pool := x509.NewCertPool() + pool.AddCert(root) + + // Login with majordomo and get certificates + cert, tlsConfig, err := login(authority, token, csr, signer, u.Host, pool) + if err != nil { + return nil, err + } + + // Start TLS renewer and set the GetClientCertificate callback to it. + renewer, err := tlsutil.NewRenewer(cert, tlsConfig, func() (*tls.Certificate, *tls.Config, error) { + return login(authority, token, csr, signer, u.Host, pool) + }) + if err != nil { + return nil, err + } + tlsConfig.GetClientCertificate = renewer.GetClientCertificate + + // Start mTLS client + conn, err := grpc.Dial(u.Host, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) + if err != nil { + return nil, errors.Wrapf(err, "error connecting %s", u.Host) + } + + return &linkedCaClient{ + renewer: renewer, + client: linkedca.NewMajordomoClient(conn), + authorityID: authority, + }, nil +} + +func (c *linkedCaClient) Run() { + c.renewer.Run() +} + +func (c *linkedCaClient) Stop() { + c.renewer.Stop() +} + +func (c *linkedCaClient) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { + resp, err := c.client.CreateProvisioner(ctx, &linkedca.CreateProvisionerRequest{ + Type: prov.Type, + Name: prov.Name, + Details: prov.Details, + Claims: prov.Claims, + X509Template: prov.X509Template, + SshTemplate: prov.SshTemplate, + }) + if err != nil { + return errors.Wrap(err, "error creating provisioner") + } + prov.Id = resp.Id + prov.AuthorityId = resp.AuthorityId + return nil +} + +func (c *linkedCaClient) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) { + resp, err := c.client.GetProvisioner(ctx, &linkedca.GetProvisionerRequest{ + Id: id, + }) + if err != nil { + return nil, errors.Wrap(err, "error getting provisioners") + } + return resp, nil +} + +func (c *linkedCaClient) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) { + resp, err := c.client.GetConfiguration(ctx, &linkedca.ConfigurationRequest{ + AuthorityId: c.authorityID, + }) + if err != nil { + return nil, errors.Wrap(err, "error getting provisioners") + } + return resp.Provisioners, nil +} + +func (c *linkedCaClient) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error { + _, err := c.client.UpdateProvisioner(ctx, &linkedca.UpdateProvisionerRequest{ + Id: prov.Id, + Name: prov.Name, + Details: prov.Details, + Claims: prov.Claims, + X509Template: prov.X509Template, + SshTemplate: prov.SshTemplate, + }) + return errors.Wrap(err, "error updating provisioner") +} + +func (c *linkedCaClient) DeleteProvisioner(ctx context.Context, id string) error { + _, err := c.client.DeleteProvisioner(ctx, &linkedca.DeleteProvisionerRequest{ + Id: id, + }) + return errors.Wrap(err, "error deleting provisioner") +} + +func (c *linkedCaClient) CreateAdmin(ctx context.Context, adm *linkedca.Admin) error { + resp, err := c.client.CreateAdmin(ctx, &linkedca.CreateAdminRequest{ + Subject: adm.Subject, + ProvisionerId: adm.ProvisionerId, + Type: adm.Type, + }) + if err != nil { + return errors.Wrap(err, "error creating admin") + } + adm.Id = resp.Id + adm.AuthorityId = resp.AuthorityId + return nil +} + +func (c *linkedCaClient) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error) { + resp, err := c.client.GetAdmin(ctx, &linkedca.GetAdminRequest{ + Id: id, + }) + if err != nil { + return nil, errors.Wrap(err, "error getting admins") + } + return resp, nil +} + +func (c *linkedCaClient) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) { + resp, err := c.client.GetConfiguration(ctx, &linkedca.ConfigurationRequest{ + AuthorityId: c.authorityID, + }) + if err != nil { + return nil, errors.Wrap(err, "error getting admins") + } + return resp.Admins, nil +} + +func (c *linkedCaClient) UpdateAdmin(ctx context.Context, adm *linkedca.Admin) error { + _, err := c.client.UpdateAdmin(ctx, &linkedca.UpdateAdminRequest{ + Id: adm.Id, + Type: adm.Type, + }) + return errors.Wrap(err, "error updating admin") +} + +func (c *linkedCaClient) DeleteAdmin(ctx context.Context, id string) error { + _, err := c.client.DeleteAdmin(ctx, &linkedca.DeleteAdminRequest{ + Id: id, + }) + return errors.Wrap(err, "error deleting admin") +} + +func (c *linkedCaClient) StoreCertificateChain(fullchain ...*x509.Certificate) error { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + _, err := c.client.PostCertificate(ctx, &linkedca.CertificateRequest{ + PemCertificate: serializeCertificateChain(fullchain[0]), + PemCertificateChain: serializeCertificateChain(fullchain[1:]...), + }) + return errors.Wrap(err, "error posting certificate") +} + +func (c *linkedCaClient) StoreRenewedCertificate(parent *x509.Certificate, fullchain ...*x509.Certificate) error { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + _, err := c.client.PostCertificate(ctx, &linkedca.CertificateRequest{ + PemCertificate: serializeCertificateChain(fullchain[0]), + PemCertificateChain: serializeCertificateChain(fullchain[1:]...), + PemParentCertificate: serializeCertificateChain(parent), + }) + return errors.Wrap(err, "error posting certificate") +} + +func (c *linkedCaClient) StoreSSHCertificate(crt *ssh.Certificate) error { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + _, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{ + Certificate: string(ssh.MarshalAuthorizedKey(crt)), + }) + return errors.Wrap(err, "error posting ssh certificate") +} + +func (c *linkedCaClient) Revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + _, err := c.client.RevokeCertificate(ctx, &linkedca.RevokeCertificateRequest{ + Serial: rci.Serial, + PemCertificate: serializeCertificate(crt), + Reason: rci.Reason, + ReasonCode: linkedca.RevocationReasonCode(rci.ReasonCode), + Passive: true, + }) + + return errors.Wrap(err, "error revoking certificate") +} + +func (c *linkedCaClient) RevokeSSH(cert *ssh.Certificate, rci *db.RevokedCertificateInfo) error { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + _, err := c.client.RevokeSSHCertificate(ctx, &linkedca.RevokeSSHCertificateRequest{ + Serial: rci.Serial, + Certificate: serializeSSHCertificate(cert), + Reason: rci.Reason, + ReasonCode: linkedca.RevocationReasonCode(rci.ReasonCode), + Passive: true, + }) + + return errors.Wrap(err, "error revoking ssh certificate") +} + +func (c *linkedCaClient) IsRevoked(serial string) (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + resp, err := c.client.GetCertificateStatus(ctx, &linkedca.GetCertificateStatusRequest{ + Serial: serial, + }) + if err != nil { + return false, errors.Wrap(err, "error getting certificate status") + } + return resp.Status != linkedca.RevocationStatus_ACTIVE, nil +} + +func (c *linkedCaClient) IsSSHRevoked(serial string) (bool, error) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + resp, err := c.client.GetSSHCertificateStatus(ctx, &linkedca.GetSSHCertificateStatusRequest{ + Serial: serial, + }) + if err != nil { + return false, errors.Wrap(err, "error getting certificate status") + } + return resp.Status != linkedca.RevocationStatus_ACTIVE, nil +} + +func serializeCertificate(crt *x509.Certificate) string { + if crt == nil { + return "" + } + return string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + })) +} + +func serializeCertificateChain(fullchain ...*x509.Certificate) string { + var chain string + for _, crt := range fullchain { + chain += string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: crt.Raw, + })) + } + return chain +} + +func serializeSSHCertificate(crt *ssh.Certificate) string { + if crt == nil { + return "" + } + return string(ssh.MarshalAuthorizedKey(crt)) +} + +func getAuthority(sans []string) (string, error) { + for _, s := range sans { + if strings.HasPrefix(s, "urn:smallstep:authority:") { + if regexp.MustCompile(uuidPattern).MatchString(s[24:]) { + return s[24:], nil + } + } + } + return "", fmt.Errorf("error parsing token: invalid sans claim") +} + +// getRootCertificate creates an insecure majordomo client and returns the +// verified root certificate. +func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + InsecureSkipVerify: true, + }))) + if err != nil { + return nil, errors.Wrapf(err, "error connecting %s", endpoint) + } + + ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + client := linkedca.NewMajordomoClient(conn) + resp, err := client.GetRootCertificate(ctx, &linkedca.GetRootCertificateRequest{ + Fingerprint: fingerprint, + }) + if err != nil { + return nil, fmt.Errorf("error getting root certificate: %w", err) + } + + var block *pem.Block + b := []byte(resp.PemCertificate) + for len(b) > 0 { + block, b = pem.Decode(b) + if block == nil { + break + } + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("error parsing certificate: %w", err) + } + + // verify the sha256 + sum := sha256.Sum256(cert.Raw) + if !strings.EqualFold(fingerprint, hex.EncodeToString(sum[:])) { + return nil, fmt.Errorf("error verifying certificate: SHA256 fingerprint does not match") + } + + return cert, nil + } + + return nil, fmt.Errorf("error getting root certificate: certificate not found") +} + +// login creates a new majordomo client with just the root ca pool and returns +// the signed certificate and tls configuration. +func login(authority, token string, csr *x509.CertificateRequest, signer crypto.PrivateKey, endpoint string, rootCAs *x509.CertPool) (*tls.Certificate, *tls.Config, error) { + // Connect to majordomo + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: rootCAs, + }))) + if err != nil { + return nil, nil, errors.Wrapf(err, "error connecting %s", endpoint) + } + + // Login to get the signed certificate + ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + client := linkedca.NewMajordomoClient(conn) + resp, err := client.Login(ctx, &linkedca.LoginRequest{ + AuthorityId: authority, + Token: token, + PemCertificateRequest: string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csr.Raw, + })), + }) + if err != nil { + return nil, nil, errors.Wrapf(err, "error logging in %s", endpoint) + } + + // Parse login response + var block *pem.Block + var bundle []*x509.Certificate + rest := []byte(resp.PemCertificateChain) + for { + block, rest = pem.Decode(rest) + if block == nil { + break + } + if block.Type != "CERTIFICATE" { + return nil, nil, errors.New("error decoding login response: pemCertificateChain is not a certificate bundle") + } + crt, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, nil, errors.Wrap(err, "error parsing login response") + } + bundle = append(bundle, crt) + } + if len(bundle) == 0 { + return nil, nil, errors.New("error decoding login response: pemCertificateChain should not be empty") + } + + // Build tls.Certificate with PemCertificate and intermediates in the + // PemCertificateChain + cert := &tls.Certificate{ + PrivateKey: signer, + } + rest = []byte(resp.PemCertificate) + for { + block, rest = pem.Decode(rest) + if block == nil { + break + } + if block.Type == "CERTIFICATE" { + leaf, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, nil, errors.Wrap(err, "error parsing pemCertificate") + } + cert.Certificate = append(cert.Certificate, block.Bytes) + cert.Leaf = leaf + } + } + + // Add intermediates to the tls.Certificate + last := len(bundle) - 1 + for i := 0; i < last; i++ { + cert.Certificate = append(cert.Certificate, bundle[i].Raw) + } + + // Add root to the pool if it's not there yet + rootCAs.AddCert(bundle[last]) + + return cert, &tls.Config{ + RootCAs: rootCAs, + }, nil +} diff --git a/authority/options.go b/authority/options.go index 4e9fbdbc..0f80cbbf 100644 --- a/authority/options.go +++ b/authority/options.go @@ -22,9 +22,9 @@ type Option func(*Authority) error // WithConfig replaces the current config with the given one. No validation is // performed in the given value. -func WithConfig(config *config.Config) Option { +func WithConfig(cfg *config.Config) Option { return func(a *Authority) error { - a.config = config + a.config = cfg return nil } } @@ -38,11 +38,47 @@ func WithConfigFile(filename string) Option { } } +// WithPassword set the password to decrypt the intermediate key as well as the +// ssh host and user keys if they are not overridden by other options. +func WithPassword(password []byte) Option { + return func(a *Authority) (err error) { + a.password = password + return + } +} + +// WithSSHHostPassword set the password to decrypt the key used to sign SSH host +// certificates. +func WithSSHHostPassword(password []byte) Option { + return func(a *Authority) (err error) { + a.sshHostPassword = password + return + } +} + +// WithSSHUserPassword set the password to decrypt the key used to sign SSH user +// certificates. +func WithSSHUserPassword(password []byte) Option { + return func(a *Authority) (err error) { + a.sshUserPassword = password + return + } +} + +// WithIssuerPassword set the password to decrypt the certificate issuer private +// key used in RA mode. +func WithIssuerPassword(password []byte) Option { + return func(a *Authority) (err error) { + a.issuerPassword = password + return + } +} + // WithDatabase sets an already initialized authority database to a new // authority. This option is intended to be use on graceful reloads. -func WithDatabase(db db.AuthDB) Option { +func WithDatabase(d db.AuthDB) Option { return func(a *Authority) error { - a.db = db + a.db = d return nil } } @@ -189,9 +225,18 @@ func WithX509FederatedBundle(pemCerts []byte) Option { } // WithAdminDB is an option to set the database backing the admin APIs. -func WithAdminDB(db admin.DB) Option { +func WithAdminDB(d admin.DB) Option { return func(a *Authority) error { - a.adminDB = db + a.adminDB = d + return nil + } +} + +// WithLinkedCAToken is an option to set the authentication token used to enable +// linked ca. +func WithLinkedCAToken(token string) Option { + return func(a *Authority) error { + a.linkedCAToken = token return nil } } diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index c1c77ce5..cd129b7b 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -312,7 +312,7 @@ func (p *AWS) GetType() Type { } // GetEncryptedKey is not available in an AWS provisioner. -func (p *AWS) GetEncryptedKey() (kid string, key string, ok bool) { +func (p *AWS) GetEncryptedKey() (kid, key string, ok bool) { return "", "", false } @@ -449,13 +449,15 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er // There's no way to trust them other than TOFU. var so []SignOption if p.DisableCustomSANs { - dnsName := fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region) - so = append(so, dnsNamesValidator([]string{dnsName})) - so = append(so, ipAddressesValidator([]net.IP{ - net.ParseIP(doc.PrivateIP), - })) - so = append(so, emailAddressesValidator(nil)) - so = append(so, urisValidator(nil)) + dnsName := fmt.Sprintf("ip-%s.%s.compute.internal", strings.ReplaceAll(doc.PrivateIP, ".", "-"), doc.Region) + so = append(so, + dnsNamesValidator([]string{dnsName}), + ipAddressesValidator([]net.IP{ + net.ParseIP(doc.PrivateIP), + }), + emailAddressesValidator(nil), + urisValidator(nil), + ) // Template options data.SetSANs([]string{dnsName, doc.PrivateIP}) @@ -515,6 +517,11 @@ func (p *AWS) readURL(url string) ([]byte, error) { var resp *http.Response var err error + // Initialize IMDS versions when this is called from the cli. + if len(p.IMDSVersions) == 0 { + p.IMDSVersions = []string{"v2", "v1"} + } + for _, v := range p.IMDSVersions { switch v { case "v1": @@ -664,7 +671,7 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) { if p.DisableCustomSANs { if payload.Subject != doc.InstanceID && payload.Subject != doc.PrivateIP && - payload.Subject != fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region) { + payload.Subject != fmt.Sprintf("ip-%s.%s.compute.internal", strings.ReplaceAll(doc.PrivateIP, ".", "-"), doc.Region) { return nil, errs.Unauthorized("aws.authorizeToken; invalid token - invalid subject claim (sub)") } } @@ -715,7 +722,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, // Validated principals. principals := []string{ doc.PrivateIP, - fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region), + fmt.Sprintf("ip-%s.%s.compute.internal", strings.ReplaceAll(doc.PrivateIP, ".", "-"), doc.Region), } // Only enforce known principals if disable custom sans is true. diff --git a/authority/provisioner/aws_test.go b/authority/provisioner/aws_test.go index dadf1f17..0d2786db 100644 --- a/authority/provisioner/aws_test.go +++ b/authority/provisioner/aws_test.go @@ -141,6 +141,12 @@ func TestAWS_GetIdentityToken(t *testing.T) { p7.config.signatureURL = p1.config.signatureURL p7.config.tokenURL = p1.config.tokenURL + p8, err := generateAWS() + assert.FatalError(t, err) + p8.IMDSVersions = nil + p8.Accounts = p1.Accounts + p8.config = p1.config + caURL := "https://ca.smallstep.com" u, err := url.Parse(caURL) assert.FatalError(t, err) @@ -156,6 +162,7 @@ func TestAWS_GetIdentityToken(t *testing.T) { wantErr bool }{ {"ok", p1, args{"foo.local", caURL}, false}, + {"ok no imds", p8, args{"foo.local", caURL}, false}, {"fail ca url", p1, args{"foo.local", "://ca.smallstep.com"}, true}, {"fail identityURL", p2, args{"foo.local", caURL}, true}, {"fail signatureURL", p3, args{"foo.local", caURL}, true}, @@ -656,15 +663,15 @@ func TestAWS_AuthorizeSign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignMethod) - got, err := tt.aws.AuthorizeSign(ctx, tt.args.token) - if (err != nil) != tt.wantErr { + switch got, err := tt.aws.AuthorizeSign(ctx, tt.args.token); { + case (err != nil) != tt.wantErr: t.Errorf("AWS.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) return - } else if err != nil { + case err != nil: sc, ok := err.(errs.StatusCoder) assert.Fatal(t, ok, "error does not implement StatusCoder interface") assert.Equals(t, sc.StatusCode(), tt.code) - } else { + default: assert.Len(t, tt.wantLen, got) for _, o := range got { switch v := o.(type) { diff --git a/authority/provisioner/azure.go b/authority/provisioner/azure.go index 230f246f..a90d1728 100644 --- a/authority/provisioner/azure.go +++ b/authority/provisioner/azure.go @@ -131,9 +131,10 @@ func (p *Azure) GetTokenID(token string) (string, error) { return "", errors.Wrap(err, "error verifying claims") } - // If TOFU is disabled create return the token kid + // If TOFU is disabled then allow token re-use. Azure caches the token for + // 24h and without allowing the re-use we cannot use it twice. if p.DisableTrustOnFirstUse { - return claims.ID, nil + return "", ErrAllowTokenReuse } sum := sha256.Sum256([]byte(claims.XMSMirID)) @@ -151,7 +152,7 @@ func (p *Azure) GetType() Type { } // GetEncryptedKey is not available in an Azure provisioner. -func (p *Azure) GetEncryptedKey() (kid string, key string, ok bool) { +func (p *Azure) GetEncryptedKey() (kid, key string, ok bool) { return "", "", false } @@ -302,11 +303,13 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, var so []SignOption if p.DisableCustomSANs { // name will work only inside the virtual network - so = append(so, commonNameValidator(name)) - so = append(so, dnsNamesValidator([]string{name})) - so = append(so, ipAddressesValidator(nil)) - so = append(so, emailAddressesValidator(nil)) - so = append(so, urisValidator(nil)) + so = append(so, + commonNameValidator(name), + dnsNamesValidator([]string{name}), + ipAddressesValidator(nil), + emailAddressesValidator(nil), + urisValidator(nil), + ) // Enforce SANs in the template. data.SetSANs([]string{name}) diff --git a/authority/provisioner/azure_test.go b/authority/provisioner/azure_test.go index f21a5676..b7c321a6 100644 --- a/authority/provisioner/azure_test.go +++ b/authority/provisioner/azure_test.go @@ -72,7 +72,7 @@ func TestAzure_GetTokenID(t *testing.T) { wantErr bool }{ {"ok", p1, args{t1}, w1, false}, - {"ok no TOFU", p2, args{t2}, "the-jti", false}, + {"ok no TOFU", p2, args{t2}, "", true}, {"fail token", p1, args{"bad-token"}, "", true}, {"fail claims", p1, args{"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey.fooo"}, "", true}, } @@ -446,15 +446,15 @@ func TestAzure_AuthorizeSign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignMethod) - got, err := tt.azure.AuthorizeSign(ctx, tt.args.token) - if (err != nil) != tt.wantErr { + switch got, err := tt.azure.AuthorizeSign(ctx, tt.args.token); { + case (err != nil) != tt.wantErr: t.Errorf("Azure.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) return - } else if err != nil { + case err != nil: sc, ok := err.(errs.StatusCoder) assert.Fatal(t, ok, "error does not implement StatusCoder interface") assert.Equals(t, sc.StatusCode(), tt.code) - } else { + default: assert.Len(t, tt.wantLen, got) for _, o := range got { switch v := o.(type) { diff --git a/authority/provisioner/collection.go b/authority/provisioner/collection.go index 3ba98a23..1bec8689 100644 --- a/authority/provisioner/collection.go +++ b/authority/provisioner/collection.go @@ -37,8 +37,9 @@ func (p provisionerSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // provisioner. type loadByTokenPayload struct { jose.Claims - AuthorizedParty string `json:"azp"` // OIDC client id - TenantID string `json:"tid"` // Microsoft Azure tenant id + Email string `json:"email"` // OIDC email + AuthorizedParty string `json:"azp"` // OIDC client id + TenantID string `json:"tid"` // Microsoft Azure tenant id } // Collection is a memory map of provisioners. @@ -129,12 +130,20 @@ func (c *Collection) LoadByToken(token *jose.JSONWebToken, claims *jose.Claims) return p, ok } } - // Try with tid (Azure) + // Try with tid (Azure, Azure OIDC) if payload.TenantID != "" { + // Try to load an OIDC provisioner first. + if payload.Email != "" { + if p, ok := c.LoadByTokenID(payload.Audience[0]); ok { + return p, ok + } + } + // Try to load an Azure provisioner. if p, ok := c.LoadByTokenID(payload.TenantID); ok { return p, ok } } + // Fallback to aud return c.LoadByTokenID(payload.Audience[0]) } @@ -220,14 +229,15 @@ func (c *Collection) Remove(id string) error { var found bool for i, elem := range c.sorted { - if elem.provisioner.GetID() == id { - // Remove index in sorted list - copy(c.sorted[i:], c.sorted[i+1:]) // Shift a[i+1:] left one index. - c.sorted[len(c.sorted)-1] = uidProvisioner{} // Erase last element (write zero value). - c.sorted = c.sorted[:len(c.sorted)-1] // Truncate slice. - found = true - break + if elem.provisioner.GetID() != id { + continue } + // Remove index in sorted list + copy(c.sorted[i:], c.sorted[i+1:]) // Shift a[i+1:] left one index. + c.sorted[len(c.sorted)-1] = uidProvisioner{} // Erase last element (write zero value). + c.sorted = c.sorted[:len(c.sorted)-1] // Truncate slice. + found = true + break } if !found { return admin.NewError(admin.ErrorNotFoundType, "provisioner %s not found in sorted list", prov.GetName()) diff --git a/authority/provisioner/gcp.go b/authority/provisioner/gcp.go index 1b599fb3..98d776d1 100644 --- a/authority/provisioner/gcp.go +++ b/authority/provisioner/gcp.go @@ -150,7 +150,7 @@ func (p *GCP) GetType() Type { } // GetEncryptedKey is not available in a GCP provisioner. -func (p *GCP) GetEncryptedKey() (kid string, key string, ok bool) { +func (p *GCP) GetEncryptedKey() (kid, key string, ok bool) { return "", "", false } @@ -244,15 +244,17 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er if p.DisableCustomSANs { dnsName1 := fmt.Sprintf("%s.c.%s.internal", ce.InstanceName, ce.ProjectID) dnsName2 := fmt.Sprintf("%s.%s.c.%s.internal", ce.InstanceName, ce.Zone, ce.ProjectID) - so = append(so, commonNameSliceValidator([]string{ - ce.InstanceName, ce.InstanceID, dnsName1, dnsName2, - })) - so = append(so, dnsNamesValidator([]string{ - dnsName1, dnsName2, - })) - so = append(so, ipAddressesValidator(nil)) - so = append(so, emailAddressesValidator(nil)) - so = append(so, urisValidator(nil)) + so = append(so, + commonNameSliceValidator([]string{ + ce.InstanceName, ce.InstanceID, dnsName1, dnsName2, + }), + dnsNamesValidator([]string{ + dnsName1, dnsName2, + }), + ipAddressesValidator(nil), + emailAddressesValidator(nil), + urisValidator(nil), + ) // Template SANs data.SetSANs([]string{dnsName1, dnsName2}) diff --git a/authority/provisioner/gcp_test.go b/authority/provisioner/gcp_test.go index d6c4054c..5f6f9bc7 100644 --- a/authority/provisioner/gcp_test.go +++ b/authority/provisioner/gcp_test.go @@ -535,15 +535,15 @@ func TestGCP_AuthorizeSign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := NewContextWithMethod(context.Background(), SignMethod) - got, err := tt.gcp.AuthorizeSign(ctx, tt.args.token) - if (err != nil) != tt.wantErr { + switch got, err := tt.gcp.AuthorizeSign(ctx, tt.args.token); { + case (err != nil) != tt.wantErr: t.Errorf("GCP.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr) return - } else if err != nil { + case err != nil: sc, ok := err.(errs.StatusCoder) assert.Fatal(t, ok, "error does not implement StatusCoder interface") assert.Equals(t, sc.StatusCode(), tt.code) - } else { + default: assert.Len(t, tt.wantLen, got) for _, o := range got { switch v := o.(type) { diff --git a/authority/provisioner/keystore.go b/authority/provisioner/keystore.go index f775e150..d1811fab 100644 --- a/authority/provisioner/keystore.go +++ b/authority/provisioner/keystore.go @@ -18,7 +18,7 @@ const ( defaultCacheJitter = 1 * time.Hour ) -var maxAgeRegex = regexp.MustCompile("max-age=([0-9]+)") +var maxAgeRegex = regexp.MustCompile(`max-age=(\d+)`) type keyStore struct { sync.RWMutex diff --git a/authority/provisioner/noop.go b/authority/provisioner/noop.go index 18a38331..1709fbca 100644 --- a/authority/provisioner/noop.go +++ b/authority/provisioner/noop.go @@ -29,7 +29,7 @@ func (p *noop) GetType() Type { return noopType } -func (p *noop) GetEncryptedKey() (kid string, key string, ok bool) { +func (p *noop) GetEncryptedKey() (kid, key string, ok bool) { return "", "", false } diff --git a/authority/provisioner/oidc.go b/authority/provisioner/oidc.go index b6bca872..ac1f2a25 100644 --- a/authority/provisioner/oidc.go +++ b/authority/provisioner/oidc.go @@ -49,6 +49,29 @@ type openIDPayload struct { Groups []string `json:"groups"` } +func (o *openIDPayload) IsAdmin(admins []string) bool { + if o.Email != "" { + email := sanitizeEmail(o.Email) + for _, e := range admins { + if email == sanitizeEmail(e) { + return true + } + } + } + + // The groups and emails can be in the same array for now, but consider + // making a specialized option later. + for _, name := range o.Groups { + for _, admin := range admins { + if name == admin { + return true + } + } + } + + return false +} + // OIDC represents an OAuth 2.0 OpenID Connect provider. // // ClientSecret is mandatory, but it can be an empty string. @@ -73,35 +96,6 @@ type OIDC struct { getIdentityFunc GetIdentityFunc } -// IsAdmin returns true if the given email is in the Admins allowlist, false -// otherwise. -func (o *OIDC) IsAdmin(email string) bool { - if email != "" { - email = sanitizeEmail(email) - for _, e := range o.Admins { - if email == sanitizeEmail(e) { - return true - } - } - } - return false -} - -// IsAdminGroup returns true if the one group in the given list is in the Admins -// allowlist, false otherwise. -func (o *OIDC) IsAdminGroup(groups []string) bool { - for _, g := range groups { - // The groups and emails can be in the same array for now, but consider - // making a specialized option later. - for _, gadmin := range o.Admins { - if g == gadmin { - return true - } - } - } - return false -} - func sanitizeEmail(email string) string { if i := strings.LastIndex(email, "@"); i >= 0 { email = email[:i] + strings.ToLower(email[i:]) @@ -154,7 +148,7 @@ func (o *OIDC) GetType() Type { } // GetEncryptedKey is not available in an OIDC provisioner. -func (o *OIDC) GetEncryptedKey() (kid string, key string, ok bool) { +func (o *OIDC) GetEncryptedKey() (kid, key string, ok bool) { return "", "", false } @@ -199,7 +193,7 @@ func (o *OIDC) Init(config Config) (err error) { } // Replace {tenantid} with the configured one if o.TenantID != "" { - o.configuration.Issuer = strings.Replace(o.configuration.Issuer, "{tenantid}", o.TenantID, -1) + o.configuration.Issuer = strings.ReplaceAll(o.configuration.Issuer, "{tenantid}", o.TenantID) } // Get JWK key set o.keyStore, err = newKeyStore(o.configuration.JWKSetURI) @@ -234,7 +228,7 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error { } // Validate domains (case-insensitive) - if p.Email != "" && len(o.Domains) > 0 && !o.IsAdmin(p.Email) { + if p.Email != "" && len(o.Domains) > 0 && !p.IsAdmin(o.Admins) { email := sanitizeEmail(p.Email) var found bool for _, d := range o.Domains { @@ -313,9 +307,10 @@ func (o *OIDC) AuthorizeRevoke(ctx context.Context, token string) error { } // Only admins can revoke certificates. - if o.IsAdmin(claims.Email) { + if claims.IsAdmin(o.Admins) { return nil } + return errs.Unauthorized("oidc.AuthorizeRevoke; cannot revoke with non-admin oidc token") } @@ -351,7 +346,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e // Use the default template unless no-templates are configured and email is // an admin, in that case we will use the CR template. defaultTemplate := x509util.DefaultLeafTemplate - if !o.Options.GetX509Options().HasTemplate() && o.IsAdmin(claims.Email) { + if !o.Options.GetX509Options().HasTemplate() && claims.IsAdmin(o.Admins) { defaultTemplate = x509util.DefaultAdminLeafTemplate } @@ -420,10 +415,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption // Use the default template unless no-templates are configured and email is // an admin, in that case we will use the parameters in the request. - isAdmin := o.IsAdmin(claims.Email) - if !isAdmin && len(claims.Groups) > 0 { - isAdmin = o.IsAdminGroup(claims.Groups) - } + isAdmin := claims.IsAdmin(o.Admins) defaultTemplate := sshutil.DefaultTemplate if isAdmin && !o.Options.GetSSHOptions().HasTemplate() { defaultTemplate = sshutil.DefaultAdminTemplate @@ -471,10 +463,11 @@ func (o *OIDC) AuthorizeSSHRevoke(ctx context.Context, token string) error { } // Only admins can revoke certificates. - if !o.IsAdmin(claims.Email) { - return errs.Unauthorized("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token") + if claims.IsAdmin(o.Admins) { + return nil } - return nil + + return errs.Unauthorized("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token") } func getAndDecode(uri string, v interface{}) error { diff --git a/authority/provisioner/oidc_test.go b/authority/provisioner/oidc_test.go index 48f879a8..7bf6ad7a 100644 --- a/authority/provisioner/oidc_test.go +++ b/authority/provisioner/oidc_test.go @@ -321,32 +321,26 @@ func TestOIDC_AuthorizeSign(t *testing.T) { assert.Fatal(t, ok, "error does not implement StatusCoder interface") assert.Equals(t, sc.StatusCode(), tt.code) assert.Nil(t, got) - } else { - if assert.NotNil(t, got) { - if tt.name == "admin" { - assert.Len(t, 5, got) - } else { - assert.Len(t, 5, got) - } - for _, o := range got { - switch v := o.(type) { - case certificateOptionsFunc: - 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)) - } + } else if assert.NotNil(t, got) { + assert.Len(t, 5, got) + for _, o := range got { + switch v := o.(type) { + case certificateOptionsFunc: + 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)) } } } @@ -698,3 +692,39 @@ func Test_sanitizeEmail(t *testing.T) { }) } } + +func Test_openIDPayload_IsAdmin(t *testing.T) { + type fields struct { + Email string + Groups []string + } + type args struct { + admins []string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + {"ok email", fields{"admin@smallstep.com", nil}, args{[]string{"admin@smallstep.com"}}, true}, + {"ok email multiple", fields{"admin@smallstep.com", []string{"admin", "eng"}}, args{[]string{"eng@smallstep.com", "admin@smallstep.com"}}, true}, + {"ok email sanitized", fields{"admin@Smallstep.com", nil}, args{[]string{"admin@smallStep.com"}}, true}, + {"ok group", fields{"", []string{"admin"}}, args{[]string{"admin"}}, true}, + {"ok group multiple", fields{"admin@smallstep.com", []string{"engineering", "admin"}}, args{[]string{"admin"}}, true}, + {"fail missing", fields{"eng@smallstep.com", []string{"admin"}}, args{[]string{"admin@smallstep.com"}}, false}, + {"fail email letter case", fields{"Admin@smallstep.com", []string{}}, args{[]string{"admin@smallstep.com"}}, false}, + {"fail group letter case", fields{"", []string{"Admin"}}, args{[]string{"admin"}}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &openIDPayload{ + Email: tt.fields.Email, + Groups: tt.fields.Groups, + } + if got := o.IsAdmin(tt.args.admins); got != tt.want { + t.Errorf("openIDPayload.IsAdmin() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index 100aa588..f86c4863 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -138,7 +138,7 @@ func unsafeParseSigned(s string) (map[string]interface{}, error) { return nil, err } claims := make(map[string]interface{}) - if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil { + if err := token.UnsafeClaimsWithoutVerification(&claims); err != nil { return nil, err } return claims, nil diff --git a/authority/provisioner/provisioner.go b/authority/provisioner/provisioner.go index 83cc6946..5d6b2f80 100644 --- a/authority/provisioner/provisioner.go +++ b/authority/provisioner/provisioner.go @@ -4,6 +4,7 @@ import ( "context" "crypto/x509" "encoding/json" + stderrors "errors" "net/url" "regexp" "strings" @@ -32,6 +33,17 @@ type Interface interface { AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) } +// ErrAllowTokenReuse is an error that is returned by provisioners that allows +// the reuse of tokens. +// +// This is, for example, returned by the Azure provisioner when +// DisableTrustOnFirstUse is set to true. Azure caches tokens for up to 24hr and +// has no mechanism for getting a different token - this can be an issue when +// rebooting a VM. In contrast, AWS and GCP have facilities for requesting a new +// token. Therefore, for the Azure provisioner we are enabling token reuse, with +// the understanding that we are not following security best practices +var ErrAllowTokenReuse = stderrors.New("allow token reuse") + // Audiences stores all supported audiences by request type. type Audiences struct { Sign []string @@ -111,7 +123,7 @@ func (a Audiences) WithFragment(fragment string) Audiences { // generateSignAudience generates a sign audience with the format // https:///1.0/sign#provisionerID -func generateSignAudience(caURL string, provisionerID string) (string, error) { +func generateSignAudience(caURL, provisionerID string) (string, error) { u, err := url.Parse(caURL) if err != nil { return "", errors.Wrapf(err, "error parsing %s", caURL) diff --git a/authority/provisioner/sign_ssh_options.go b/authority/provisioner/sign_ssh_options.go index a872513e..158470d1 100644 --- a/authority/provisioner/sign_ssh_options.go +++ b/authority/provisioner/sign_ssh_options.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "encoding/json" "math/big" + "strings" "time" "github.com/pkg/errors" @@ -455,10 +456,10 @@ func containsAllMembers(group, subgroup []string) bool { } visit := make(map[string]struct{}, lg) for i := 0; i < lg; i++ { - visit[group[i]] = struct{}{} + visit[strings.ToLower(group[i])] = struct{}{} } for i := 0; i < lsg; i++ { - if _, ok := visit[subgroup[i]]; !ok { + if _, ok := visit[strings.ToLower(subgroup[i])]; !ok { return false } } diff --git a/authority/provisioner/sign_ssh_options_test.go b/authority/provisioner/sign_ssh_options_test.go index 693690f6..3a1ff324 100644 --- a/authority/provisioner/sign_ssh_options_test.go +++ b/authority/provisioner/sign_ssh_options_test.go @@ -44,7 +44,7 @@ func TestSSHOptions_Modify(t *testing.T) { valid func(*ssh.Certificate) err error } - tests := map[string](func() test){ + tests := map[string]func() test{ "fail/unexpected-cert-type": func() test { return test{ so: SignSSHOptions{CertType: "foo"}, @@ -117,7 +117,7 @@ func TestSSHOptions_Match(t *testing.T) { cmp SignSSHOptions err error } - tests := map[string](func() test){ + tests := map[string]func() test{ "fail/cert-type": func() test { return test{ so: SignSSHOptions{CertType: "foo"}, @@ -208,7 +208,7 @@ func Test_sshCertPrincipalsModifier_Modify(t *testing.T) { cert *ssh.Certificate expected []string } - tests := map[string](func() test){ + tests := map[string]func() test{ "ok": func() test { a := []string{"foo", "bar"} return test{ @@ -234,7 +234,7 @@ func Test_sshCertKeyIDModifier_Modify(t *testing.T) { cert *ssh.Certificate expected string } - tests := map[string](func() test){ + tests := map[string]func() test{ "ok": func() test { a := "foo" return test{ @@ -260,7 +260,7 @@ func Test_sshCertTypeModifier_Modify(t *testing.T) { cert *ssh.Certificate expected uint32 } - tests := map[string](func() test){ + tests := map[string]func() test{ "ok/user": func() test { return test{ modifier: sshCertTypeModifier("user"), @@ -299,7 +299,7 @@ func Test_sshCertValidAfterModifier_Modify(t *testing.T) { cert *ssh.Certificate expected uint64 } - tests := map[string](func() test){ + tests := map[string]func() test{ "ok": func() test { return test{ modifier: sshCertValidAfterModifier(15), @@ -324,7 +324,7 @@ func Test_sshCertDefaultsModifier_Modify(t *testing.T) { cert *ssh.Certificate valid func(*ssh.Certificate) } - tests := map[string](func() test){ + tests := map[string]func() test{ "ok/changes": func() test { n := time.Now() va := NewTimeDuration(n.Add(1 * time.Minute)) @@ -388,7 +388,7 @@ func Test_sshDefaultExtensionModifier_Modify(t *testing.T) { valid func(*ssh.Certificate) err error } - tests := map[string](func() test){ + tests := map[string]func() test{ "fail/unexpected-cert-type": func() test { cert := &ssh.Certificate{CertType: 3} return test{ diff --git a/authority/provisioner/sshpop.go b/authority/provisioner/sshpop.go index 8bc76edf..99974ff1 100644 --- a/authority/provisioner/sshpop.go +++ b/authority/provisioner/sshpop.go @@ -8,7 +8,6 @@ import ( "time" "github.com/pkg/errors" - "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "golang.org/x/crypto/ssh" @@ -30,7 +29,6 @@ type SSHPOP struct { Type string `json:"type"` Name string `json:"name"` Claims *Claims `json:"claims,omitempty"` - db db.AuthDB claimer *Claimer audiences Audiences sshPubKeys *SSHKeys @@ -102,7 +100,6 @@ func (p *SSHPOP) Init(config Config) error { } p.audiences = config.Audiences.WithFragment(p.GetIDForToken()) - p.db = config.DB p.sshPubKeys = config.SSHKeys return nil } @@ -110,6 +107,8 @@ func (p *SSHPOP) Init(config Config) error { // 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. +// +// Checking for certificate revocation has been moved to the authority package. func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayload, error) { sshCert, jwt, err := ExtractSSHPOPCert(token) if err != nil { @@ -117,14 +116,6 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa "sshpop.authorizeToken; error extracting sshpop header from token") } - // Check for revocation. - if isRevoked, err := p.db.IsSSHRevoked(strconv.FormatUint(sshCert.Serial, 10)); err != nil { - return nil, errs.Wrap(http.StatusInternalServerError, err, - "sshpop.authorizeToken; error checking checking sshpop cert revocation") - } else if isRevoked { - return nil, errs.Unauthorized("sshpop.authorizeToken; sshpop certificate is revoked") - } - // Check validity period of the certificate. n := time.Now() if sshCert.ValidAfter != 0 && time.Unix(int64(sshCert.ValidAfter), 0).After(n) { diff --git a/authority/provisioner/sshpop_test.go b/authority/provisioner/sshpop_test.go index 5d51b90e..3d343967 100644 --- a/authority/provisioner/sshpop_test.go +++ b/authority/provisioner/sshpop_test.go @@ -11,7 +11,6 @@ import ( "github.com/pkg/errors" "github.com/smallstep/assert" - "github.com/smallstep/certificates/db" "github.com/smallstep/certificates/errs" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" @@ -47,7 +46,7 @@ func createSSHCert(cert *ssh.Certificate, signer ssh.Signer) (*ssh.Certificate, if err != nil { return nil, nil, err } - if err = cert.SignCert(rand.Reader, signer); err != nil { + if err := cert.SignCert(rand.Reader, signer); err != nil { return nil, nil, err } return cert, jwk, nil @@ -83,52 +82,9 @@ func TestSSHPOP_authorizeToken(t *testing.T) { err: errors.New("sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: "), } }, - "fail/error-revoked-db-check": func(t *testing.T) test { - p, err := generateSSHPOP() - assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, errors.New("force") - }, - } - cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) - assert.FatalError(t, err) - tok, err := generateSSHPOPToken(p, cert, jwk) - assert.FatalError(t, err) - return test{ - p: p, - token: tok, - code: http.StatusInternalServerError, - err: errors.New("sshpop.authorizeToken; error checking checking sshpop cert revocation: force"), - } - }, - "fail/cert-already-revoked": func(t *testing.T) test { - p, err := generateSSHPOP() - assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return true, nil - }, - } - cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) - assert.FatalError(t, err) - tok, err := generateSSHPOPToken(p, cert, jwk) - assert.FatalError(t, err) - return test{ - p: p, - token: tok, - code: http.StatusUnauthorized, - err: errors.New("sshpop.authorizeToken; sshpop certificate is revoked"), - } - }, "fail/cert-not-yet-valid": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{ CertType: ssh.UserCert, ValidAfter: uint64(time.Now().Add(time.Minute).Unix()), @@ -146,11 +102,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) { "fail/cert-past-validity": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{ CertType: ssh.UserCert, ValidBefore: uint64(time.Now().Add(-time.Minute).Unix()), @@ -168,11 +119,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) { "fail/no-signer-found": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner) assert.FatalError(t, err) tok, err := generateSSHPOPToken(p, cert, jwk) @@ -187,11 +133,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) { "fail/error-parsing-claims-bad-sig": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, _, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) assert.FatalError(t, err) otherJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) @@ -208,11 +149,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) { "fail/invalid-claims-issuer": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) assert.FatalError(t, err) tok, err := generateToken("foo", "bar", testAudiences.Sign[0], "", @@ -228,11 +164,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) { "fail/invalid-audience": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) assert.FatalError(t, err) tok, err := generateToken("foo", p.GetName(), "invalid-aud", "", @@ -248,11 +179,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) { "fail/empty-subject": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) assert.FatalError(t, err) tok, err := generateToken("", p.GetName(), testAudiences.Sign[0], "", @@ -268,11 +194,6 @@ func TestSSHPOP_authorizeToken(t *testing.T) { "ok": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) assert.FatalError(t, err) tok, err := generateSSHPOPToken(p, cert, jwk) @@ -293,10 +214,8 @@ func TestSSHPOP_authorizeToken(t *testing.T) { 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) - } + } else if assert.Nil(t, tc.err) { + assert.NotNil(t, claims) } }) } @@ -330,11 +249,6 @@ func TestSSHPOP_AuthorizeSSHRevoke(t *testing.T) { "fail/subject-not-equal-serial": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner) assert.FatalError(t, err) tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRevoke[0], "", @@ -350,11 +264,6 @@ func TestSSHPOP_AuthorizeSSHRevoke(t *testing.T) { "ok": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.UserCert}, sshSigner) assert.FatalError(t, err) tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRevoke[0], "", @@ -419,11 +328,6 @@ func TestSSHPOP_AuthorizeSSHRenew(t *testing.T) { "fail/not-host-cert": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshUserSigner) assert.FatalError(t, err) tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRenew[0], "", @@ -439,11 +343,6 @@ func TestSSHPOP_AuthorizeSSHRenew(t *testing.T) { "ok": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner) assert.FatalError(t, err) tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRenew[0], "", @@ -511,11 +410,6 @@ func TestSSHPOP_AuthorizeSSHRekey(t *testing.T) { "fail/not-host-cert": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshUserSigner) assert.FatalError(t, err) tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRekey[0], "", @@ -531,11 +425,6 @@ func TestSSHPOP_AuthorizeSSHRekey(t *testing.T) { "ok": func(t *testing.T) test { p, err := generateSSHPOP() assert.FatalError(t, err) - p.db = &db.MockAuthDB{ - MIsSSHRevoked: func(sn string) (bool, error) { - return false, nil - }, - } cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner) assert.FatalError(t, err) tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRekey[0], "", diff --git a/authority/provisioner/utils_test.go b/authority/provisioner/utils_test.go index 534e83cf..e39efbcf 100644 --- a/authority/provisioner/utils_test.go +++ b/authority/provisioner/utils_test.go @@ -732,7 +732,7 @@ func withSSHPOPFile(cert *ssh.Certificate) tokOption { } } -func generateToken(sub, iss, aud string, email string, sans []string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) { +func generateToken(sub, iss, aud, 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) @@ -773,7 +773,7 @@ func generateToken(sub, iss, aud string, email string, sans []string, iat time.T return jose.Signed(sig).Claims(claims).CompactSerialize() } -func generateOIDCToken(sub, iss, aud string, email string, preferredUsername string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) { +func generateOIDCToken(sub, iss, aud, email, preferredUsername string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) { so := new(jose.SignerOptions) so.WithType("JWT") so.WithHeader("kid", jwk.KeyID) diff --git a/authority/provisioners.go b/authority/provisioners.go index d2581e76..7e02126f 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -4,12 +4,17 @@ import ( "context" "crypto/x509" "encoding/json" + "encoding/pem" "fmt" + "io/ioutil" + "github.com/pkg/errors" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/errs" + step "go.step.sm/cli-utils/config" + "go.step.sm/cli-utils/ui" "go.step.sm/crypto/jose" "go.step.sm/linkedca" "gopkg.in/square/go-jose.v2/jwt" @@ -234,6 +239,14 @@ func (a *Authority) RemoveProvisioner(ctx context.Context, id string) error { } func CreateFirstProvisioner(ctx context.Context, db admin.DB, password string) (*linkedca.Provisioner, error) { + if password == "" { + pass, err := ui.PromptPasswordGenerate("Please enter the password to encrypt your first provisioner, leave empty and we'll generate one") + if err != nil { + return nil, err + } + password = string(pass) + } + jwk, jwe, err := jose.GenerateDefaultKeyPair([]byte(password)) if err != nil { return nil, admin.WrapErrorISE(err, "error generating JWK key pair") @@ -398,6 +411,13 @@ func durationsToCertificates(d *linkedca.Durations) (min, max, def *provisioner. return } +func durationsToLinkedca(d *provisioner.Duration) string { + if d == nil { + return "" + } + return d.Duration.String() +} + // claimsToCertificates converts the linkedca provisioner claims type to the // certifictes claims type. func claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) { @@ -438,6 +458,109 @@ func claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) { return pc, nil } +func claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims { + if c == nil { + return nil + } + + disableRenewal := config.DefaultDisableRenewal + if c.DisableRenewal != nil { + disableRenewal = *c.DisableRenewal + } + + lc := &linkedca.Claims{ + DisableRenewal: disableRenewal, + } + + if c.DefaultTLSDur != nil || c.MinTLSDur != nil || c.MaxTLSDur != nil { + lc.X509 = &linkedca.X509Claims{ + Enabled: true, + Durations: &linkedca.Durations{ + Default: durationsToLinkedca(c.DefaultTLSDur), + Min: durationsToLinkedca(c.MinTLSDur), + Max: durationsToLinkedca(c.MaxTLSDur), + }, + } + } + + if c.EnableSSHCA != nil && *c.EnableSSHCA { + lc.Ssh = &linkedca.SSHClaims{ + Enabled: true, + } + if c.DefaultUserSSHDur != nil || c.MinUserSSHDur != nil || c.MaxUserSSHDur != nil { + lc.Ssh.UserDurations = &linkedca.Durations{ + Default: durationsToLinkedca(c.DefaultUserSSHDur), + Min: durationsToLinkedca(c.MinUserSSHDur), + Max: durationsToLinkedca(c.MaxUserSSHDur), + } + } + if c.DefaultHostSSHDur != nil || c.MinHostSSHDur != nil || c.MaxHostSSHDur != nil { + lc.Ssh.HostDurations = &linkedca.Durations{ + Default: durationsToLinkedca(c.DefaultHostSSHDur), + Min: durationsToLinkedca(c.MinHostSSHDur), + Max: durationsToLinkedca(c.MaxHostSSHDur), + } + } + } + + return lc +} + +func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *linkedca.Template, error) { + var err error + var x509Template, sshTemplate *linkedca.Template + + if p == nil { + return nil, nil, nil + } + + if p.X509 != nil && p.X509.HasTemplate() { + x509Template = &linkedca.Template{ + Template: nil, + Data: nil, + } + + if p.X509.Template != "" { + x509Template.Template = []byte(p.SSH.Template) + } else if p.X509.TemplateFile != "" { + filename := step.StepAbs(p.X509.TemplateFile) + if x509Template.Template, err = ioutil.ReadFile(filename); err != nil { + return nil, nil, errors.Wrap(err, "error reading x509 template") + } + } + } + + if p.SSH != nil && p.SSH.HasTemplate() { + sshTemplate = &linkedca.Template{ + Template: nil, + Data: nil, + } + + if p.SSH.Template != "" { + sshTemplate.Template = []byte(p.SSH.Template) + } else if p.SSH.TemplateFile != "" { + filename := step.StepAbs(p.SSH.TemplateFile) + if sshTemplate.Template, err = ioutil.ReadFile(filename); err != nil { + return nil, nil, errors.Wrap(err, "error reading ssh template") + } + } + } + + return x509Template, sshTemplate, nil +} + +func provisionerPEMToLinkedca(b []byte) [][]byte { + var roots [][]byte + var block *pem.Block + for { + if block, b = pem.Decode(b); block == nil { + break + } + roots = append(roots, pem.EncodeToMemory(block)) + } + return roots +} + // ProvisionerToCertificates converts the linkedca provisioner type to the certificates provisioner // interface. func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, error) { @@ -448,7 +571,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, details := p.Details.GetData() if details == nil { - return nil, fmt.Errorf("provisioner does not have any details") + return nil, errors.New("provisioner does not have any details") } options := optionsToCertificates(p) @@ -457,7 +580,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, case *linkedca.ProvisionerDetails_JWK: jwk := new(jose.JSONWebKey) if err := json.Unmarshal(d.JWK.PublicKey, &jwk); err != nil { - return nil, err + return nil, errors.Wrap(err, "error unmarshaling public key") } return &provisioner.JWK{ ID: p.Id, @@ -588,6 +711,233 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, } } +// ProvisionerToLinkedca converts a provisioner.Interface to a +// linkedca.Provisioner type. +func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, error) { + switch p := p.(type) { + case *provisioner.JWK: + x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + if err != nil { + return nil, err + } + publicKey, err := json.Marshal(p.Key) + if err != nil { + return nil, errors.Wrap(err, "error marshaling key") + } + return &linkedca.Provisioner{ + Id: p.ID, + Type: linkedca.Provisioner_JWK, + Name: p.GetName(), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_JWK{ + JWK: &linkedca.JWKProvisioner{ + PublicKey: publicKey, + EncryptedPrivateKey: []byte(p.EncryptedKey), + }, + }, + }, + Claims: claimsToLinkedca(p.Claims), + X509Template: x509Template, + SshTemplate: sshTemplate, + }, nil + case *provisioner.OIDC: + x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + if err != nil { + return nil, err + } + return &linkedca.Provisioner{ + Id: p.ID, + Type: linkedca.Provisioner_OIDC, + Name: p.GetName(), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_OIDC{ + OIDC: &linkedca.OIDCProvisioner{ + ClientId: p.ClientID, + ClientSecret: p.ClientSecret, + ConfigurationEndpoint: p.ConfigurationEndpoint, + Admins: p.Admins, + Domains: p.Domains, + Groups: p.Groups, + ListenAddress: p.ListenAddress, + TenantId: p.TenantID, + }, + }, + }, + Claims: claimsToLinkedca(p.Claims), + X509Template: x509Template, + SshTemplate: sshTemplate, + }, nil + case *provisioner.GCP: + x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + if err != nil { + return nil, err + } + return &linkedca.Provisioner{ + Id: p.ID, + Type: linkedca.Provisioner_GCP, + Name: p.GetName(), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_GCP{ + GCP: &linkedca.GCPProvisioner{ + ServiceAccounts: p.ServiceAccounts, + ProjectIds: p.ProjectIDs, + DisableCustomSans: p.DisableCustomSANs, + DisableTrustOnFirstUse: p.DisableTrustOnFirstUse, + InstanceAge: p.InstanceAge.String(), + }, + }, + }, + Claims: claimsToLinkedca(p.Claims), + X509Template: x509Template, + SshTemplate: sshTemplate, + }, nil + case *provisioner.AWS: + x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + if err != nil { + return nil, err + } + return &linkedca.Provisioner{ + Id: p.ID, + Type: linkedca.Provisioner_AWS, + Name: p.GetName(), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_AWS{ + AWS: &linkedca.AWSProvisioner{ + Accounts: p.Accounts, + DisableCustomSans: p.DisableCustomSANs, + DisableTrustOnFirstUse: p.DisableTrustOnFirstUse, + InstanceAge: p.InstanceAge.String(), + }, + }, + }, + Claims: claimsToLinkedca(p.Claims), + X509Template: x509Template, + SshTemplate: sshTemplate, + }, nil + case *provisioner.Azure: + x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + if err != nil { + return nil, err + } + return &linkedca.Provisioner{ + Id: p.ID, + Type: linkedca.Provisioner_AZURE, + Name: p.GetName(), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_Azure{ + Azure: &linkedca.AzureProvisioner{ + TenantId: p.TenantID, + ResourceGroups: p.ResourceGroups, + Audience: p.Audience, + DisableCustomSans: p.DisableCustomSANs, + DisableTrustOnFirstUse: p.DisableTrustOnFirstUse, + }, + }, + }, + Claims: claimsToLinkedca(p.Claims), + X509Template: x509Template, + SshTemplate: sshTemplate, + }, nil + case *provisioner.ACME: + x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + if err != nil { + return nil, err + } + return &linkedca.Provisioner{ + Id: p.ID, + Type: linkedca.Provisioner_ACME, + Name: p.GetName(), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_ACME{ + ACME: &linkedca.ACMEProvisioner{ + ForceCn: p.ForceCN, + }, + }, + }, + Claims: claimsToLinkedca(p.Claims), + X509Template: x509Template, + SshTemplate: sshTemplate, + }, nil + case *provisioner.X5C: + x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + if err != nil { + return nil, err + } + return &linkedca.Provisioner{ + Id: p.ID, + Type: linkedca.Provisioner_X5C, + Name: p.GetName(), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_X5C{ + X5C: &linkedca.X5CProvisioner{ + Roots: provisionerPEMToLinkedca(p.Roots), + }, + }, + }, + Claims: claimsToLinkedca(p.Claims), + X509Template: x509Template, + SshTemplate: sshTemplate, + }, nil + case *provisioner.K8sSA: + x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + if err != nil { + return nil, err + } + return &linkedca.Provisioner{ + Id: p.ID, + Type: linkedca.Provisioner_K8SSA, + Name: p.GetName(), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_K8SSA{ + K8SSA: &linkedca.K8SSAProvisioner{ + PublicKeys: provisionerPEMToLinkedca(p.PubKeys), + }, + }, + }, + Claims: claimsToLinkedca(p.Claims), + X509Template: x509Template, + SshTemplate: sshTemplate, + }, nil + case *provisioner.SSHPOP: + return &linkedca.Provisioner{ + Id: p.ID, + Type: linkedca.Provisioner_SSHPOP, + Name: p.GetName(), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_SSHPOP{ + SSHPOP: &linkedca.SSHPOPProvisioner{}, + }, + }, + Claims: claimsToLinkedca(p.Claims), + }, nil + case *provisioner.SCEP: + x509Template, sshTemplate, err := provisionerOptionsToLinkedca(p.Options) + if err != nil { + return nil, err + } + return &linkedca.Provisioner{ + Id: p.ID, + Type: linkedca.Provisioner_SCEP, + Name: p.GetName(), + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_SCEP{ + SCEP: &linkedca.SCEPProvisioner{ + ForceCn: p.ForceCN, + Challenge: p.GetChallengePassword(), + Capabilities: p.Capabilities, + MinimumPublicKeyLength: int32(p.MinimumPublicKeyLength), + }, + }, + }, + Claims: claimsToLinkedca(p.Claims), + X509Template: x509Template, + SshTemplate: sshTemplate, + }, nil + default: + return nil, fmt.Errorf("provisioner %s not implemented", p.GetType()) + } +} + func parseInstanceAge(age string) (provisioner.Duration, error) { var instanceAge provisioner.Duration if age != "" { diff --git a/authority/ssh.go b/authority/ssh.go index 335b6702..762319ae 100644 --- a/authority/ssh.go +++ b/authority/ssh.go @@ -108,7 +108,7 @@ func (a *Authority) GetSSHConfig(ctx context.Context, typ string, data map[strin // GetSSHBastion returns the bastion configuration, for the given pair user, // hostname. -func (a *Authority) GetSSHBastion(ctx context.Context, user string, hostname string) (*config.Bastion, error) { +func (a *Authority) GetSSHBastion(ctx context.Context, user, hostname string) (*config.Bastion, error) { if a.sshBastionFunc != nil { bs, err := a.sshBastionFunc(ctx, user, hostname) return bs, errs.Wrap(http.StatusInternalServerError, err, "authority.GetSSHBastion") @@ -239,7 +239,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi } } - if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error storing certificate in db") } @@ -249,7 +249,11 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi // RenewSSH creates a signed SSH certificate using the old SSH certificate as a template. func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, error) { if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 { - return nil, errs.BadRequest("rewnewSSH: cannot renew certificate without validity period") + return nil, errs.BadRequest("renewSSH: cannot renew certificate without validity period") + } + + if err := a.authorizeSSHCertificate(ctx, oldCert); err != nil { + return nil, err } backdate := a.config.AuthorityConfig.Backdate.Duration @@ -294,7 +298,7 @@ func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ss return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate") } - if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db") } @@ -319,6 +323,10 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub return nil, errs.BadRequest("rekeySSH; cannot rekey certificate without validity period") } + if err := a.authorizeSSHCertificate(ctx, oldCert); err != nil { + return nil, err + } + backdate := a.config.AuthorityConfig.Backdate.Duration duration := time.Duration(oldCert.ValidBefore-oldCert.ValidAfter) * time.Second now := time.Now() @@ -369,13 +377,23 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub } } - if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db") } return cert, nil } +func (a *Authority) storeSSHCertificate(cert *ssh.Certificate) error { + type sshCertificateStorer interface { + StoreSSHCertificate(crt *ssh.Certificate) error + } + if s, ok := a.adminDB.(sshCertificateStorer); ok { + return s.StoreSSHCertificate(cert) + } + return a.db.StoreSSHCertificate(cert) +} + // IsValidForAddUser checks if a user provisioner certificate can be issued to // the given certificate. func IsValidForAddUser(cert *ssh.Certificate) error { @@ -451,7 +469,7 @@ func (a *Authority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, subje } cert.Signature = sig - if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { + if err = a.storeSSHCertificate(cert); err != nil && err != db.ErrNotImplemented { return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error storing certificate in db") } @@ -459,7 +477,7 @@ func (a *Authority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, subje } // CheckSSHHost checks the given principal has been registered before. -func (a *Authority) CheckSSHHost(ctx context.Context, principal string, token string) (bool, error) { +func (a *Authority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) { if a.sshCheckHostFunc != nil { exists, err := a.sshCheckHostFunc(ctx, principal, token, a.GetRootCertificates()) if err != nil { @@ -513,5 +531,5 @@ func (a *Authority) getAddUserCommand(principal string) string { } else { cmd = a.config.SSH.AddUserCommand } - return strings.Replace(cmd, "", principal, -1) + return strings.ReplaceAll(cmd, "", principal) } diff --git a/authority/ssh_test.go b/authority/ssh_test.go index 8ca26af0..41df8576 100644 --- a/authority/ssh_test.go +++ b/authority/ssh_test.go @@ -87,6 +87,52 @@ func (m sshTestOptionsModifier) Modify(cert *ssh.Certificate, opts provisioner.S return fmt.Errorf(string(m)) } +func TestAuthority_initHostOnly(t *testing.T) { + auth := testAuthority(t, func(a *Authority) error { + a.config.SSH.UserKey = "" + return nil + }) + + // Check keys + keys, err := auth.GetSSHRoots(context.Background()) + assert.NoError(t, err) + assert.Len(t, 1, keys.HostKeys) + assert.Len(t, 0, keys.UserKeys) + + // Check templates, user templates should work fine. + _, err = auth.GetSSHConfig(context.Background(), "user", nil) + assert.NoError(t, err) + + _, err = auth.GetSSHConfig(context.Background(), "host", map[string]string{ + "Certificate": "ssh_host_ecdsa_key-cert.pub", + "Key": "ssh_host_ecdsa_key", + }) + assert.Error(t, err) +} + +func TestAuthority_initUserOnly(t *testing.T) { + auth := testAuthority(t, func(a *Authority) error { + a.config.SSH.HostKey = "" + return nil + }) + + // Check keys + keys, err := auth.GetSSHRoots(context.Background()) + assert.NoError(t, err) + assert.Len(t, 0, keys.HostKeys) + assert.Len(t, 1, keys.UserKeys) + + // Check templates, host templates should work fine. + _, err = auth.GetSSHConfig(context.Background(), "host", map[string]string{ + "Certificate": "ssh_host_ecdsa_key-cert.pub", + "Key": "ssh_host_ecdsa_key", + }) + assert.NoError(t, err) + + _, err = auth.GetSSHConfig(context.Background(), "user", nil) + assert.Error(t, err) +} + func TestAuthority_SignSSH(t *testing.T) { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.FatalError(t, err) @@ -153,6 +199,8 @@ func TestAuthority_SignSSH(t *testing.T) { }{ {"ok-user", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false}, {"ok-host", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false}, + {"ok-user-only", fields{signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false}, + {"ok-host-only", fields{nil, signer}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false}, {"ok-opts-type-user", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user"}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert}, false}, {"ok-opts-type-host", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "host"}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert}, false}, {"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SignSSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false}, @@ -750,6 +798,11 @@ func TestAuthority_RekeySSH(t *testing.T) { now := time.Now().UTC() a := testAuthority(t) + a.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, + } type test struct { auth *Authority @@ -763,6 +816,56 @@ func TestAuthority_RekeySSH(t *testing.T) { code int } tests := map[string]func(t *testing.T) *test{ + "fail/is-revoked": func(t *testing.T) *test { + auth := testAuthority(t) + auth.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return true, nil + }, + } + return &test{ + auth: auth, + userSigner: signer, + hostSigner: signer, + cert: &ssh.Certificate{ + Serial: 1234567890, + ValidAfter: uint64(now.Unix()), + ValidBefore: uint64(now.Add(time.Hour).Unix()), + CertType: ssh.UserCert, + ValidPrincipals: []string{"foo", "bar"}, + KeyId: "foo", + }, + key: pub, + signOpts: []provisioner.SignOption{}, + err: errors.New("authority.authorizeSSHCertificate: certificate has been revoked"), + code: http.StatusUnauthorized, + } + }, + "fail/is-revoked-error": func(t *testing.T) *test { + auth := testAuthority(t) + auth.db = &db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, errors.New("an error") + }, + } + return &test{ + auth: auth, + userSigner: signer, + hostSigner: signer, + cert: &ssh.Certificate{ + Serial: 1234567890, + ValidAfter: uint64(now.Unix()), + ValidBefore: uint64(now.Add(time.Hour).Unix()), + CertType: ssh.UserCert, + ValidPrincipals: []string{"foo", "bar"}, + KeyId: "foo", + }, + key: pub, + signOpts: []provisioner.SignOption{}, + err: errors.New("authority.authorizeSSHCertificate: an error"), + code: http.StatusInternalServerError, + } + }, "fail/opts-type": func(t *testing.T) *test { return &test{ userSigner: signer, @@ -831,6 +934,9 @@ func TestAuthority_RekeySSH(t *testing.T) { "fail/db-store": func(t *testing.T) *test { return &test{ auth: testAuthority(t, WithDatabase(&db.MockAuthDB{ + MIsSSHRevoked: func(sn string) (bool, error) { + return false, nil + }, MStoreSSHCertificate: func(cert *ssh.Certificate) error { return errors.New("force") }, diff --git a/authority/tls.go b/authority/tls.go index 36f8c4f4..a77afecf 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -21,6 +21,7 @@ import ( "go.step.sm/crypto/keyutil" "go.step.sm/crypto/pemutil" "go.step.sm/crypto/x509util" + "golang.org/x/crypto/ssh" ) // GetTLSOptions returns the tls options configured. @@ -36,7 +37,6 @@ func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc { if def == nil { return errors.New("default ASN1DN template cannot be nil") } - if len(crt.Subject.Country) == 0 && def.Country != "" { crt.Subject.Country = append(crt.Subject.Country, def.Country) } @@ -55,7 +55,12 @@ func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc { if len(crt.Subject.StreetAddress) == 0 && def.StreetAddress != "" { crt.Subject.StreetAddress = append(crt.Subject.StreetAddress, def.StreetAddress) } - + if crt.Subject.SerialNumber == "" && def.SerialNumber != "" { + crt.Subject.SerialNumber = def.SerialNumber + } + if crt.Subject.CommonName == "" && def.CommonName != "" { + crt.Subject.CommonName = def.CommonName + } return nil } } @@ -280,9 +285,15 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5 // `StoreCertificate(...*x509.Certificate) error` instead of just // `StoreCertificate(*x509.Certificate) error`. func (a *Authority) storeCertificate(fullchain []*x509.Certificate) error { - if s, ok := a.db.(interface { + type certificateChainStorer interface { StoreCertificateChain(...*x509.Certificate) error - }); ok { + } + // Store certificate in linkedca + if s, ok := a.adminDB.(certificateChainStorer); ok { + return s.StoreCertificateChain(fullchain...) + } + // Store certificate in local db + if s, ok := a.db.(certificateChainStorer); ok { return s.StoreCertificateChain(fullchain...) } return a.db.StoreCertificate(fullchain[0]) @@ -293,9 +304,15 @@ func (a *Authority) storeCertificate(fullchain []*x509.Certificate) error { // // TODO: at some point we should implement this in the standard implementation. func (a *Authority) storeRenewedCertificate(oldCert *x509.Certificate, fullchain []*x509.Certificate) error { - if s, ok := a.db.(interface { + type renewedCertificateChainStorer interface { StoreRenewedCertificate(*x509.Certificate, ...*x509.Certificate) error - }); ok { + } + // Store certificate in linkedca + if s, ok := a.adminDB.(renewedCertificateChainStorer); ok { + return s.StoreRenewedCertificate(oldCert, fullchain...) + } + // Store certificate in local db + if s, ok := a.db.(renewedCertificateChainStorer); ok { return s.StoreRenewedCertificate(oldCert, fullchain...) } return a.db.StoreCertificate(fullchain[0]) @@ -368,22 +385,22 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error } rci.ProvisionerID = p.GetID() rci.TokenID, err = p.GetTokenID(revokeOpts.OTT) - if err != nil { + if err != nil && !errors.Is(err, provisioner.ErrAllowTokenReuse) { return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke; could not get ID for token") } - opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID)) - opts = append(opts, errs.WithKeyVal("tokenID", rci.TokenID)) - } else { + opts = append(opts, + errs.WithKeyVal("provisionerID", rci.ProvisionerID), + errs.WithKeyVal("tokenID", rci.TokenID), + ) + } else if p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil { // Load the Certificate provisioner if one exists. - if p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil { - rci.ProvisionerID = p.GetID() - opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID)) - } + rci.ProvisionerID = p.GetID() + opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID)) } if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod { - err = a.db.RevokeSSH(rci) + err = a.revokeSSH(nil, rci) } else { // Revoke an X.509 certificate using CAS. If the certificate is not // provided we will try to read it from the db. If the read fails we @@ -410,7 +427,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error } // Save as revoked in the Db. - err = a.db.Revoke(rci) + err = a.revoke(revokedCert, rci) } switch err { case nil: @@ -425,6 +442,24 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error } } +func (a *Authority) revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error { + if lca, ok := a.adminDB.(interface { + Revoke(*x509.Certificate, *db.RevokedCertificateInfo) error + }); ok { + return lca.Revoke(crt, rci) + } + return a.db.Revoke(rci) +} + +func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateInfo) error { + if lca, ok := a.adminDB.(interface { + RevokeSSH(*ssh.Certificate, *db.RevokedCertificateInfo) error + }); ok { + return lca.RevokeSSH(crt, rci) + } + return a.db.Revoke(rci) +} + // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { fatal := func(err error) (*tls.Certificate, error) { diff --git a/authority/tls_test.go b/authority/tls_test.go index cdd4c59a..f1d1748d 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -426,6 +426,7 @@ ZYtQ9Ot36qc= {Id: stepOIDProvisioner, Value: []byte("foo")}, {Id: []int{1, 1, 1}, Value: []byte("bar")}})) now := time.Now().UTC() + // nolint:gocritic enforcedExtraOptions := append(extraOpts, &certificateDurationEnforcer{ NotBefore: now, NotAfter: now.Add(365 * 24 * time.Hour), diff --git a/ca/acmeClient.go b/ca/acmeClient.go index 5633dac5..d1f40f32 100644 --- a/ca/acmeClient.go +++ b/ca/acmeClient.go @@ -345,7 +345,7 @@ func readACMEError(r io.ReadCloser) error { ae := new(acme.Error) err = json.Unmarshal(b, &ae) // If we successfully marshaled to an ACMEError then return the ACMEError. - if err != nil || len(ae.Error()) == 0 { + if err != nil || ae.Error() == "" { fmt.Printf("b = %s\n", b) // Throw up our hands. return errors.Errorf("%s", b) diff --git a/ca/acmeClient_test.go b/ca/acmeClient_test.go index f5963de4..656a82cf 100644 --- a/ca/acmeClient_test.go +++ b/ca/acmeClient_test.go @@ -1247,6 +1247,7 @@ func TestACMEClient_GetCertificate(t *testing.T) { Type: "Certificate", Bytes: leaf.Raw, }) + // nolint:gocritic certBytes := append(leafb, leafb...) certBytes = append(certBytes, leafb...) ac := &ACMEClient{ diff --git a/ca/adminClient.go b/ca/adminClient.go index 2f3d4b5d..6022f677 100644 --- a/ca/adminClient.go +++ b/ca/adminClient.go @@ -70,7 +70,7 @@ func NewAdminClient(endpoint string, opts ...ClientOption) (*AdminClient, error) }, nil } -func (c *AdminClient) generateAdminToken(path string) (string, error) { +func (c *AdminClient) generateAdminToken(urlPath string) (string, error) { // A random jwt id will be used to identify duplicated tokens jwtID, err := randutil.Hex(64) // 256 bits if err != nil { @@ -82,7 +82,7 @@ func (c *AdminClient) generateAdminToken(path string) (string, error) { token.WithJWTID(jwtID), token.WithKid(c.x5cJWK.KeyID), token.WithIssuer(c.x5cIssuer), - token.WithAudience(path), + token.WithAudience(urlPath), token.WithValidity(now, now.Add(token.DefaultValidity)), token.WithX5CCerts(c.x5cCertStrs), } @@ -348,14 +348,15 @@ func (c *AdminClient) GetProvisioner(opts ...ProvisionerOption) (*linkedca.Provi return nil, err } var u *url.URL - if len(o.id) > 0 { + switch { + case len(o.id) > 0: u = c.endpoint.ResolveReference(&url.URL{ Path: "/admin/provisioners/id", RawQuery: o.rawQuery(), }) - } else if len(o.name) > 0 { + case len(o.name) > 0: u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.name)}) - } else { + default: return nil, errors.New("must set either name or id in method options") } tok, err := c.generateAdminToken(u.Path) @@ -456,14 +457,15 @@ func (c *AdminClient) RemoveProvisioner(opts ...ProvisionerOption) error { return err } - if len(o.id) > 0 { + switch { + case len(o.id) > 0: u = c.endpoint.ResolveReference(&url.URL{ Path: path.Join(adminURLPrefix, "provisioners/id"), RawQuery: o.rawQuery(), }) - } else if len(o.name) > 0 { + case len(o.name) > 0: u = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "provisioners", o.name)}) - } else { + default: return errors.New("must set either name or id in method options") } tok, err := c.generateAdminToken(u.Path) diff --git a/ca/bootstrap.go b/ca/bootstrap.go index 5f06e986..42087985 100644 --- a/ca/bootstrap.go +++ b/ca/bootstrap.go @@ -30,7 +30,7 @@ func Bootstrap(token string) (*Client, error) { // Validate bootstrap token switch { - case len(claims.SHA) == 0: + case claims.SHA == "": return nil, errors.New("invalid bootstrap token: sha claim is not present") case !strings.HasPrefix(strings.ToLower(claims.Audience[0]), "http"): return nil, errors.New("invalid bootstrap token: aud claim is not a url") diff --git a/ca/ca.go b/ca/ca.go index 4551286b..c76e8c0a 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -29,10 +29,13 @@ import ( ) type options struct { - configFile string - password []byte - issuerPassword []byte - database db.AuthDB + configFile string + linkedCAToken string + password []byte + issuerPassword []byte + sshHostPassword []byte + sshUserPassword []byte + database db.AuthDB } func (o *options) apply(opts []Option) { @@ -60,6 +63,22 @@ func WithPassword(password []byte) Option { } } +// WithSSHHostPassword sets the given password to decrypt the key used to sign +// ssh host certificates. +func WithSSHHostPassword(password []byte) Option { + return func(o *options) { + o.sshHostPassword = password + } +} + +// WithSSHUserPassword sets the given password to decrypt the key used to sign +// ssh user certificates. +func WithSSHUserPassword(password []byte) Option { + return func(o *options) { + o.sshUserPassword = password + } +} + // WithIssuerPassword sets the given password as the configured certificate // issuer password in the CA options. func WithIssuerPassword(password []byte) Option { @@ -69,9 +88,16 @@ func WithIssuerPassword(password []byte) Option { } // WithDatabase sets the given authority database to the CA options. -func WithDatabase(db db.AuthDB) Option { +func WithDatabase(d db.AuthDB) Option { return func(o *options) { - o.database = db + o.database = d + } +} + +// WithLinkedCAToken sets the token used to authenticate with the linkedca. +func WithLinkedCAToken(token string) Option { + return func(o *options) { + o.linkedCAToken = token } } @@ -87,35 +113,34 @@ type CA struct { } // New creates and initializes the CA with the given configuration and options. -func New(config *config.Config, opts ...Option) (*CA, error) { +func New(cfg *config.Config, opts ...Option) (*CA, error) { ca := &CA{ - config: config, + config: cfg, opts: new(options), } ca.opts.apply(opts) - return ca.Init(config) + return ca.Init(cfg) } // Init initializes the CA with the given configuration. -func (ca *CA) Init(config *config.Config) (*CA, error) { - // Intermediate Password. - if len(ca.opts.password) > 0 { - ca.config.Password = string(ca.opts.password) +func (ca *CA) Init(cfg *config.Config) (*CA, error) { + // Set password, it's ok to set nil password, the ca will prompt for them if + // they are required. + opts := []authority.Option{ + authority.WithPassword(ca.opts.password), + authority.WithSSHHostPassword(ca.opts.sshHostPassword), + authority.WithSSHUserPassword(ca.opts.sshUserPassword), + authority.WithIssuerPassword(ca.opts.issuerPassword), + } + if ca.opts.linkedCAToken != "" { + opts = append(opts, authority.WithLinkedCAToken(ca.opts.linkedCAToken)) } - // Certificate issuer password for RA mode. - if len(ca.opts.issuerPassword) > 0 { - if ca.config.AuthorityConfig != nil && ca.config.AuthorityConfig.CertificateIssuer != nil { - ca.config.AuthorityConfig.CertificateIssuer.Password = string(ca.opts.issuerPassword) - } - } - - var opts []authority.Option if ca.opts.database != nil { opts = append(opts, authority.WithDatabase(ca.opts.database)) } - auth, err := authority.New(config, opts...) + auth, err := authority.New(cfg, opts...) if err != nil { return nil, err } @@ -141,8 +166,8 @@ func (ca *CA) Init(config *config.Config) (*CA, error) { }) //Add ACME api endpoints in /acme and /1.0/acme - dns := config.DNSNames[0] - u, err := url.Parse("https://" + config.Address) + dns := cfg.DNSNames[0] + u, err := url.Parse("https://" + cfg.Address) if err != nil { return nil, err } @@ -154,7 +179,7 @@ func (ca *CA) Init(config *config.Config) (*CA, error) { // ACME Router prefix := "acme" var acmeDB acme.DB - if config.DB == nil { + if cfg.DB == nil { acmeDB = nil } else { acmeDB, err = acmeNoSQL.New(auth.GetDatabase().(nosql.DB)) @@ -163,7 +188,7 @@ func (ca *CA) Init(config *config.Config) (*CA, error) { } } acmeHandler := acmeAPI.NewHandler(acmeAPI.HandlerOptions{ - Backdate: *config.AuthorityConfig.Backdate, + Backdate: *cfg.AuthorityConfig.Backdate, DB: acmeDB, DNS: dns, Prefix: prefix, @@ -179,7 +204,7 @@ func (ca *CA) Init(config *config.Config) (*CA, error) { }) // Admin API Router - if config.AuthorityConfig.EnableAdmin { + if cfg.AuthorityConfig.EnableAdmin { adminDB := auth.GetAdminDatabase() if adminDB != nil { adminHandler := adminAPI.NewHandler(auth) @@ -223,8 +248,8 @@ func (ca *CA) Init(config *config.Config) (*CA, error) { //dumpRoutes(mux) // Add monitoring if configured - if len(config.Monitoring) > 0 { - m, err := monitoring.New(config.Monitoring) + if len(cfg.Monitoring) > 0 { + m, err := monitoring.New(cfg.Monitoring) if err != nil { return nil, err } @@ -233,8 +258,8 @@ func (ca *CA) Init(config *config.Config) (*CA, error) { } // Add logger if configured - if len(config.Logger) > 0 { - logger, err := logging.New("ca", config.Logger) + if len(cfg.Logger) > 0 { + logger, err := logging.New("ca", cfg.Logger) if err != nil { return nil, err } @@ -242,16 +267,16 @@ func (ca *CA) Init(config *config.Config) (*CA, error) { insecureHandler = logger.Middleware(insecureHandler) } - ca.srv = server.New(config.Address, handler, tlsConfig) + ca.srv = server.New(cfg.Address, handler, tlsConfig) // only start the insecure server if the insecure address is configured // and, currently, also only when it should serve SCEP endpoints. - if ca.shouldServeSCEPEndpoints() && config.InsecureAddress != "" { + if ca.shouldServeSCEPEndpoints() && cfg.InsecureAddress != "" { // TODO: instead opt for having a single server.Server but two // http.Servers handling the HTTP and HTTPS handler? The latter // will probably introduce more complexity in terms of graceful // reload. - ca.insecureSrv = server.New(config.InsecureAddress, insecureHandler, nil) + ca.insecureSrv = server.New(cfg.InsecureAddress, insecureHandler, nil) } return ca, nil @@ -260,24 +285,24 @@ func (ca *CA) Init(config *config.Config) (*CA, error) { // Run starts the CA calling to the server ListenAndServe method. func (ca *CA) Run() error { var wg sync.WaitGroup - errors := make(chan error, 1) + errs := make(chan error, 1) if ca.insecureSrv != nil { wg.Add(1) go func() { defer wg.Done() - errors <- ca.insecureSrv.ListenAndServe() + errs <- ca.insecureSrv.ListenAndServe() }() } wg.Add(1) go func() { defer wg.Done() - errors <- ca.srv.ListenAndServe() + errs <- ca.srv.ListenAndServe() }() // wait till error occurs; ensures the servers keep listening - err := <-errors + err := <-errs wg.Wait() @@ -306,7 +331,7 @@ func (ca *CA) Stop() error { // Reload reloads the configuration of the CA and calls to the server Reload // method. func (ca *CA) Reload() error { - config, err := config.LoadConfiguration(ca.opts.configFile) + cfg, err := config.LoadConfiguration(ca.opts.configFile) if err != nil { return errors.Wrap(err, "error reloading ca configuration") } @@ -318,14 +343,17 @@ func (ca *CA) Reload() error { } // Do not allow reload if the database configuration has changed. - if !reflect.DeepEqual(ca.config.DB, config.DB) { + if !reflect.DeepEqual(ca.config.DB, cfg.DB) { logContinue("Reload failed because the database configuration has changed.") return errors.New("error reloading ca: database configuration cannot change") } - newCA, err := New(config, + newCA, err := New(cfg, WithPassword(ca.opts.password), + WithSSHHostPassword(ca.opts.sshHostPassword), + WithSSHUserPassword(ca.opts.sshUserPassword), WithIssuerPassword(ca.opts.issuerPassword), + WithLinkedCAToken(ca.opts.linkedCAToken), WithConfigFile(ca.opts.configFile), WithDatabase(ca.auth.GetDatabase()), ) diff --git a/ca/ca_test.go b/ca/ca_test.go index 6e297733..ff264db7 100644 --- a/ca/ca_test.go +++ b/ca/ca_test.go @@ -322,7 +322,7 @@ ZEp7knvU2psWRw== assert.Equals(t, intermediate, realIntermediate) } else { err := readError(body) - if len(tc.errMsg) == 0 { + if tc.errMsg == "" { assert.FatalError(t, errors.New("must validate response error")) } assert.HasPrefix(t, err.Error(), tc.errMsg) @@ -375,7 +375,7 @@ func TestCAProvisioners(t *testing.T) { assert.Equals(t, a, b) } else { err := readError(body) - if len(tc.errMsg) == 0 { + if tc.errMsg == "" { assert.FatalError(t, errors.New("must validate response error")) } assert.HasPrefix(t, err.Error(), tc.errMsg) @@ -436,7 +436,7 @@ func TestCAProvisionerEncryptedKey(t *testing.T) { assert.Equals(t, ek.Key, tc.expectedKey) } else { err := readError(body) - if len(tc.errMsg) == 0 { + if tc.errMsg == "" { assert.FatalError(t, errors.New("must validate response error")) } assert.HasPrefix(t, err.Error(), tc.errMsg) @@ -497,7 +497,7 @@ func TestCARoot(t *testing.T) { assert.Equals(t, root.RootPEM.Certificate, rootCrt) } else { err := readError(body) - if len(tc.errMsg) == 0 { + if tc.errMsg == "" { assert.FatalError(t, errors.New("must validate response error")) } assert.HasPrefix(t, err.Error(), tc.errMsg) @@ -665,7 +665,7 @@ func TestCARenew(t *testing.T) { assert.Equals(t, *sign.TLSOptions, authority.DefaultTLSOptions) } else { err := readError(body) - if len(tc.errMsg) == 0 { + if tc.errMsg == "" { assert.FatalError(t, errors.New("must validate response error")) } assert.HasPrefix(t, err.Error(), tc.errMsg) diff --git a/ca/client.go b/ca/client.go index 8997fbd5..cfeddba0 100644 --- a/ca/client.go +++ b/ca/client.go @@ -74,17 +74,17 @@ func (c *uaClient) SetTransport(tr http.RoundTripper) { c.Client.Transport = tr } -func (c *uaClient) Get(url string) (*http.Response, error) { - req, err := http.NewRequest("GET", url, nil) +func (c *uaClient) Get(u string) (*http.Response, error) { + req, err := http.NewRequest("GET", u, nil) if err != nil { - return nil, errors.Wrapf(err, "new request GET %s failed", url) + return nil, errors.Wrapf(err, "new request GET %s failed", u) } req.Header.Set("User-Agent", UserAgent) return c.Client.Do(req) } -func (c *uaClient) Post(url, contentType string, body io.Reader) (*http.Response, error) { - req, err := http.NewRequest("POST", url, body) +func (c *uaClient) Post(u, contentType string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest("POST", u, body) if err != nil { return nil, err } @@ -305,7 +305,7 @@ func WithAdminX5C(certs []*x509.Certificate, key interface{}, passwordFile strin err error opts []jose.Option ) - if len(passwordFile) != 0 { + if passwordFile != "" { opts = append(opts, jose.WithPasswordFile(passwordFile)) } blk, err := pemutil.Serialize(key) @@ -326,14 +326,14 @@ func WithAdminX5C(certs []*x509.Certificate, key interface{}, passwordFile strin for _, e := range o.x5cCert.Extensions { if e.Id.Equal(stepOIDProvisioner) { - var provisioner stepProvisionerASN1 - if _, err := asn1.Unmarshal(e.Value, &provisioner); err != nil { + var prov stepProvisionerASN1 + if _, err := asn1.Unmarshal(e.Value, &prov); err != nil { return errors.Wrap(err, "error unmarshaling provisioner OID from certificate") } - o.x5cIssuer = string(provisioner.Name) + o.x5cIssuer = string(prov.Name) } } - if len(o.x5cIssuer) == 0 { + if o.x5cIssuer == "" { return errors.New("provisioner extension not found in certificate") } @@ -631,7 +631,7 @@ retry: // do not match. func (c *Client) Root(sha256Sum string) (*api.RootResponse, error) { var retried bool - sha256Sum = strings.ToLower(strings.Replace(sha256Sum, "-", "", -1)) + sha256Sum = strings.ToLower(strings.ReplaceAll(sha256Sum, "-", "")) u := c.endpoint.ResolveReference(&url.URL{Path: "/root/" + sha256Sum}) retry: resp, err := newInsecureClient().Get(u.String()) @@ -651,7 +651,7 @@ retry: } // verify the sha256 sum := sha256.Sum256(root.RootPEM.Raw) - if sha256Sum != strings.ToLower(hex.EncodeToString(sum[:])) { + if !strings.EqualFold(sha256Sum, strings.ToLower(hex.EncodeToString(sum[:]))) { return nil, errs.BadRequest("client.Root; root certificate SHA256 fingerprint do not match") } return &root, nil @@ -1066,16 +1066,16 @@ retry: } return nil, readError(resp.Body) } - var config api.SSHConfigResponse - if err := readJSON(resp.Body, &config); err != nil { + var cfg api.SSHConfigResponse + if err := readJSON(resp.Body, &cfg); err != nil { return nil, errors.Wrapf(err, "error reading %s", u) } - return &config, nil + return &cfg, nil } // SSHCheckHost performs the POST /ssh/check-host request to the CA with the // given principal. -func (c *Client) SSHCheckHost(principal string, token string) (*api.SSHCheckPrincipalResponse, error) { +func (c *Client) SSHCheckHost(principal, token string) (*api.SSHCheckPrincipalResponse, error) { var retried bool body, err := json.Marshal(&api.SSHCheckPrincipalRequest{ Type: provisioner.SSHHostCert, diff --git a/ca/client_test.go b/ca/client_test.go index 30669e6e..187066f0 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -135,7 +135,7 @@ func parseCertificateRequest(data string) *x509.CertificateRequest { return csr } -func equalJSON(t *testing.T, a interface{}, b interface{}) bool { +func equalJSON(t *testing.T, a, b interface{}) bool { if reflect.DeepEqual(a, b) { return true } diff --git a/ca/identity/client_test.go b/ca/identity/client_test.go index c792a6dc..402ec7b8 100644 --- a/ca/identity/client_test.go +++ b/ca/identity/client_test.go @@ -187,11 +187,12 @@ func TestLoadClient(t *testing.T) { } else { gotTransport := got.Client.Transport.(*http.Transport) wantTransport := tt.want.Client.Transport.(*http.Transport) - if gotTransport.TLSClientConfig.GetClientCertificate == nil { + switch { + case gotTransport.TLSClientConfig.GetClientCertificate == nil: t.Error("LoadClient() transport does not define GetClientCertificate") - } else if !reflect.DeepEqual(got.CaURL, tt.want.CaURL) || !reflect.DeepEqual(gotTransport.TLSClientConfig.RootCAs.Subjects(), wantTransport.TLSClientConfig.RootCAs.Subjects()) { + case !reflect.DeepEqual(got.CaURL, tt.want.CaURL) || !reflect.DeepEqual(gotTransport.TLSClientConfig.RootCAs.Subjects(), wantTransport.TLSClientConfig.RootCAs.Subjects()): t.Errorf("LoadClient() = %#v, want %#v", got, tt.want) - } else { + default: crt, err := gotTransport.TLSClientConfig.GetClientCertificate(nil) if err != nil { t.Errorf("LoadClient() GetClientCertificate error = %v", err) diff --git a/ca/testdata/ca.json b/ca/testdata/ca.json index 0a5149d9..d40325e8 100644 --- a/ca/testdata/ca.json +++ b/ca/testdata/ca.json @@ -9,12 +9,11 @@ "logger": {"format": "text"}, "tls": { "minVersion": 1.2, - "maxVersion": 1.2, + "maxVersion": 1.3, "renegotiation": false, "cipherSuites": [ - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" ] }, "authority": { diff --git a/ca/tls.go b/ca/tls.go index cb9f4707..0738d0e0 100644 --- a/ca/tls.go +++ b/ca/tls.go @@ -105,7 +105,7 @@ func (c *Client) getClientTLSConfig(ctx context.Context, sign *api.SignResponse, tr := getDefaultTransport(tlsConfig) // Use mutable tls.Config on renew - tr.DialTLS = c.buildDialTLS(tlsCtx) // nolint:staticcheck + tr.DialTLS = c.buildDialTLS(tlsCtx) // nolint:staticcheck,gocritic // tr.DialTLSContext = c.buildDialTLSContext(tlsCtx) renewer.RenewCertificate = getRenewFunc(tlsCtx, c, tr, pk) @@ -154,7 +154,7 @@ func (c *Client) GetServerTLSConfig(ctx context.Context, sign *api.SignResponse, // Update renew function with transport tr := getDefaultTransport(tlsConfig) // Use mutable tls.Config on renew - tr.DialTLS = c.buildDialTLS(tlsCtx) // nolint:staticcheck + tr.DialTLS = c.buildDialTLS(tlsCtx) // nolint:staticcheck,gocritic // tr.DialTLSContext = c.buildDialTLSContext(tlsCtx) renewer.RenewCertificate = getRenewFunc(tlsCtx, c, tr, pk) @@ -195,7 +195,7 @@ func (c *Client) buildDialTLS(ctx *TLSOptionCtx) func(network, addr string) (net } // buildDialTLSContext returns an implementation of DialTLSContext callback in http.Transport. -// nolint:unused +// nolint:unused,gocritic func (c *Client) buildDialTLSContext(tlsCtx *TLSOptionCtx) func(ctx context.Context, network, addr string) (net.Conn, error) { return func(ctx context.Context, network, addr string) (net.Conn, error) { d := getDefaultDialer() @@ -253,6 +253,8 @@ func TLSCertificate(sign *api.SignResponse, pk crypto.PrivateKey) (*tls.Certific return nil, err } + // nolint:gocritic + // using a new variable for clarity chain := append(certPEM, caPEM...) cert, err := tls.X509KeyPair(chain, keyPEM) if err != nil { @@ -277,9 +279,9 @@ func getDefaultTLSConfig(sign *api.SignResponse) *tls.Config { // getDefaultDialer returns a new dialer with the default configuration. func getDefaultDialer() *net.Dialer { + // With the KeepAlive parameter set to 0, it will be use Golang's default. return &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, + Timeout: 30 * time.Second, } } diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index 61cac9a2..badad7fc 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -38,10 +38,17 @@ type Options struct { CertificateChain []*x509.Certificate `json:"-"` Signer crypto.Signer `json:"-"` - // IsCreator is set to true when we're creating a certificate authority. Is - // used to skip some validations when initializing a CertificateAuthority. + // IsCreator is set to true when we're creating a certificate authority. It + // is used to skip some validations when initializing a + // CertificateAuthority. This option is used on SoftCAS and CloudCAS. IsCreator bool `json:"-"` + // IsCAGetter is set to true when we're just using the + // CertificateAuthorityGetter interface to retrieve the root certificate. It + // is used to skip some validations when initializing a + // CertificateAuthority. This option is used on StepCAS. + IsCAGetter bool `json:"-"` + // KeyManager is the KMS used to generate keys in SoftCAS. KeyManager kms.KeyManager `json:"-"` diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index b47a9c13..bf745c17 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -108,6 +108,9 @@ type GetCertificateAuthorityResponse struct { RootCertificate *x509.Certificate } +// CreateKeyRequest is the request used to generate a new key using a KMS. +type CreateKeyRequest = apiv1.CreateKeyRequest + // CreateCertificateAuthorityRequest is the request used to generate a root or // intermediate certificate. type CreateCertificateAuthorityRequest struct { @@ -126,7 +129,7 @@ type CreateCertificateAuthorityRequest struct { // CreateKey defines the KMS CreateKeyRequest to use when creating a new // CertificateAuthority. If CreateKey is nil, a default algorithm will be // used. - CreateKey *apiv1.CreateKeyRequest + CreateKey *CreateKeyRequest } // CreateCertificateAuthorityResponse is the response for @@ -136,6 +139,7 @@ type CreateCertificateAuthorityResponse struct { Name string Certificate *x509.Certificate CertificateChain []*x509.Certificate + KeyName string PublicKey crypto.PublicKey PrivateKey crypto.PrivateKey Signer crypto.Signer diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index d4dd3c8c..cf9a5470 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -1,6 +1,7 @@ package apiv1 import ( + "crypto/x509" "net/http" "strings" ) @@ -26,6 +27,12 @@ type CertificateAuthorityCreator interface { CreateCertificateAuthority(req *CreateCertificateAuthorityRequest) (*CreateCertificateAuthorityResponse, error) } +// SignatureAlgorithmGetter is an optional implementation in a crypto.Signer +// that returns the SignatureAlgorithm to use. +type SignatureAlgorithmGetter interface { + SignatureAlgorithm() x509.SignatureAlgorithm +} + // Type represents the CAS type used. type Type string diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index 2e9da260..e3e956a9 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -29,9 +29,7 @@ func init() { }) } -var now = func() time.Time { - return time.Now() -} +var now = time.Now // The actual regular expression that matches a certificate authority is: // ^projects/[a-z][a-z0-9-]{4,28}[a-z0-9]/locations/[a-z0-9-]+/caPools/[a-zA-Z0-9-_]+/certificateAuthorities/[a-zA-Z0-9-_]+$ diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go index 0561000c..7f996c15 100644 --- a/cas/cloudcas/cloudcas_test.go +++ b/cas/cloudcas/cloudcas_test.go @@ -12,7 +12,6 @@ import ( "encoding/pem" "fmt" "io" - "log" "net" "os" "reflect" @@ -103,7 +102,7 @@ MHcCAQEEIN51Rgg6YcQVLeCRzumdw4pjM3VWqFIdCbnsV3Up1e/goAoGCCqGSM49 AwEHoUQDQgAEjJIcDhvvxi7gu4aFkiW/8+E3BfPhmhXU5RlDQusre+MHXc7XYMtk Lm6PXPeTF1DNdS21Ju1G/j1yUykGJOmxkg== -----END EC PRIVATE KEY-----` - // nolint:unused,deadcode + // nolint:unused,deadcode,gocritic testIntermediateKey = `-----BEGIN EC PRIVATE KEY----- MHcCAQEEIMMX/XkXGnRDD4fYu7Z4rHACdJn/iyOy2UTwsv+oZ0C+oAoGCCqGSM49 AwEHoUQDQgAE8u6rGAFj5CZpdzzMogLwUyCMnp0X9wtv4OKDRcpzkYf9PU5GuGA6 @@ -190,7 +189,7 @@ func (b *badSigner) Public() crypto.PublicKey { return b.pub } -func (b *badSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +func (b *badSigner) Sign(rnd io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { return nil, fmt.Errorf("πŸ’₯") } @@ -730,7 +729,7 @@ func TestCloudCAS_RevokeCertificate(t *testing.T) { func Test_createCertificateID(t *testing.T) { buf := new(bytes.Buffer) setTeeReader(t, buf) - uuid, err := uuid.NewRandomFromReader(rand.Reader) + id, err := uuid.NewRandomFromReader(rand.Reader) if err != nil { t.Fatal(err) } @@ -741,7 +740,7 @@ func Test_createCertificateID(t *testing.T) { want string wantErr bool }{ - {"ok", uuid.String(), false}, + {"ok", id.String(), false}, {"fail", "", true}, } for _, tt := range tests { @@ -858,7 +857,7 @@ func TestCloudCAS_CreateCertificateAuthority(t *testing.T) { return lis.Dial() })) if err != nil { - log.Fatal(err) + t.Fatal(err) } client, err := lroauto.NewOperationsClient(context.Background(), option.WithGRPCConn(conn)) diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 21760490..8e67d016 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -19,9 +19,7 @@ func init() { }) } -var now = func() time.Time { - return time.Now() -} +var now = time.Now // SoftCAS implements a Certificate Authority Service using Golang or KMS // crypto. This is the default CAS used in step-ca. @@ -68,7 +66,7 @@ func (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 } req.Template.Issuer = c.CertificateChain[0].Subject - cert, err := x509util.CreateCertificate(req.Template, c.CertificateChain[0], req.Template.PublicKey, c.Signer) + cert, err := createCertificate(req.Template, c.CertificateChain[0], req.Template.PublicKey, c.Signer) if err != nil { return nil, err } @@ -93,7 +91,7 @@ func (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.R req.Template.NotAfter = t.Add(req.Lifetime) req.Template.Issuer = c.CertificateChain[0].Subject - cert, err := x509util.CreateCertificate(req.Template, c.CertificateChain[0], req.Template.PublicKey, c.Signer) + cert, err := createCertificate(req.Template, c.CertificateChain[0], req.Template.PublicKey, c.Signer) if err != nil { return nil, err } @@ -150,12 +148,12 @@ func (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthori var cert *x509.Certificate switch req.Type { case apiv1.RootCA: - cert, err = x509util.CreateCertificate(req.Template, req.Template, signer.Public(), signer) + cert, err = createCertificate(req.Template, req.Template, signer.Public(), signer) if err != nil { return nil, err } case apiv1.IntermediateCA: - cert, err = x509util.CreateCertificate(req.Template, req.Parent.Certificate, signer.Public(), req.Parent.Signer) + cert, err = createCertificate(req.Template, req.Parent.Certificate, signer.Public(), req.Parent.Signer) if err != nil { return nil, err } @@ -174,6 +172,7 @@ func (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthori Name: cert.Subject.CommonName, Certificate: cert, CertificateChain: chain, + KeyName: key.Name, PublicKey: key.PublicKey, PrivateKey: key.PrivateKey, Signer: signer, @@ -210,3 +209,16 @@ func (c *SoftCAS) createSigner(req *kmsapi.CreateSignerRequest) (crypto.Signer, } return c.KeyManager.CreateSigner(req) } + +// createCertificate sets the SignatureAlgorithm of the template if necessary +// and calls x509util.CreateCertificate. +func createCertificate(template, parent *x509.Certificate, pub crypto.PublicKey, signer crypto.Signer) (*x509.Certificate, error) { + // Signers can specify the signature algorithm. This is especially important + // when x509.CreateCertificate attempts to validate a RSAPSS signature. + if template.SignatureAlgorithm == 0 { + if sa, ok := signer.(apiv1.SignatureAlgorithmGetter); ok { + template.SignatureAlgorithm = sa.SignatureAlgorithm() + } + } + return x509util.CreateCertificate(template, parent, pub, signer) +} diff --git a/cas/softcas/softcas_test.go b/cas/softcas/softcas_test.go index 092a0337..7d3add4f 100644 --- a/cas/softcas/softcas_test.go +++ b/cas/softcas/softcas_test.go @@ -75,6 +75,15 @@ var ( testSignedIntermediateTemplate = mustSign(testIntermediateTemplate, testSignedRootTemplate, testNow, testNow.Add(24*time.Hour)) ) +type signatureAlgorithmSigner struct { + crypto.Signer + algorithm x509.SignatureAlgorithm +} + +func (s *signatureAlgorithmSigner) SignatureAlgorithm() x509.SignatureAlgorithm { + return s.algorithm +} + type mockKeyManager struct { signer crypto.Signer errGetPublicKey error @@ -97,6 +106,7 @@ func (m *mockKeyManager) CreateKey(req *kmsapi.CreateKeyRequest) (*kmsapi.Create signer = m.signer } return &kmsapi.CreateKeyResponse{ + Name: req.Name, PrivateKey: signer, PublicKey: signer.Public(), }, m.errCreateKey @@ -124,7 +134,7 @@ func (b *badSigner) Public() crypto.PublicKey { return testSigner.Public() } -func (b *badSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +func (b *badSigner) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) { return nil, fmt.Errorf("πŸ’₯") } @@ -247,6 +257,13 @@ func TestSoftCAS_CreateCertificate(t *testing.T) { tmplNoSerial := *testTemplate tmplNoSerial.SerialNumber = nil + saTemplate := *testSignedTemplate + saTemplate.SignatureAlgorithm = 0 + saSigner := &signatureAlgorithmSigner{ + Signer: testSigner, + algorithm: x509.PureEd25519, + } + type fields struct { Issuer *x509.Certificate Signer crypto.Signer @@ -267,6 +284,12 @@ func TestSoftCAS_CreateCertificate(t *testing.T) { Certificate: testSignedTemplate, CertificateChain: []*x509.Certificate{testIssuer}, }, false}, + {"ok signature algorithm", fields{testIssuer, saSigner}, args{&apiv1.CreateCertificateRequest{ + Template: &saTemplate, Lifetime: 24 * time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: testSignedTemplate, + CertificateChain: []*x509.Certificate{testIssuer}, + }, false}, {"ok with notBefore", fields{testIssuer, testSigner}, args{&apiv1.CreateCertificateRequest{ Template: &tmplNotBefore, Lifetime: 24 * time.Hour, }}, &apiv1.CreateCertificateResponse{ @@ -316,6 +339,11 @@ func TestSoftCAS_RenewCertificate(t *testing.T) { tmplNoSerial := *testTemplate tmplNoSerial.SerialNumber = nil + saSigner := &signatureAlgorithmSigner{ + Signer: testSigner, + algorithm: x509.PureEd25519, + } + type fields struct { Issuer *x509.Certificate Signer crypto.Signer @@ -336,6 +364,12 @@ func TestSoftCAS_RenewCertificate(t *testing.T) { Certificate: testSignedTemplate, CertificateChain: []*x509.Certificate{testIssuer}, }, false}, + {"ok signature algorithm", fields{testIssuer, saSigner}, args{&apiv1.RenewCertificateRequest{ + Template: testTemplate, Lifetime: 24 * time.Hour, + }}, &apiv1.RenewCertificateResponse{ + Certificate: testSignedTemplate, + CertificateChain: []*x509.Certificate{testIssuer}, + }, false}, {"fail template", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true}, {"fail lifetime", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{Template: testTemplate}}, nil, true}, {"fail CreateCertificate", fields{testIssuer, testSigner}, args{&apiv1.RenewCertificateRequest{ @@ -425,6 +459,11 @@ func Test_now(t *testing.T) { func TestSoftCAS_CreateCertificateAuthority(t *testing.T) { mockNow(t) + saSigner := &signatureAlgorithmSigner{ + Signer: testSigner, + algorithm: x509.PureEd25519, + } + type fields struct { Issuer *x509.Certificate Signer crypto.Signer @@ -467,6 +506,33 @@ func TestSoftCAS_CreateCertificateAuthority(t *testing.T) { PrivateKey: testSigner, Signer: testSigner, }, false}, + {"ok signature algorithm", fields{nil, nil, &mockKeyManager{signer: saSigner}}, args{&apiv1.CreateCertificateAuthorityRequest{ + Type: apiv1.RootCA, + Template: testRootTemplate, + Lifetime: 24 * time.Hour, + }}, &apiv1.CreateCertificateAuthorityResponse{ + Name: "Test Root CA", + Certificate: testSignedRootTemplate, + PublicKey: testSignedRootTemplate.PublicKey, + PrivateKey: saSigner, + Signer: saSigner, + }, false}, + {"ok createKey", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{ + Type: apiv1.RootCA, + Template: testRootTemplate, + Lifetime: 24 * time.Hour, + CreateKey: &kmsapi.CreateKeyRequest{ + Name: "root_ca.crt", + SignatureAlgorithm: kmsapi.ECDSAWithSHA256, + }, + }}, &apiv1.CreateCertificateAuthorityResponse{ + Name: "Test Root CA", + Certificate: testSignedRootTemplate, + PublicKey: testSignedRootTemplate.PublicKey, + KeyName: "root_ca.crt", + PrivateKey: testSigner, + Signer: testSigner, + }, false}, {"fail template", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{ Type: apiv1.RootCA, Lifetime: 24 * time.Hour, diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index 49a99963..9fcbd36c 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -47,10 +47,13 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) { return nil, err } - // Create configured issuer - iss, err := newStepIssuer(caURL, client, opts.CertificateIssuer) - if err != nil { - return nil, err + var iss stepIssuer + // Create configured issuer unless we only want to use GetCertificateAuthority. + // This avoid the request for the password if not provided. + if !opts.IsCAGetter { + if iss, err = newStepIssuer(caURL, client, opts.CertificateIssuer); err != nil { + return nil, err + } } return &StepCAS{ @@ -87,9 +90,9 @@ func (s *StepCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.R return nil, apiv1.ErrNotImplemented{Message: "stepCAS does not support mTLS renewals"} } +// RevokeCertificate revokes a certificate. func (s *StepCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { - switch { - case req.SerialNumber == "" && req.Certificate == nil: + if req.SerialNumber == "" && req.Certificate == nil { return nil, errors.New("revokeCertificateRequest `serialNumber` or `certificate` are required") } diff --git a/cas/stepcas/stepcas_test.go b/cas/stepcas/stepcas_test.go index fb8259f5..f430a1dd 100644 --- a/cas/stepcas/stepcas_test.go +++ b/cas/stepcas/stepcas_test.go @@ -411,6 +411,19 @@ func TestNew(t *testing.T) { client: client, fingerprint: testRootFingerprint, }, false}, + {"ok ca getter", args{context.TODO(), apiv1.Options{ + IsCAGetter: true, + CertificateAuthority: caURL.String(), + CertificateAuthorityFingerprint: testRootFingerprint, + CertificateIssuer: &apiv1.CertificateIssuer{ + Type: "jwk", + Provisioner: "ra@doe.org", + }, + }}, &StepCAS{ + iss: nil, + client: client, + fingerprint: testRootFingerprint, + }, false}, {"fail authority", args{context.TODO(), apiv1.Options{ CertificateAuthority: "", CertificateAuthorityFingerprint: testRootFingerprint, diff --git a/cas/stepcas/x5c_issuer.go b/cas/stepcas/x5c_issuer.go index 636d22f9..76ed9c3c 100644 --- a/cas/stepcas/x5c_issuer.go +++ b/cas/stepcas/x5c_issuer.go @@ -19,9 +19,7 @@ const defaultValidity = 5 * time.Minute // timeNow returns the current time. // This method is used for unit testing purposes. -var timeNow = func() time.Time { - return time.Now() -} +var timeNow = time.Now type x5cIssuer struct { caURL *url.URL diff --git a/cas/stepcas/x5c_issuer_test.go b/cas/stepcas/x5c_issuer_test.go index a3190255..b1bc653d 100644 --- a/cas/stepcas/x5c_issuer_test.go +++ b/cas/stepcas/x5c_issuer_test.go @@ -22,7 +22,7 @@ func (b noneSigner) Public() crypto.PublicKey { return []byte(b) } -func (b noneSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { +func (b noneSigner) Sign(rnd io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { return digest, nil } diff --git a/cmd/step-awskms-init/main.go b/cmd/step-awskms-init/main.go index 0d686239..8e30745f 100644 --- a/cmd/step-awskms-init/main.go +++ b/cmd/step-awskms-init/main.go @@ -24,13 +24,16 @@ import ( func main() { var credentialsFile, region string - var ssh bool + var enableSSH bool flag.StringVar(&credentialsFile, "credentials-file", "", "Path to the `file` containing the AWS KMS credentials.") flag.StringVar(®ion, "region", "", "AWS KMS region name.") - flag.BoolVar(&ssh, "ssh", false, "Create SSH keys.") + flag.BoolVar(&enableSSH, "ssh", false, "Create SSH keys.") flag.Usage = usage flag.Parse() + // Initialize windows terminal + ui.Init() + c, err := awskms.New(context.Background(), apiv1.Options{ Type: string(apiv1.AmazonKMS), Region: region, @@ -44,16 +47,20 @@ func main() { fatal(err) } - if ssh { + if enableSSH { ui.Println() if err := createSSH(c); err != nil { fatal(err) } } + + // Reset windows terminal + ui.Reset() } func fatal(err error) { fmt.Fprintln(os.Stderr, err) + ui.Reset() os.Exit(1) } @@ -113,7 +120,7 @@ func createX509(c *awskms.KMS) error { return err } - if err = fileutil.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{ + if err := fileutil.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: b, }), 0600); err != nil { @@ -156,7 +163,7 @@ func createX509(c *awskms.KMS) error { return err } - if err = fileutil.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ + if err := fileutil.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: b, }), 0600); err != nil { @@ -186,7 +193,7 @@ func createSSH(c *awskms.KMS) error { return err } - if err = fileutil.WriteFile("ssh_user_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { + if err := fileutil.WriteFile("ssh_user_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { return err } @@ -207,7 +214,7 @@ func createSSH(c *awskms.KMS) error { return err } - if err = fileutil.WriteFile("ssh_host_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { + if err := fileutil.WriteFile("ssh_host_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { return err } diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index 4396e028..01d800d8 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -22,10 +22,12 @@ import ( "go.step.sm/cli-utils/command" "go.step.sm/cli-utils/command/version" "go.step.sm/cli-utils/config" + "go.step.sm/cli-utils/ui" "go.step.sm/cli-utils/usage" // Enabled kms interfaces. _ "github.com/smallstep/certificates/kms/awskms" + _ "github.com/smallstep/certificates/kms/azurekms" _ "github.com/smallstep/certificates/kms/cloudkms" _ "github.com/smallstep/certificates/kms/softkms" _ "github.com/smallstep/certificates/kms/sshagentkms" @@ -52,6 +54,11 @@ func init() { rand.Seed(time.Now().UnixNano()) } +func exit(code int) { + ui.Reset() + os.Exit(code) +} + // appHelpTemplate contains the modified template for the main app var appHelpTemplate = `## NAME **{{.HelpName}}** -- {{.Usage}} @@ -90,6 +97,9 @@ Please send us a sentence or two, good or bad: **feedback@smallstep.com** or htt ` func main() { + // Initialize windows terminal + ui.Init() + // Override global framework components cli.VersionPrinter = func(c *cli.Context) { version.Command(c) @@ -107,7 +117,9 @@ func main() { app.HelpName = "step-ca" app.Version = config.Version() app.Usage = "an online certificate authority for secure automated certificate management" - app.UsageText = `**step-ca** [**--password-file**=] [**--issuer-password-file**=] [**--resolver**=] [**--help**] [**--version**]` + app.UsageText = `**step-ca** [**--password-file**=] +[**--ssh-host-password-file**=] [**--ssh-user-password-file**=] +[**--issuer-password-file**=] [**--resolver**=] [**--help**] [**--version**]` app.Description = `**step-ca** runs the Step Online Certificate Authority (Step CA) using the given configuration. See the README.md for more detailed configuration documentation. @@ -162,8 +174,10 @@ $ step-ca $STEPPATH/config/ca.json --password-file ./password.txt } else { fmt.Fprintln(os.Stderr, err) } - os.Exit(1) + exit(1) } + + exit(0) } func flagValue(f cli.Flag) reflect.Value { @@ -178,8 +192,8 @@ var placeholderString = regexp.MustCompile(`<.*?>`) func stringifyFlag(f cli.Flag) string { fv := flagValue(f) - usage := fv.FieldByName("Usage").String() - placeholder := placeholderString.FindString(usage) + usg := fv.FieldByName("Usage").String() + placeholder := placeholderString.FindString(usg) if placeholder == "" { switch f.(type) { case cli.BoolFlag, cli.BoolTFlag: @@ -187,5 +201,5 @@ func stringifyFlag(f cli.Flag) string { placeholder = "" } } - return cli.FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder) + "\t" + usage + return cli.FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder) + "\t" + usg } diff --git a/cmd/step-cloudkms-init/main.go b/cmd/step-cloudkms-init/main.go index 69573c5d..27dc82ad 100644 --- a/cmd/step-cloudkms-init/main.go +++ b/cmd/step-cloudkms-init/main.go @@ -27,13 +27,13 @@ func main() { var credentialsFile string var project, location, ring string var protectionLevelName string - var ssh bool + var enableSSH bool flag.StringVar(&credentialsFile, "credentials-file", "", "Path to the `file` containing the Google's Cloud KMS credentials.") flag.StringVar(&project, "project", "", "Google Cloud Project ID.") flag.StringVar(&location, "location", "global", "Cloud KMS location name.") flag.StringVar(&ring, "ring", "pki", "Cloud KMS ring name.") flag.StringVar(&protectionLevelName, "protection-level", "SOFTWARE", "Protection level to use, SOFTWARE or HSM.") - flag.BoolVar(&ssh, "ssh", false, "Create SSH keys.") + flag.BoolVar(&enableSSH, "ssh", false, "Create SSH keys.") flag.Usage = usage flag.Parse() @@ -62,6 +62,9 @@ func main() { os.Exit(1) } + // Initialize windows terminal + ui.Init() + c, err := cloudkms.New(context.Background(), apiv1.Options{ Type: string(apiv1.CloudKMS), CredentialsFile: credentialsFile, @@ -74,16 +77,20 @@ func main() { fatal(err) } - if ssh { + if enableSSH { ui.Println() if err := createSSH(c, project, location, ring, protectionLevel); err != nil { fatal(err) } } + + // Reset windows terminal + ui.Reset() } func fatal(err error) { fmt.Fprintln(os.Stderr, err) + ui.Reset() os.Exit(1) } @@ -146,7 +153,7 @@ func createPKI(c *cloudkms.CloudKMS, project, location, keyRing string, protecti return err } - if err = fileutil.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{ + if err := fileutil.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: b, }), 0600); err != nil { @@ -190,7 +197,7 @@ func createPKI(c *cloudkms.CloudKMS, project, location, keyRing string, protecti return err } - if err = fileutil.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ + if err := fileutil.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: b, }), 0600); err != nil { @@ -223,7 +230,7 @@ func createSSH(c *cloudkms.CloudKMS, project, location, keyRing string, protecti return err } - if err = fileutil.WriteFile("ssh_user_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { + if err := fileutil.WriteFile("ssh_user_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { return err } @@ -245,7 +252,7 @@ func createSSH(c *cloudkms.CloudKMS, project, location, keyRing string, protecti return err } - if err = fileutil.WriteFile("ssh_host_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { + if err := fileutil.WriteFile("ssh_host_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil { return err } diff --git a/cmd/step-pkcs11-init/main.go b/cmd/step-pkcs11-init/main.go index 34f9f8f8..0db1e4d9 100644 --- a/cmd/step-pkcs11-init/main.go +++ b/cmd/step-pkcs11-init/main.go @@ -50,6 +50,7 @@ type Config struct { NoCerts bool EnableSSH bool Force bool + Extractable bool } // Validate checks the flags in the config. @@ -117,6 +118,7 @@ func main() { flag.BoolVar(&c.EnableSSH, "ssh", false, "Enable the creation of ssh keys.") flag.BoolVar(&c.NoCerts, "no-certs", false, "Do not store certificates in the module.") flag.BoolVar(&c.Force, "force", false, "Force the delete of previous keys.") + flag.BoolVar(&c.Extractable, "extractable", false, "Allow export of private keys under wrap.") flag.Usage = usage flag.Parse() @@ -129,6 +131,9 @@ func main() { fatal(err) } + // Initialize windows terminal + ui.Init() + if u.Get("pin-value") == "" && u.Get("pin-source") == "" && c.Pin == "" { pin, err := ui.PromptPassword("What is the PKCS#11 PIN?") if err != nil { @@ -201,6 +206,9 @@ func main() { if err := createPKI(k, c); err != nil { fatalClose(err, k) } + + // Reset windows terminal + ui.Reset() } func fatal(err error) { @@ -209,6 +217,7 @@ func fatal(err error) { } else { fmt.Fprintln(os.Stderr, err) } + ui.Reset() os.Exit(1) } @@ -286,6 +295,7 @@ func createPKI(k kms.KeyManager, c Config) error { resp, err := k.CreateKey(&apiv1.CreateKeyRequest{ Name: c.RootKeyObject, SignatureAlgorithm: apiv1.ECDSAWithSHA256, + Extractable: c.Extractable, }) if err != nil { return err @@ -322,15 +332,16 @@ func createPKI(k kms.KeyManager, c Config) error { } if cm, ok := k.(kms.CertificateManager); ok && !c.NoCerts { - if err = cm.StoreCertificate(&apiv1.StoreCertificateRequest{ + if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{ Name: c.RootObject, Certificate: root, + Extractable: c.Extractable, }); err != nil { return err } } - if err = fileutil.WriteFile(c.RootPath, pem.EncodeToMemory(&pem.Block{ + if err := fileutil.WriteFile(c.RootPath, pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: b, }), 0600); err != nil { @@ -366,6 +377,7 @@ func createPKI(k kms.KeyManager, c Config) error { resp, err := k.CreateKey(&apiv1.CreateKeyRequest{ Name: c.CrtKeyObject, SignatureAlgorithm: apiv1.ECDSAWithSHA256, + Extractable: c.Extractable, }) if err != nil { return err @@ -399,15 +411,16 @@ func createPKI(k kms.KeyManager, c Config) error { } if cm, ok := k.(kms.CertificateManager); ok && !c.NoCerts { - if err = cm.StoreCertificate(&apiv1.StoreCertificateRequest{ + if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{ Name: c.CrtObject, Certificate: intermediate, + Extractable: c.Extractable, }); err != nil { return err } } - if err = fileutil.WriteFile(c.CrtPath, pem.EncodeToMemory(&pem.Block{ + if err := fileutil.WriteFile(c.CrtPath, pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: b, }), 0600); err != nil { diff --git a/cmd/step-yubikey-init/main.go b/cmd/step-yubikey-init/main.go index df7b9ea8..8b0ffab5 100644 --- a/cmd/step-yubikey-init/main.go +++ b/cmd/step-yubikey-init/main.go @@ -87,6 +87,9 @@ func main() { fatal(err) } + // Initialize windows terminal + ui.Init() + pin, err := ui.PromptPassword("What is the YubiKey PIN?") if err != nil { fatal(err) @@ -119,6 +122,9 @@ func main() { defer func() { _ = k.Close() }() + + // Reset windows terminal + ui.Reset() } func fatal(err error) { @@ -127,6 +133,7 @@ func fatal(err error) { } else { fmt.Fprintln(os.Stderr, err) } + ui.Reset() os.Exit(1) } @@ -221,7 +228,7 @@ func createPKI(k kms.KeyManager, c Config) error { } if cm, ok := k.(kms.CertificateManager); ok { - if err = cm.StoreCertificate(&apiv1.StoreCertificateRequest{ + if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{ Name: c.RootSlot, Certificate: root, }); err != nil { @@ -229,7 +236,7 @@ func createPKI(k kms.KeyManager, c Config) error { } } - if err = fileutil.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{ + if err := fileutil.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: b, }), 0600); err != nil { @@ -298,7 +305,7 @@ func createPKI(k kms.KeyManager, c Config) error { } if cm, ok := k.(kms.CertificateManager); ok { - if err = cm.StoreCertificate(&apiv1.StoreCertificateRequest{ + if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{ Name: c.CrtSlot, Certificate: intermediate, }); err != nil { @@ -306,7 +313,7 @@ func createPKI(k kms.KeyManager, c Config) error { } } - if err = fileutil.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ + if err := fileutil.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: b, }), 0600); err != nil { diff --git a/commands/app.go b/commands/app.go index 8833726c..84232a6c 100644 --- a/commands/app.go +++ b/commands/app.go @@ -8,11 +8,13 @@ import ( "net" "net/http" "os" + "strings" "unicode" "github.com/pkg/errors" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/ca" + "github.com/smallstep/certificates/pki" "github.com/urfave/cli" "go.step.sm/cli-utils/errs" ) @@ -21,13 +23,26 @@ import ( var AppCommand = cli.Command{ Name: "start", Action: appAction, - UsageText: `**step-ca** -[**--password-file**=] [**--issuer-password-file**=] [**--resolver**=]`, + UsageText: `**step-ca** [**--password-file**=] +[**--ssh-host-password-file**=] [**--ssh-user-password-file**=] +[**--issuer-password-file**=] [**--resolver**=]`, Flags: []cli.Flag{ cli.StringFlag{ Name: "password-file", Usage: `path to the containing the password to decrypt the intermediate private key.`, + }, + cli.StringFlag{ + Name: "ssh-host-password-file", + Usage: `path to the containing the password to decrypt the +private key used to sign SSH host certificates. If the flag is not passed it +will default to --password-file.`, + }, + cli.StringFlag{ + Name: "ssh-user-password-file", + Usage: `path to the containing the password to decrypt the +private key used to sign SSH user certificates. If the flag is not passed it +will default to --password-file.`, }, cli.StringFlag{ Name: "issuer-password-file", @@ -38,14 +53,22 @@ certificate issuer private key used in the RA mode.`, Name: "resolver", Usage: "address of a DNS resolver to be used instead of the default.", }, + cli.StringFlag{ + Name: "token", + Usage: "token used to enable the linked ca.", + EnvVar: "STEP_CA_TOKEN", + }, }, } // AppAction is the action used when the top command runs. func appAction(ctx *cli.Context) error { passFile := ctx.String("password-file") + sshHostPassFile := ctx.String("ssh-host-password-file") + sshUserPassFile := ctx.String("ssh-user-password-file") issuerPassFile := ctx.String("issuer-password-file") resolver := ctx.String("resolver") + token := ctx.String("token") // If zero cmd line args show help, if >1 cmd line args show error. if ctx.NArg() == 0 { @@ -56,11 +79,23 @@ func appAction(ctx *cli.Context) error { } configFile := ctx.Args().Get(0) - config, err := config.LoadConfiguration(configFile) + cfg, err := config.LoadConfiguration(configFile) if err != nil { fatal(err) } + if cfg.AuthorityConfig != nil { + if token == "" && strings.EqualFold(cfg.AuthorityConfig.DeploymentType, pki.LinkedDeployment.String()) { + return errors.New(`'step-ca' requires the '--token' flag for linked deploy type. + +To get a linked authority token: + 1. Log in or create a Certificate Manager account at ` + "\033[1mhttps://u.step.sm/linked\033[0m" + ` + 2. Add a new authority and select "Link a step-ca instance" + 3. Follow instructions in browser to start 'step-ca' using the '--token' flag +`) + } + } + var password []byte if passFile != "" { if password, err = ioutil.ReadFile(passFile); err != nil { @@ -69,6 +104,22 @@ func appAction(ctx *cli.Context) error { password = bytes.TrimRightFunc(password, unicode.IsSpace) } + var sshHostPassword []byte + if sshHostPassFile != "" { + if sshHostPassword, err = ioutil.ReadFile(sshHostPassFile); err != nil { + fatal(errors.Wrapf(err, "error reading %s", sshHostPassFile)) + } + sshHostPassword = bytes.TrimRightFunc(sshHostPassword, unicode.IsSpace) + } + + var sshUserPassword []byte + if sshUserPassFile != "" { + if sshUserPassword, err = ioutil.ReadFile(sshUserPassFile); err != nil { + fatal(errors.Wrapf(err, "error reading %s", sshUserPassFile)) + } + sshUserPassword = bytes.TrimRightFunc(sshUserPassword, unicode.IsSpace) + } + var issuerPassword []byte if issuerPassFile != "" { if issuerPassword, err = ioutil.ReadFile(issuerPassFile); err != nil { @@ -85,10 +136,13 @@ func appAction(ctx *cli.Context) error { } } - srv, err := ca.New(config, + srv, err := ca.New(cfg, ca.WithConfigFile(configFile), ca.WithPassword(password), - ca.WithIssuerPassword(issuerPassword)) + ca.WithSSHHostPassword(sshHostPassword), + ca.WithSSHUserPassword(sshUserPassword), + ca.WithIssuerPassword(issuerPassword), + ca.WithLinkedCAToken(token)) if err != nil { fatal(err) } diff --git a/commands/export.go b/commands/export.go new file mode 100644 index 00000000..5586f576 --- /dev/null +++ b/commands/export.go @@ -0,0 +1,113 @@ +package commands + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "unicode" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/config" + "github.com/urfave/cli" + "google.golang.org/protobuf/encoding/protojson" + + "go.step.sm/cli-utils/command" + "go.step.sm/cli-utils/errs" +) + +func init() { + command.Register(cli.Command{ + Name: "export", + Usage: "export the current configuration of step-ca", + UsageText: "**step-ca export** ", + Action: exportAction, + Description: `**step-ca export** exports the current configuration of step-ca. + +Note that neither the PKI password nor the certificate issuer password will be +included in the export file. + +## POSITIONAL ARGUMENTS + + +: The ca.json that contains the step-ca configuration. + +## EXAMPLES + +Export the current configuration: +''' +$ step-ca export $(step path)/config/ca.json +'''`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "password-file", + Usage: `path to the containing the password to decrypt the +intermediate private key.`, + }, + cli.StringFlag{ + Name: "issuer-password-file", + Usage: `path to the containing the password to decrypt the +certificate issuer private key used in the RA mode.`, + }, + }, + }) +} + +func exportAction(ctx *cli.Context) error { + if err := errs.NumberOfArguments(ctx, 1); err != nil { + return err + } + + configFile := ctx.Args().Get(0) + passwordFile := ctx.String("password-file") + issuerPasswordFile := ctx.String("issuer-password-file") + + cfg, err := config.LoadConfiguration(configFile) + if err != nil { + return err + } + if err := cfg.Validate(); err != nil { + return err + } + + if passwordFile != "" { + b, err := ioutil.ReadFile(passwordFile) + if err != nil { + return errors.Wrapf(err, "error reading %s", passwordFile) + } + cfg.Password = string(bytes.TrimRightFunc(b, unicode.IsSpace)) + } + if issuerPasswordFile != "" { + b, err := ioutil.ReadFile(issuerPasswordFile) + if err != nil { + return errors.Wrapf(err, "error reading %s", issuerPasswordFile) + } + if cfg.AuthorityConfig.CertificateIssuer != nil { + cfg.AuthorityConfig.CertificateIssuer.Password = string(bytes.TrimRightFunc(b, unicode.IsSpace)) + } + } + + auth, err := authority.New(cfg) + if err != nil { + return err + } + + export, err := auth.Export() + if err != nil { + return err + } + + b, err := protojson.Marshal(export) + if err != nil { + return errors.Wrap(err, "error marshaling export") + } + + var buf bytes.Buffer + if err := json.Indent(&buf, b, "", "\t"); err != nil { + return errors.Wrap(err, "error indenting export") + } + + fmt.Println(buf.String()) + return nil +} diff --git a/commands/onboard.go b/commands/onboard.go index 251a4b47..ebd468f5 100644 --- a/commands/onboard.go +++ b/commands/onboard.go @@ -103,8 +103,8 @@ func onboardAction(ctx *cli.Context) error { return errors.Wrap(msg, "error receiving onboarding guide") } - var config onboardingConfiguration - if err := readJSON(res.Body, &config); err != nil { + var cfg onboardingConfiguration + if err := readJSON(res.Body, &cfg); err != nil { return errors.Wrap(err, "error unmarshaling response") } @@ -112,16 +112,16 @@ func onboardAction(ctx *cli.Context) error { if err != nil { return err } - config.password = []byte(password) + cfg.password = []byte(password) ui.Println("Initializing step-ca with the following configuration:") - ui.PrintSelected("Name", config.Name) - ui.PrintSelected("DNS", config.DNS) - ui.PrintSelected("Address", config.Address) + ui.PrintSelected("Name", cfg.Name) + ui.PrintSelected("DNS", cfg.DNS) + ui.PrintSelected("Address", cfg.Address) ui.PrintSelected("Password", password) ui.Println() - caConfig, fp, err := onboardPKI(config) + caConfig, fp, err := onboardPKI(cfg) if err != nil { return err } @@ -149,47 +149,55 @@ func onboardAction(ctx *cli.Context) error { ui.Println("Initialized!") ui.Println("Step CA is starting. Please return to the onboarding guide in your browser to continue.") - srv, err := ca.New(caConfig, ca.WithPassword(config.password)) + srv, err := ca.New(caConfig, ca.WithPassword(cfg.password)) if err != nil { fatal(err) } go ca.StopReloaderHandler(srv) - if err = srv.Run(); err != nil && err != http.ErrServerClosed { + if err := srv.Run(); err != nil && err != http.ErrServerClosed { fatal(err) } return nil } -func onboardPKI(config onboardingConfiguration) (*config.Config, string, error) { +func onboardPKI(cfg onboardingConfiguration) (*config.Config, string, error) { + var opts = []pki.Option{ + pki.WithAddress(cfg.Address), + pki.WithDNSNames([]string{cfg.DNS}), + pki.WithProvisioner("admin"), + } + p, err := pki.New(apiv1.Options{ Type: apiv1.SoftCAS, IsCreator: true, - }) + }, opts...) if err != nil { return nil, "", err } - p.SetAddress(config.Address) - p.SetDNSNames([]string{config.DNS}) - + // Generate pki ui.Println("Generating root certificate...") - root, err := p.GenerateRootCertificate(config.Name, config.Name, config.Name, config.password) + root, err := p.GenerateRootCertificate(cfg.Name, cfg.Name, cfg.Name, cfg.password) if err != nil { return nil, "", err } ui.Println("Generating intermediate certificate...") - err = p.GenerateIntermediateCertificate(config.Name, config.Name, config.Name, root, config.password) + err = p.GenerateIntermediateCertificate(cfg.Name, cfg.Name, cfg.Name, root, cfg.password) if err != nil { return nil, "", err } + // Write files to disk + if err := p.WriteFiles(); err != nil { + return nil, "", err + } + // Generate provisioner - p.SetProvisioner("admin") ui.Println("Generating admin provisioner...") - if err = p.GenerateKeyPairs(config.password); err != nil { + if err := p.GenerateKeyPairs(cfg.password); err != nil { return nil, "", err } @@ -203,7 +211,7 @@ func onboardPKI(config onboardingConfiguration) (*config.Config, string, error) if err != nil { return nil, "", errors.Wrapf(err, "error marshaling %s", p.GetCAConfigPath()) } - if err = fileutil.WriteFile(p.GetCAConfigPath(), b, 0666); err != nil { + if err := fileutil.WriteFile(p.GetCAConfigPath(), b, 0666); err != nil { return nil, "", errs.FileError(err, p.GetCAConfigPath()) } diff --git a/cosign.pub b/cosign.pub new file mode 100644 index 00000000..9a0b42be --- /dev/null +++ b/cosign.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs+6THbAiXx4bja5ARQFNZmPwZjlD +GRvt5H+9ZFDhrcFPR1E7eB2rt1B/DhobANdHGKjvEBZEf0v4X/7S+SHrIw== +-----END PUBLIC KEY----- diff --git a/db/db_test.go b/db/db_test.go index 7efc623e..40f59215 100644 --- a/db/db_test.go +++ b/db/db_test.go @@ -144,15 +144,15 @@ func TestUseToken(t *testing.T) { } for name, tc := range tests { t.Run(name, func(t *testing.T) { - ok, err := tc.db.UseToken(tc.id, tc.tok) - if err != nil { + switch ok, err := tc.db.UseToken(tc.id, tc.tok); { + case err != nil: if assert.NotNil(t, tc.want.err) { assert.HasPrefix(t, err.Error(), tc.want.err.Error()) } assert.False(t, ok) - } else if ok { + case ok: assert.True(t, tc.want.ok) - } else { + default: assert.False(t, tc.want.ok) } }) diff --git a/docker/Dockerfile.step-ca b/docker/Dockerfile.step-ca index 4a1908d6..9363b6ae 100644 --- a/docker/Dockerfile.step-ca +++ b/docker/Dockerfile.step-ca @@ -24,4 +24,7 @@ VOLUME ["/home/step"] STOPSIGNAL SIGTERM HEALTHCHECK CMD step ca health 2>/dev/null | grep "^ok" >/dev/null +COPY docker/entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] CMD exec /usr/local/bin/step-ca --password-file $PWDPATH $CONFIGPATH diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..1f48c028 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -eo pipefail + +# Paraphrased from: +# https://github.com/influxdata/influxdata-docker/blob/0d341f18067c4652dfa8df7dcb24d69bf707363d/influxdb/2.0/entrypoint.sh +# (a repo with no LICENSE.md) + +export STEPPATH=$(step path) + +# List of env vars required for step ca init +declare -ra REQUIRED_INIT_VARS=(DOCKER_STEPCA_INIT_NAME DOCKER_STEPCA_INIT_DNS_NAMES) + +# Ensure all env vars required to run step ca init are set. +function init_if_possible () { + local missing_vars=0 + for var in "${REQUIRED_INIT_VARS[@]}"; do + if [ -z "${!var}" ]; then + missing_vars=1 + fi + done + if [ ${missing_vars} = 1 ]; then + >&2 echo "there is no ca.json config file; please run step ca init, or provide config parameters via DOCKER_STEPCA_INIT_ vars" + else + step_ca_init "${@}" + fi +} + +function generate_password () { + set +o pipefail + < /dev/urandom tr -dc A-Za-z0-9 | head -c40 + echo + set -o pipefail +} + +# Initialize a CA if not already initialized +function step_ca_init () { + local -a setup_args=( + --name "${DOCKER_STEPCA_INIT_NAME}" + --dns "${DOCKER_STEPCA_INIT_DNS_NAMES}" + --provisioner "${DOCKER_STEPCA_INIT_PROVISIONER_NAME:-admin}" + --password-file "${STEPPATH}/password" + --address ":9000" + ) + if [ -n "${DOCKER_STEPCA_INIT_PASSWORD}" ]; then + echo "${DOCKER_STEPCA_INIT_PASSWORD}" > "${STEPPATH}/password" + else + generate_password > "${STEPPATH}/password" + fi + if [ -n "${DOCKER_STEPCA_INIT_SSH}" ]; then + setup_args=("${setup_args[@]}" --ssh) + fi + step ca init "${setup_args[@]}" + mv $STEPPATH/password $PWDPATH +} + +if [ ! -f "${STEPPATH}/config/ca.json" ]; then + init_if_possible +fi + +exec "${@}" diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 93749026..35f75159 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -7,12 +7,20 @@ to manage issues, etc. ## Table of Contents -* [Building From Source](#building-from-source) -* [Asking Support Questions](#asking-support-questions) -* [Reporting Issues](#reporting-issues) -* [Submitting Patches](#submitting-patches) - * [Code Contribution Guidelines](#code-contribution-guidelines) - * [Git Commit Message Guidelines](#git-commit-message-guidelines) +- [Contributing to `step certificates`](#contributing-to-step-certificates) + - [Table of Contents](#table-of-contents) + - [Building From Source](#building-from-source) + - [Build a standard `step-ca`](#build-a-standard-step-ca) + - [Build `step-ca` using CGO](#build-step-ca-using-cgo) + - [The CGO build enables PKCS #11 and YubiKey PIV support](#the-cgo-build-enables-pkcs-11-and-yubikey-piv-support) + - [1. Install PCSC support](#1-install-pcsc-support) + - [2. Build `step-ca`](#2-build-step-ca) + - [Asking Support Questions](#asking-support-questions) + - [Reporting Issues](#reporting-issues) + - [Code Contribution](#code-contribution) + - [Submitting Patches](#submitting-patches) + - [Code Contribution Guidelines](#code-contribution-guidelines) + - [Git Commit Message Guidelines](#git-commit-message-guidelines) ## Building From Source @@ -73,7 +81,7 @@ When the build is complete, you will find binaries in `bin/`. ## Asking Support Questions -Feel free to post a question on our [GitHub Discussions](https://github.com/smallstep/certificates/discussions) page, or find us on [Gitter](https://gitter.im/smallstep/community). +Feel free to post a question on our [GitHub Discussions](https://github.com/smallstep/certificates/discussions) page, or find us on [Discord](https://bit.ly/step-discord). ## Reporting Issues diff --git a/docs/provisioners.md b/docs/provisioners.md index 7ee9af50..18010f88 100644 --- a/docs/provisioners.md +++ b/docs/provisioners.md @@ -1,7 +1,7 @@ # Provisioners > Note: The canonical documentation for `step-ca` provisioners now lives at -> https://smallstep.com/docs/step-ca/configuration#provisioners. Documentation +> https://smallstep.com/docs/step-ca/provisioners. Documentation > found on this page may be out of date. Provisioners are people or code that are registered with the CA and authorized diff --git a/docs/revocation.md b/docs/revocation.md index e994940d..4f3a7d5e 100644 --- a/docs/revocation.md +++ b/docs/revocation.md @@ -202,7 +202,8 @@ through an example. [Use TLS Everywhere](https://smallstep.com/blog/use-tls.html) and let us know what you think of our tools. Get in touch over [Twitter](twitter.com/smallsteplabs) or through our -[GitHub Discussions](https://github.com/smallstep/certificates/discussions) to chat with us in real time. +[GitHub Discussions](https://github.com/smallstep/certificates/discussions) to find answers to frequently asked questions. +[Discord](https://bit.ly/step-discord) to chat with us in real time. ## Further Reading diff --git a/examples/basic-client/client.go b/examples/basic-client/client.go index db6092bf..42358ac8 100644 --- a/examples/basic-client/client.go +++ b/examples/basic-client/client.go @@ -116,7 +116,6 @@ func main() { Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, MaxIdleConns: 100, diff --git a/go.mod b/go.mod index 58228557..48299d6a 100644 --- a/go.mod +++ b/go.mod @@ -1,47 +1,54 @@ module github.com/smallstep/certificates -go 1.14 +go 1.15 require ( cloud.google.com/go v0.83.0 - github.com/Masterminds/sprig/v3 v3.1.0 + github.com/Azure/azure-sdk-for-go v58.0.0+incompatible + github.com/Azure/go-autorest/autorest v0.11.17 + github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 + github.com/Azure/go-autorest/autorest/date v0.3.0 + github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect + github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.2 github.com/ThalesIgnite/crypto11 v1.2.4 github.com/aws/aws-sdk-go v1.30.29 + github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect github.com/go-chi/chi v4.0.2+incompatible github.com/go-kit/kit v0.10.0 // indirect github.com/go-piv/piv-go v1.7.0 - github.com/golang/mock v1.5.0 - github.com/google/uuid v1.1.2 + github.com/golang/mock v1.6.0 + github.com/google/uuid v1.3.0 github.com/googleapis/gax-go/v2 v2.0.5 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.13 // indirect - github.com/micromdm/scep/v2 v2.0.0 + github.com/micromdm/scep/v2 v2.1.0 github.com/newrelic/go-agent v2.15.0+incompatible github.com/pkg/errors v0.9.1 github.com/rs/xid v1.2.1 github.com/sirupsen/logrus v1.4.2 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 - github.com/smallstep/nosql v0.3.6 - github.com/stretchr/testify v1.7.0 // indirect + github.com/smallstep/nosql v0.3.8 github.com/urfave/cli v1.22.4 - go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 - go.step.sm/cli-utils v0.4.1 - go.step.sm/crypto v0.9.0 - go.step.sm/linkedca v0.0.0-20210611183751-27424aae8d25 - golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 - golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 + go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 + go.step.sm/cli-utils v0.6.1 + go.step.sm/crypto v0.13.0 + go.step.sm/linkedca v0.7.0 + golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 + golang.org/x/net v0.0.0-20210913180222-943fd674d43e google.golang.org/api v0.47.0 - google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c - google.golang.org/grpc v1.38.0 - google.golang.org/protobuf v1.26.0 - gopkg.in/square/go-jose.v2 v2.5.1 + google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492 + google.golang.org/grpc v1.39.0 + google.golang.org/protobuf v1.27.1 + gopkg.in/square/go-jose.v2 v2.6.0 ) +// avoid license conflict from juju/ansiterm until https://github.com/manifoldco/promptui/pull/181 +// is merged or other dependency in path currently in violation fixes compliance +replace github.com/manifoldco/promptui => github.com/nguyer/promptui v0.8.1-0.20210517132806-70ccd4709797 + // replace github.com/smallstep/nosql => ../nosql - -//replace go.step.sm/crypto => ../crypto - -//replace go.step.sm/cli-utils => ../cli-utils - -replace go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 => github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568 +// replace go.step.sm/crypto => ../crypto +// replace go.step.sm/cli-utils => ../cli-utils +// replace go.step.sm/linkedca => ../linkedca diff --git a/go.sum b/go.sum index e2e71580..252832ea 100644 --- a/go.sum +++ b/go.sum @@ -40,92 +40,96 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/Azure/azure-sdk-for-go v58.0.0+incompatible h1:Cw16jiP4dI+CK761aq44ol4RV5dUiIIXky1+EKpoiVM= +github.com/Azure/azure-sdk-for-go v58.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.17 h1:2zCdHwNgRH+St1J+ZMf66xI8aLr/5KMy+wWLH97zwYM= +github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.11 h1:L4/pmq7poLdsy41Bj1FayKvBhayuWRYkx9HU5i4Ybl0= +github.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 h1:TzPg6B6fTZ0G1zBf3T54aI7p3cAT6u//TOXGPmFMOXg= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.8/go.mod h1:kxyKZTSfKh8OVFWPAgOgQ/frrJgeYQJPyR5fLFmXko4= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= +github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= -github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -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/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig/v3 v3.1.0 h1:j7GpgZ7PdFqNsmncycTHsLmVPf5/3wJtlgW9TNDYD9Y= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= github.com/ThalesIgnite/crypto11 v1.2.4 h1:3MebRK/U0mA2SmSthXAIZAdUA9w8+ZuKem2O6HuR1f8= github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= github.com/ThalesIgnite/crypto11 v1.2.4/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= -github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.30.29 h1:NXNqBS9hjOCpDL8SyCyl38gZX3LLLunKOJc5E7vJ8P0= github.com/aws/aws-sdk-go v1.30.29/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/aws/aws-sdk-go-v2 v0.18.0 h1:qZ+woO4SamnH/eEbjM2IDLhRNwIwND/RQyVlBLp3Jqg= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= -github.com/casbin/casbin/v2 v2.1.2 h1:bTwon/ECRx9dwBy2ewRVr5OiqjeXSGiTUY74sDPQi/g= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -133,58 +137,49 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O 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/clbanning/x2j v0.0.0-20191024224557-825249438eec h1:EdRZT3IeKQmfCSrgo8SZ8V3MEnskuJP0wCYNpe+aiXo= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= -github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= -github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20201003150343-5d1bab4fc658 h1:/WBjuutuivOA02gpDtrvrWKw01ugkyt3QnimB7enbtI= -github.com/dgraph-io/badger/v2 v2.0.1-rc1.0.20201003150343-5d1bab4fc658/go.mod h1:2uGEvGm+JSDLd5UAaKIFSbXDcYyeH0fWJP4N2HMMYMI= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd h1:KoJOtZf+6wpQaDTuOWGuo61GxcPBIfhwRxRTaTWGCTc= github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd/go.mod h1:YylP9MpCYGVZQrly/j/diqcdUetCRRePeBB0c2VGXsA= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -192,32 +187,26 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d h1:QyzYnTnPE15SQyUeqU6qLbWxMkwyAyu+vGksa0b7j00= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db h1:gb2Z18BhTPJPpLQWj4T+rfKHYCHxRHCtRxhKKjRidVw= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 h1:a9ENSRDFBUPkJ5lCgVZh26+ZbGyoVJG7yb5SSzF5H54= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -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/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.4.0 h1:KeVK+Emj3c3S4eRztFuzbFYb2BAgf2jmwDwyXEri7Lo= github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= @@ -227,17 +216,13 @@ github.com/go-piv/piv-go v1.7.0/go.mod h1:ON2WvQncm7dIkCQ7kYJs+nc3V4jHGfrrJnSF8H github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.6.0 h1:MmJCxYVKTJ0SplGKqFVX3SBnmaUhODHZrrFF6jMbpZk= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -251,8 +236,9 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -267,14 +253,11 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -289,13 +272,10 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -315,106 +295,76 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda h1:5ikpG9mYCMFiZX0nkxoV6aU2IpCHPdws3gCNgdZeEV0= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/consul/api v1.3.0 h1:HXNYlRkkM/t+Y/Yhxtwcy02dlYwIaoxzvxPnS+cqy78= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= -github.com/hashicorp/consul/sdk v0.3.0 h1:UOxjlb4xVNF93jak1mzzoBatyFju9nrkxpVwIp/QqxQ= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/hudl/fargo v1.3.0 h1:0U6+BtN6LhaYuTnIJq4Wyq5cpn6O2kWrxAtcqBmYY6w= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -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/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0 h1:ZqfnKyx9KGpRcW04j5nnPDgRgoXUeLh2YFBeFzphcA0= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -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.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= @@ -422,19 +372,11 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn 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/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 h1:143Bb8f8DuGWck/xpNUOckBVYfFbBTnLevfRZ1aVVqo= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1 h1:vi1F1IQ8N7hNWytK9DpJsUfQhGuNSc19z330K6vl4zk= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= -github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/lyft/protoc-gen-validate v0.0.13 h1:KNt/RhmQTOLr7Aj8PsJ7mTronaFyx80mRTT9qF261dA= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= -github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= @@ -446,132 +388,95 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/micromdm/scep/v2 v2.0.0 h1:cRzcY0S5QX+0+J+7YC4P2uZSnfMup8S8zJu/bLFgOkA= -github.com/micromdm/scep/v2 v2.0.0/go.mod h1:ouaDs5tcjOjdHD/h8BGaQsWE87MUnQ/wMTMgfMMIpPc= -github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXmaU= +github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvvGoOExBcc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f h1:eVB9ELsoq5ouItQBr5Tj334bhPJG/MX+m7rTchmzVUQ= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -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/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2AeKLr2AMdilSD8+f9bvMnNN8VS5iDtovc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2 h1:i2Ly0B+1+rzNZHHWtD4ZwKi+OU5l+uQo1iDHZ2PmiIc= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/newrelic/go-agent v2.15.0+incompatible h1:IB0Fy+dClpBq9aEoIrLyQXzU34JyI1xVTanPLB/+jvU= github.com/newrelic/go-agent v2.15.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= -github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk= +github.com/nguyer/promptui v0.8.1-0.20210517132806-70ccd4709797 h1:unCiBzwNjcuVbP3bgM76z0ORyIuI4sspop1qhkQJ044= +github.com/nguyer/promptui v0.8.1-0.20210517132806-70ccd4709797/go.mod h1:CBMXL3a2sC3Q8TjpLcQt8w/3aQ23VSy6r7UFeCG6phA= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5 h1:58+kh9C6jJVXYjt8IE48G2eWl6BjwU5Gj0gqY84fy78= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568 h1:+MPqEswjYiS0S1FCTg8MIhMBMzxiVQ94rooFwvPPiWk= -github.com/omorsi/pkcs7 v0.0.0-20210217142924-a7b80a2a8568/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0 h1:YyUAhaEfjoWXclZVJ9sGoNct7j4TVk7lZWlQw5UXuoo= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 h1:ZCnq+JUrvXcDVhX/xRolRBZifmabN1HcS1wrPSvxhrU= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 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/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.3.0 h1:miYCvYqFXtl/J9FIy8eNpBfYthAEFg+Ys0XyUVEcDsc= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.1.0 h1:ElTg5tNp4DqfV7UQjDqv2+RJlNzsDtvNAWccbItceIE= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.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= @@ -579,14 +484,11 @@ github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNue github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -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/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -595,35 +497,28 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/nosql v0.3.6 h1:cq6a3NwjFJxkVlWU1T4qGskcfEXr0fO1WqQrraDO1Po= -github.com/smallstep/nosql v0.3.6/go.mod h1:h1zC/Z54uNHc8euquLED4qJNCrMHd3nytA141ZZh4qQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smallstep/nosql v0.3.8 h1:1/EWUbbEdz9ai0g9Fd09VekVjtxp+5+gIHpV2PdwW3o= +github.com/smallstep/nosql v0.3.8/go.mod h1:X2qkYpNcW3yjLUvhEHfgGfClpKbFPapewvx7zo4TOFs= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sony/gobreaker v0.4.1 h1:oMnRNZXX5j85zso6xCPRNPtmAycat+WcoKbklScLDgQ= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 h1:WhxRHzgeVGETMlmVfqhRn8RIeeNoPr2Czh33I4Zdccw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a h1:AhmOdSHeswKHBjhsLs/7+1voOxT+LLrSk/Nxvk35fug= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -631,23 +526,18 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 h1:ndzgwNDnKIqyCvHTXaCqh9KlOWKvBry6nuXMJmonVsE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -657,8 +547,10 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 h1:VcrIfasaLFkyjk6KNlXQSzO+B0fZcnECiDrKJsfxka0= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mozilla.org/pkcs7 v0.0.0-20210730143726-725912489c62/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -669,31 +561,20 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.step.sm/cli-utils v0.2.0 h1:hpVu9+6dpv/7/Bd8nGJFc3V+gQ+TciSJRTu9TavDUQ4= -go.step.sm/cli-utils v0.2.0/go.mod h1:+t4qCp5NO+080DdGkJxEh3xL5S4TcYC2JTPLMM72b6Y= -go.step.sm/cli-utils v0.4.0 h1:dni6gR/6/LOqfbzm/yUdgz5O12tkxX17SxA9+pRMidI= -go.step.sm/cli-utils v0.4.0/go.mod h1:1zFgatDqEJ1Y4MNStdWa0b1NPc1fvSHbDJC+wZ6iQlE= -go.step.sm/cli-utils v0.4.1 h1:QztRUhGYjOPM1I2Nmi7V6XejQyVtcESmo+sbegxvX7Q= -go.step.sm/cli-utils v0.4.1/go.mod h1:hWYVOSlw8W9Pd+BwIbs/aftVVMRms3EG7Q2qLRwc0WA= -go.step.sm/crypto v0.6.1/go.mod h1:AKS4yMZVZD4EGjpSkY4eibuMenrvKCscb+BpWMet8c0= -go.step.sm/crypto v0.8.3 h1:TO/OPlaUrYXhs8srGEFNyL6OWVQvRmEPCUONNnQUuEM= -go.step.sm/crypto v0.8.3/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= -go.step.sm/crypto v0.9.0 h1:q2AllTSnVj4NRtyEPkGW2ohArLmbGbe6ZAL/VIOKDzA= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.step.sm/cli-utils v0.6.1 h1:v31ctEh/BFPGU067fF9Y8u2EIg6LRldUbN2dc/+u/V8= +go.step.sm/cli-utils v0.6.1/go.mod h1:stgyXHHHi9KwcR86sgzDdFC6e/tAmpF4NbqwSK7q/GM= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= -go.step.sm/linkedca v0.0.0-20210610014030-59b16916c7e7 h1:hAfzUm80XWGtFnxyVgeT/gc/3XnlVNnHD5HrLbk4Fc0= -go.step.sm/linkedca v0.0.0-20210610014030-59b16916c7e7/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= -go.step.sm/linkedca v0.0.0-20210611183751-27424aae8d25 h1:ncJqviWswJT19IdnfOYQGKG1zL7IDy4lAJz1PuM3fgw= -go.step.sm/linkedca v0.0.0-20210611183751-27424aae8d25/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= +go.step.sm/crypto v0.13.0 h1:mQuP9Uu2FNmqCJNO0OTbvolnYXzONy4wdUBtUVcP1s8= +go.step.sm/crypto v0.13.0/go.mod h1:5YzQ85BujYBu6NH18jw7nFjwuRnDch35nLzH0ES5sKg= +go.step.sm/linkedca v0.7.0 h1:ydYigs0CgLFkPGjOO4KJcAcAWbuPP8ECF1IsyHdftYc= +go.step.sm/linkedca v0.7.0/go.mod h1:5uTRjozEGSPAZal9xJqlaD38cvJcLe3o1VAFVjqcORo= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -706,8 +587,10 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 h1:3erb+vDS8lU1sxfDHF4/hhWyaXnhIaO+7RgL4fDZORA= +golang.org/x/crypto v0.0.0-20210915214749-c084706c2272/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -786,10 +669,9 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8= +golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -870,10 +752,10 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210915083310-ed5796bab164 h1:7ZDGnxgHAMw7thfC5bEos0RDAccZKxioiWBhfIe+tvw= +golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -883,8 +765,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -943,6 +826,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1005,6 +889,7 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -1022,10 +907,9 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d h1:KzwjikDymrEmYYbdyfievTwjEeGlu+OM6oiKBkF3Jfg= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492 h1:7yQQsvnwjfEahbNNEKcBHv3mR+HnB1ctGY/z1JXzx8M= +google.golang.org/genproto v0.0.0-20210719143636-1d5a45f8e492/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1044,6 +928,7 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= @@ -1051,8 +936,9 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0 h1:Klz8I9kdtkIN6EpHHUOMLCYhTn/2WAe5a0s1hcBkdTI= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1065,35 +951,31 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= -gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1107,7 +989,5 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 h1:ucqkfpjg9WzSUubAO62csmucvxl4/JeW3F4I4909XkM= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/kms/apiv1/options.go b/kms/apiv1/options.go index 7cc7f748..79b07a60 100644 --- a/kms/apiv1/options.go +++ b/kms/apiv1/options.go @@ -29,6 +29,12 @@ type CertificateManager interface { StoreCertificate(req *StoreCertificateRequest) error } +// ValidateName is an interface that KeyManager can implement to validate a +// given name or URI. +type NameValidator interface { + ValidateName(s string) error +} + // ErrNotImplemented is the type of error returned if an operation is not // implemented. type ErrNotImplemented struct { @@ -73,6 +79,8 @@ const ( YubiKey Type = "yubikey" // SSHAgentKMS is a KMS implementation using ssh-agent to access keys. SSHAgentKMS Type = "sshagentkms" + // AzureKMS is a KMS implementation using Azure Key Vault. + AzureKMS Type = "azurekms" ) // Options are the KMS options. They represent the kms object in the ca.json. @@ -81,18 +89,18 @@ type Options struct { Type string `json:"type"` // Path to the credentials file used in CloudKMS and AmazonKMS. - CredentialsFile string `json:"credentialsFile"` + CredentialsFile string `json:"credentialsFile,omitempty"` // URI is based on the PKCS #11 URI Scheme defined in // https://tools.ietf.org/html/rfc7512 and represents the configuration used // to connect to the KMS. // // Used by: pkcs11 - URI string `json:"uri"` + URI string `json:"uri,omitempty"` // Pin used to access the PKCS11 module. It can be defined in the URI using // the pin-value or pin-source properties. - Pin string `json:"pin"` + Pin string `json:"pin,omitempty"` // ManagementKey used in YubiKeys. Default management key is the hexadecimal // string 010203040506070801020304050607080102030405060708: @@ -101,13 +109,13 @@ type Options struct { // 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // } - ManagementKey string `json:"managementKey"` + ManagementKey string `json:"managementKey,omitempty"` // Region to use in AmazonKMS. - Region string `json:"region"` + Region string `json:"region,omitempty"` // Profile to use in AmazonKMS. - Profile string `json:"profile"` + Profile string `json:"profile,omitempty"` } // Validate checks the fields in Options. @@ -118,8 +126,9 @@ func (o *Options) Validate() error { switch Type(strings.ToLower(o.Type)) { case DefaultKMS, SoftKMS: // Go crypto based kms. - case CloudKMS, AmazonKMS, SSHAgentKMS: // Cloud based kms. + case CloudKMS, AmazonKMS, AzureKMS: // Cloud based kms. case YubiKey, PKCS11: // Hardware based kms. + case SSHAgentKMS: // Others default: return errors.Errorf("unsupported kms type %s", o.Type) } diff --git a/kms/apiv1/requests.go b/kms/apiv1/requests.go index f6fe7dd2..762d9dd8 100644 --- a/kms/apiv1/requests.go +++ b/kms/apiv1/requests.go @@ -100,7 +100,7 @@ type GetPublicKeyRequest struct { type CreateKeyRequest struct { // Name represents the key name or label used to identify a key. // - // Used by: awskms, cloudkms, pkcs11, yubikey. + // Used by: awskms, cloudkms, azurekms, pkcs11, yubikey. Name string // SignatureAlgorithm represents the type of key to create. @@ -110,8 +110,14 @@ type CreateKeyRequest struct { Bits int // ProtectionLevel specifies how cryptographic operations are performed. - // Used by: cloudkms + // Used by: cloudkms, azurekms. ProtectionLevel ProtectionLevel + + // Extractable defines if the new key may be exported from the HSM under a + // wrap key. On pkcs11 sets the CKA_EXTRACTABLE bit. + // + // Used by: pkcs11 + Extractable bool } // CreateKeyResponse is the response value of the kms.CreateKey method. @@ -152,4 +158,10 @@ type LoadCertificateRequest struct { type StoreCertificateRequest struct { Name string Certificate *x509.Certificate + + // Extractable defines if the new certificate may be exported from the HSM + // under a wrap key. On pkcs11 sets the CKA_EXTRACTABLE bit. + // + // Used by: pkcs11 + Extractable bool } diff --git a/kms/azurekms/internal/mock/key_vault_client.go b/kms/azurekms/internal/mock/key_vault_client.go new file mode 100644 index 00000000..42bd55fd --- /dev/null +++ b/kms/azurekms/internal/mock/key_vault_client.go @@ -0,0 +1,80 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/smallstep/certificates/kms/azurekms (interfaces: KeyVaultClient) + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + keyvault "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// KeyVaultClient is a mock of KeyVaultClient interface +type KeyVaultClient struct { + ctrl *gomock.Controller + recorder *KeyVaultClientMockRecorder +} + +// KeyVaultClientMockRecorder is the mock recorder for KeyVaultClient +type KeyVaultClientMockRecorder struct { + mock *KeyVaultClient +} + +// NewKeyVaultClient creates a new mock instance +func NewKeyVaultClient(ctrl *gomock.Controller) *KeyVaultClient { + mock := &KeyVaultClient{ctrl: ctrl} + mock.recorder = &KeyVaultClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *KeyVaultClient) EXPECT() *KeyVaultClientMockRecorder { + return m.recorder +} + +// CreateKey mocks base method +func (m *KeyVaultClient) CreateKey(arg0 context.Context, arg1, arg2 string, arg3 keyvault.KeyCreateParameters) (keyvault.KeyBundle, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateKey", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(keyvault.KeyBundle) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateKey indicates an expected call of CreateKey +func (mr *KeyVaultClientMockRecorder) CreateKey(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateKey", reflect.TypeOf((*KeyVaultClient)(nil).CreateKey), arg0, arg1, arg2, arg3) +} + +// GetKey mocks base method +func (m *KeyVaultClient) GetKey(arg0 context.Context, arg1, arg2, arg3 string) (keyvault.KeyBundle, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetKey", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(keyvault.KeyBundle) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetKey indicates an expected call of GetKey +func (mr *KeyVaultClientMockRecorder) GetKey(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKey", reflect.TypeOf((*KeyVaultClient)(nil).GetKey), arg0, arg1, arg2, arg3) +} + +// Sign mocks base method +func (m *KeyVaultClient) Sign(arg0 context.Context, arg1, arg2, arg3 string, arg4 keyvault.KeySignParameters) (keyvault.KeyOperationResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Sign", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(keyvault.KeyOperationResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Sign indicates an expected call of Sign +func (mr *KeyVaultClientMockRecorder) Sign(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sign", reflect.TypeOf((*KeyVaultClient)(nil).Sign), arg0, arg1, arg2, arg3, arg4) +} diff --git a/kms/azurekms/key_vault.go b/kms/azurekms/key_vault.go new file mode 100644 index 00000000..34d9c3f1 --- /dev/null +++ b/kms/azurekms/key_vault.go @@ -0,0 +1,342 @@ +package azurekms + +import ( + "context" + "crypto" + "regexp" + "time" + + "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/azure/auth" + "github.com/Azure/go-autorest/autorest/date" + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/uri" +) + +func init() { + apiv1.Register(apiv1.AzureKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) { + return New(ctx, opts) + }) +} + +// Scheme is the scheme used for the Azure Key Vault uris. +const Scheme = "azurekms" + +// keyIDRegexp is the regular expression that Key Vault uses on the kid. We can +// extract the vault, name and version of the key. +var keyIDRegexp = regexp.MustCompile(`^https://([0-9a-zA-Z-]+)\.vault\.azure\.net/keys/([0-9a-zA-Z-]+)/([0-9a-zA-Z-]+)$`) + +var ( + valueTrue = true + value2048 int32 = 2048 + value3072 int32 = 3072 + value4096 int32 = 4096 +) + +var now = func() time.Time { + return time.Now().UTC() +} + +type keyType struct { + Kty keyvault.JSONWebKeyType + Curve keyvault.JSONWebKeyCurveName +} + +func (k keyType) KeyType(pl apiv1.ProtectionLevel) keyvault.JSONWebKeyType { + switch k.Kty { + case keyvault.EC: + if pl == apiv1.HSM { + return keyvault.ECHSM + } + return k.Kty + case keyvault.RSA: + if pl == apiv1.HSM { + return keyvault.RSAHSM + } + return k.Kty + default: + return "" + } +} + +var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]keyType{ + apiv1.UnspecifiedSignAlgorithm: { + Kty: keyvault.EC, + Curve: keyvault.P256, + }, + apiv1.SHA256WithRSA: { + Kty: keyvault.RSA, + }, + apiv1.SHA384WithRSA: { + Kty: keyvault.RSA, + }, + apiv1.SHA512WithRSA: { + Kty: keyvault.RSA, + }, + apiv1.SHA256WithRSAPSS: { + Kty: keyvault.RSA, + }, + apiv1.SHA384WithRSAPSS: { + Kty: keyvault.RSA, + }, + apiv1.SHA512WithRSAPSS: { + Kty: keyvault.RSA, + }, + apiv1.ECDSAWithSHA256: { + Kty: keyvault.EC, + Curve: keyvault.P256, + }, + apiv1.ECDSAWithSHA384: { + Kty: keyvault.EC, + Curve: keyvault.P384, + }, + apiv1.ECDSAWithSHA512: { + Kty: keyvault.EC, + Curve: keyvault.P521, + }, +} + +// vaultResource is the value the client will use as audience. +const vaultResource = "https://vault.azure.net" + +// KeyVaultClient is the interface implemented by keyvault.BaseClient. It will +// be used for testing purposes. +type KeyVaultClient interface { + GetKey(ctx context.Context, vaultBaseURL string, keyName string, keyVersion string) (keyvault.KeyBundle, error) + CreateKey(ctx context.Context, vaultBaseURL string, keyName string, parameters keyvault.KeyCreateParameters) (keyvault.KeyBundle, error) + Sign(ctx context.Context, vaultBaseURL string, keyName string, keyVersion string, parameters keyvault.KeySignParameters) (keyvault.KeyOperationResult, error) +} + +// KeyVault implements a KMS using Azure Key Vault. +// +// The URI format used in Azure Key Vault is the following: +// +// - azurekms:name=key-name;vault=vault-name +// - azurekms:name=key-name;vault=vault-name?version=key-version +// - azurekms:name=key-name;vault=vault-name?hsm=true +// +// The scheme is "azurekms"; "name" is the key name; "vault" is the key vault +// name where the key is located; "version" is an optional parameter that +// defines the version of they key, if version is not given, the latest one will +// be used; "hsm" defines if an HSM want to be used for this key, this is +// specially useful when this is used from `step`. +// +// TODO(mariano): The implementation is using /services/keyvault/v7.1/keyvault +// package, at some point Azure might create a keyvault client with all the +// functionality in /sdk/keyvault, we should migrate to that once available. +type KeyVault struct { + baseClient KeyVaultClient + defaults DefaultOptions +} + +// DefaultOptions are custom options that can be passed as defaults using the +// URI in apiv1.Options. +type DefaultOptions struct { + Vault string + ProtectionLevel apiv1.ProtectionLevel +} + +var createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) { + baseClient := keyvault.New() + + // With an URI, try to log in only using client credentials in the URI. + // Client credentials requires: + // - client-id + // - client-secret + // - tenant-id + // And optionally the aad-endpoint to support custom clouds: + // - aad-endpoint (defaults to https://login.microsoftonline.com/) + if opts.URI != "" { + u, err := uri.ParseWithScheme(Scheme, opts.URI) + if err != nil { + return nil, err + } + + // Required options + clientID := u.Get("client-id") + clientSecret := u.Get("client-secret") + tenantID := u.Get("tenant-id") + // optional + aadEndpoint := u.Get("aad-endpoint") + + if clientID != "" && clientSecret != "" && tenantID != "" { + s := auth.EnvironmentSettings{ + Values: map[string]string{ + auth.ClientID: clientID, + auth.ClientSecret: clientSecret, + auth.TenantID: tenantID, + auth.Resource: vaultResource, + }, + Environment: azure.PublicCloud, + } + if aadEndpoint != "" { + s.Environment.ActiveDirectoryEndpoint = aadEndpoint + } + baseClient.Authorizer, err = s.GetAuthorizer() + if err != nil { + return nil, err + } + return baseClient, nil + } + } + + // Attempt to authorize with the following methods: + // 1. Environment variables. + // - Client credentials + // - Client certificate + // - Username and password + // - MSI + // 2. Using Azure CLI 2.0 on local development. + authorizer, err := auth.NewAuthorizerFromEnvironmentWithResource(vaultResource) + if err != nil { + authorizer, err = auth.NewAuthorizerFromCLIWithResource(vaultResource) + if err != nil { + return nil, errors.Wrap(err, "error getting authorizer for key vault") + } + } + baseClient.Authorizer = authorizer + return &baseClient, nil +} + +// New initializes a new KMS implemented using Azure Key Vault. +func New(ctx context.Context, opts apiv1.Options) (*KeyVault, error) { + baseClient, err := createClient(ctx, opts) + if err != nil { + return nil, err + } + + // step and step-ca do not need and URI, but having a default vault and + // protection level is useful if this package is used as an api + var defaults DefaultOptions + if opts.URI != "" { + u, err := uri.ParseWithScheme(Scheme, opts.URI) + if err != nil { + return nil, err + } + defaults.Vault = u.Get("vault") + if u.GetBool("hsm") { + defaults.ProtectionLevel = apiv1.HSM + } + } + + return &KeyVault{ + baseClient: baseClient, + defaults: defaults, + }, nil +} + +// GetPublicKey loads a public key from Azure Key Vault by its resource name. +func (k *KeyVault) GetPublicKey(req *apiv1.GetPublicKeyRequest) (crypto.PublicKey, error) { + if req.Name == "" { + return nil, errors.New("getPublicKeyRequest 'name' cannot be empty") + } + + vault, name, version, _, err := parseKeyName(req.Name, k.defaults) + if err != nil { + return nil, err + } + + ctx, cancel := defaultContext() + defer cancel() + + resp, err := k.baseClient.GetKey(ctx, vaultBaseURL(vault), name, version) + if err != nil { + return nil, errors.Wrap(err, "keyVault GetKey failed") + } + + return convertKey(resp.Key) +} + +// CreateKey creates a asymmetric key in Azure Key Vault. +func (k *KeyVault) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyResponse, error) { + if req.Name == "" { + return nil, errors.New("createKeyRequest 'name' cannot be empty") + } + + vault, name, _, hsm, err := parseKeyName(req.Name, k.defaults) + if err != nil { + return nil, err + } + + // Override protection level to HSM only if it's not specified, and is given + // in the uri. + protectionLevel := req.ProtectionLevel + if protectionLevel == apiv1.UnspecifiedProtectionLevel && hsm { + protectionLevel = apiv1.HSM + } + + kt, ok := signatureAlgorithmMapping[req.SignatureAlgorithm] + if !ok { + return nil, errors.Errorf("keyVault does not support signature algorithm '%s'", req.SignatureAlgorithm) + } + var keySize *int32 + if kt.Kty == keyvault.RSA || kt.Kty == keyvault.RSAHSM { + switch req.Bits { + case 2048: + keySize = &value2048 + case 0, 3072: + keySize = &value3072 + case 4096: + keySize = &value4096 + default: + return nil, errors.Errorf("keyVault does not support key size %d", req.Bits) + } + } + + created := date.UnixTime(now()) + + ctx, cancel := defaultContext() + defer cancel() + + resp, err := k.baseClient.CreateKey(ctx, vaultBaseURL(vault), name, keyvault.KeyCreateParameters{ + Kty: kt.KeyType(protectionLevel), + KeySize: keySize, + Curve: kt.Curve, + KeyOps: &[]keyvault.JSONWebKeyOperation{ + keyvault.Sign, keyvault.Verify, + }, + KeyAttributes: &keyvault.KeyAttributes{ + Enabled: &valueTrue, + Created: &created, + NotBefore: &created, + }, + }) + if err != nil { + return nil, errors.Wrap(err, "keyVault CreateKey failed") + } + + publicKey, err := convertKey(resp.Key) + if err != nil { + return nil, err + } + + keyURI := getKeyName(vault, name, resp) + return &apiv1.CreateKeyResponse{ + Name: keyURI, + PublicKey: publicKey, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: keyURI, + }, + }, nil +} + +// CreateSigner returns a crypto.Signer from a previously created asymmetric key. +func (k *KeyVault) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { + if req.SigningKey == "" { + return nil, errors.New("createSignerRequest 'signingKey' cannot be empty") + } + return NewSigner(k.baseClient, req.SigningKey, k.defaults) +} + +// Close closes the client connection to the Azure Key Vault. This is a noop. +func (k *KeyVault) Close() error { + return nil +} + +// ValidateName validates that the given string is a valid URI. +func (k *KeyVault) ValidateName(s string) error { + _, _, _, _, err := parseKeyName(s, k.defaults) + return err +} diff --git a/kms/azurekms/key_vault_test.go b/kms/azurekms/key_vault_test.go new file mode 100644 index 00000000..8f968189 --- /dev/null +++ b/kms/azurekms/key_vault_test.go @@ -0,0 +1,653 @@ +//go:generate mockgen -package mock -mock_names=KeyVaultClient=KeyVaultClient -destination internal/mock/key_vault_client.go github.com/smallstep/certificates/kms/azurekms KeyVaultClient +package azurekms + +import ( + "context" + "crypto" + "encoding/json" + "fmt" + "reflect" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + "github.com/Azure/go-autorest/autorest/date" + "github.com/golang/mock/gomock" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/azurekms/internal/mock" + "go.step.sm/crypto/keyutil" + "gopkg.in/square/go-jose.v2" +) + +var errTest = fmt.Errorf("test error") + +func mockNow(t *testing.T) time.Time { + old := now + t0 := time.Unix(1234567890, 123).UTC() + now = func() time.Time { + return t0 + } + t.Cleanup(func() { + now = old + }) + return t0 +} + +func mockClient(t *testing.T) *mock.KeyVaultClient { + t.Helper() + ctrl := gomock.NewController(t) + t.Cleanup(func() { + ctrl.Finish() + }) + return mock.NewKeyVaultClient(ctrl) +} + +func createJWK(t *testing.T, pub crypto.PublicKey) *keyvault.JSONWebKey { + t.Helper() + b, err := json.Marshal(&jose.JSONWebKey{ + Key: pub, + }) + if err != nil { + t.Fatal(err) + } + key := new(keyvault.JSONWebKey) + if err := json.Unmarshal(b, key); err != nil { + t.Fatal(err) + } + return key +} + +func Test_now(t *testing.T) { + t0 := now() + if loc := t0.Location(); loc != time.UTC { + t.Errorf("now() Location = %v, want %v", loc, time.UTC) + } +} + +func TestNew(t *testing.T) { + client := mockClient(t) + old := createClient + t.Cleanup(func() { + createClient = old + }) + + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + setup func() + args args + want *KeyVault + wantErr bool + }{ + {"ok", func() { + createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) { + return client, nil + } + }, args{context.Background(), apiv1.Options{}}, &KeyVault{ + baseClient: client, + }, false}, + {"ok with vault", func() { + createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) { + return client, nil + } + }, args{context.Background(), apiv1.Options{ + URI: "azurekms:vault=my-vault", + }}, &KeyVault{ + baseClient: client, + defaults: DefaultOptions{ + Vault: "my-vault", + ProtectionLevel: apiv1.UnspecifiedProtectionLevel, + }, + }, false}, + {"ok with vault + hsm", func() { + createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) { + return client, nil + } + }, args{context.Background(), apiv1.Options{ + URI: "azurekms:vault=my-vault;hsm=true", + }}, &KeyVault{ + baseClient: client, + defaults: DefaultOptions{ + Vault: "my-vault", + ProtectionLevel: apiv1.HSM, + }, + }, false}, + {"fail", func() { + createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) { + return nil, errTest + } + }, args{context.Background(), apiv1.Options{}}, nil, true}, + {"fail uri", func() { + createClient = func(ctx context.Context, opts apiv1.Options) (KeyVaultClient, error) { + return client, nil + } + }, args{context.Background(), apiv1.Options{ + URI: "kms:vault=my-vault;hsm=true", + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + got, err := New(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKeyVault_createClient(t *testing.T) { + type args struct { + ctx context.Context + opts apiv1.Options + } + tests := []struct { + name string + args args + skip bool + wantErr bool + }{ + {"ok", args{context.Background(), apiv1.Options{}}, true, false}, + {"ok with uri", args{context.Background(), apiv1.Options{ + URI: "azurekms:client-id=id;client-secret=secret;tenant-id=id", + }}, false, false}, + {"ok with uri+aad", args{context.Background(), apiv1.Options{ + URI: "azurekms:client-id=id;client-secret=secret;tenant-id=id;aad-enpoint=https%3A%2F%2Flogin.microsoftonline.us%2F", + }}, false, false}, + {"ok with uri no config", args{context.Background(), apiv1.Options{ + URI: "azurekms:", + }}, true, false}, + {"fail uri", args{context.Background(), apiv1.Options{ + URI: "kms:client-id=id;client-secret=secret;tenant-id=id", + }}, false, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.skip { + t.SkipNow() + } + _, err := createClient(tt.args.ctx, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestKeyVault_GetPublicKey(t *testing.T) { + key, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + pub := key.Public() + jwk := createJWK(t, pub) + + client := mockClient(t) + client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "").Return(keyvault.KeyBundle{ + Key: jwk, + }, nil) + client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "my-version").Return(keyvault.KeyBundle{ + Key: jwk, + }, nil) + client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", "my-version").Return(keyvault.KeyBundle{}, errTest) + + type fields struct { + baseClient KeyVaultClient + } + type args struct { + req *apiv1.GetPublicKeyRequest + } + tests := []struct { + name string + fields fields + args args + want crypto.PublicKey + wantErr bool + }{ + {"ok", fields{client}, args{&apiv1.GetPublicKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + }}, pub, false}, + {"ok with version", fields{client}, args{&apiv1.GetPublicKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key?version=my-version", + }}, pub, false}, + {"fail GetKey", fields{client}, args{&apiv1.GetPublicKeyRequest{ + Name: "azurekms:vault=my-vault;name=not-found?version=my-version", + }}, nil, true}, + {"fail empty", fields{client}, args{&apiv1.GetPublicKeyRequest{ + Name: "", + }}, nil, true}, + {"fail vault", fields{client}, args{&apiv1.GetPublicKeyRequest{ + Name: "azurekms:vault=;name=not-found?version=my-version", + }}, nil, true}, + {"fail id", fields{client}, args{&apiv1.GetPublicKeyRequest{ + Name: "azurekms:vault=;name=?version=my-version", + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &KeyVault{ + baseClient: tt.fields.baseClient, + } + got, err := k.GetPublicKey(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("KeyVault.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("KeyVault.GetPublicKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKeyVault_CreateKey(t *testing.T) { + ecKey, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + rsaKey, err := keyutil.GenerateSigner("RSA", "", 2048) + if err != nil { + t.Fatal(err) + } + ecPub := ecKey.Public() + rsaPub := rsaKey.Public() + ecJWK := createJWK(t, ecPub) + rsaJWK := createJWK(t, rsaPub) + + t0 := date.UnixTime(mockNow(t)) + client := mockClient(t) + + expects := []struct { + Name string + Kty keyvault.JSONWebKeyType + KeySize *int32 + Curve keyvault.JSONWebKeyCurveName + Key *keyvault.JSONWebKey + }{ + {"P-256", keyvault.EC, nil, keyvault.P256, ecJWK}, + {"P-256 HSM", keyvault.ECHSM, nil, keyvault.P256, ecJWK}, + {"P-256 HSM (uri)", keyvault.ECHSM, nil, keyvault.P256, ecJWK}, + {"P-256 Default", keyvault.EC, nil, keyvault.P256, ecJWK}, + {"P-384", keyvault.EC, nil, keyvault.P384, ecJWK}, + {"P-521", keyvault.EC, nil, keyvault.P521, ecJWK}, + {"RSA 0", keyvault.RSA, &value3072, "", rsaJWK}, + {"RSA 0 HSM", keyvault.RSAHSM, &value3072, "", rsaJWK}, + {"RSA 0 HSM (uri)", keyvault.RSAHSM, &value3072, "", rsaJWK}, + {"RSA 2048", keyvault.RSA, &value2048, "", rsaJWK}, + {"RSA 3072", keyvault.RSA, &value3072, "", rsaJWK}, + {"RSA 4096", keyvault.RSA, &value4096, "", rsaJWK}, + } + + for _, e := range expects { + client.EXPECT().CreateKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", keyvault.KeyCreateParameters{ + Kty: e.Kty, + KeySize: e.KeySize, + Curve: e.Curve, + KeyOps: &[]keyvault.JSONWebKeyOperation{ + keyvault.Sign, keyvault.Verify, + }, + KeyAttributes: &keyvault.KeyAttributes{ + Enabled: &valueTrue, + Created: &t0, + NotBefore: &t0, + }, + }).Return(keyvault.KeyBundle{ + Key: e.Key, + }, nil) + } + client.EXPECT().CreateKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", gomock.Any()).Return(keyvault.KeyBundle{}, errTest) + client.EXPECT().CreateKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", gomock.Any()).Return(keyvault.KeyBundle{ + Key: nil, + }, nil) + + type fields struct { + baseClient KeyVaultClient + } + type args struct { + req *apiv1.CreateKeyRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.CreateKeyResponse + wantErr bool + }{ + {"ok P-256", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + ProtectionLevel: apiv1.Software, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: ecPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok P-256 HSM", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + ProtectionLevel: apiv1.HSM, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: ecPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok P-256 HSM (uri)", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key?hsm=true", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: ecPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok P-256 Default", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: ecPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok P-384", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + SignatureAlgorithm: apiv1.ECDSAWithSHA384, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: ecPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok P-521", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + SignatureAlgorithm: apiv1.ECDSAWithSHA512, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: ecPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok RSA 0", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + Bits: 0, + SignatureAlgorithm: apiv1.SHA256WithRSA, + ProtectionLevel: apiv1.Software, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: rsaPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok RSA 0 HSM", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + Bits: 0, + SignatureAlgorithm: apiv1.SHA256WithRSAPSS, + ProtectionLevel: apiv1.HSM, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: rsaPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok RSA 0 HSM (uri)", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key;hsm=true", + Bits: 0, + SignatureAlgorithm: apiv1.SHA256WithRSAPSS, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: rsaPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok RSA 2048", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + Bits: 2048, + SignatureAlgorithm: apiv1.SHA384WithRSA, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: rsaPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok RSA 3072", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + Bits: 3072, + SignatureAlgorithm: apiv1.SHA512WithRSA, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: rsaPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"ok RSA 4096", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=my-key", + Bits: 4096, + SignatureAlgorithm: apiv1.SHA512WithRSAPSS, + }}, &apiv1.CreateKeyResponse{ + Name: "azurekms:name=my-key;vault=my-vault", + PublicKey: rsaPub, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: "azurekms:name=my-key;vault=my-vault", + }, + }, false}, + {"fail createKey", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=not-found", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }}, nil, true}, + {"fail convertKey", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=not-found", + SignatureAlgorithm: apiv1.ECDSAWithSHA256, + }}, nil, true}, + {"fail name", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "", + }}, nil, true}, + {"fail vault", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=;name=not-found?version=my-version", + }}, nil, true}, + {"fail id", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=?version=my-version", + }}, nil, true}, + {"fail SignatureAlgorithm", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=not-found", + SignatureAlgorithm: apiv1.PureEd25519, + }}, nil, true}, + {"fail bit size", fields{client}, args{&apiv1.CreateKeyRequest{ + Name: "azurekms:vault=my-vault;name=not-found", + SignatureAlgorithm: apiv1.SHA384WithRSAPSS, + Bits: 1024, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &KeyVault{ + baseClient: tt.fields.baseClient, + } + got, err := k.CreateKey(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("KeyVault.CreateKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("KeyVault.CreateKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKeyVault_CreateSigner(t *testing.T) { + key, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + pub := key.Public() + jwk := createJWK(t, pub) + + client := mockClient(t) + client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "").Return(keyvault.KeyBundle{ + Key: jwk, + }, nil) + client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "my-version").Return(keyvault.KeyBundle{ + Key: jwk, + }, nil) + client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", "my-version").Return(keyvault.KeyBundle{}, errTest) + + type fields struct { + baseClient KeyVaultClient + } + type args struct { + req *apiv1.CreateSignerRequest + } + tests := []struct { + name string + fields fields + args args + want crypto.Signer + wantErr bool + }{ + {"ok", fields{client}, args{&apiv1.CreateSignerRequest{ + SigningKey: "azurekms:vault=my-vault;name=my-key", + }}, &Signer{ + client: client, + vaultBaseURL: "https://my-vault.vault.azure.net/", + name: "my-key", + version: "", + publicKey: pub, + }, false}, + {"ok with version", fields{client}, args{&apiv1.CreateSignerRequest{ + SigningKey: "azurekms:vault=my-vault;name=my-key;version=my-version", + }}, &Signer{ + client: client, + vaultBaseURL: "https://my-vault.vault.azure.net/", + name: "my-key", + version: "my-version", + publicKey: pub, + }, false}, + {"fail GetKey", fields{client}, args{&apiv1.CreateSignerRequest{ + SigningKey: "azurekms:vault=my-vault;name=not-found;version=my-version", + }}, nil, true}, + {"fail SigningKey", fields{client}, args{&apiv1.CreateSignerRequest{ + SigningKey: "", + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &KeyVault{ + baseClient: tt.fields.baseClient, + } + got, err := k.CreateSigner(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("KeyVault.CreateSigner() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("KeyVault.CreateSigner() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKeyVault_Close(t *testing.T) { + client := mockClient(t) + type fields struct { + baseClient KeyVaultClient + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + {"ok", fields{client}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &KeyVault{ + baseClient: tt.fields.baseClient, + } + if err := k.Close(); (err != nil) != tt.wantErr { + t.Errorf("KeyVault.Close() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_keyType_KeyType(t *testing.T) { + type fields struct { + Kty keyvault.JSONWebKeyType + Curve keyvault.JSONWebKeyCurveName + } + type args struct { + pl apiv1.ProtectionLevel + } + tests := []struct { + name string + fields fields + args args + want keyvault.JSONWebKeyType + }{ + {"ec", fields{keyvault.EC, keyvault.P256}, args{apiv1.UnspecifiedProtectionLevel}, keyvault.EC}, + {"ec software", fields{keyvault.EC, keyvault.P384}, args{apiv1.Software}, keyvault.EC}, + {"ec hsm", fields{keyvault.EC, keyvault.P521}, args{apiv1.HSM}, keyvault.ECHSM}, + {"rsa", fields{keyvault.RSA, keyvault.P256}, args{apiv1.UnspecifiedProtectionLevel}, keyvault.RSA}, + {"rsa software", fields{keyvault.RSA, ""}, args{apiv1.Software}, keyvault.RSA}, + {"rsa hsm", fields{keyvault.RSA, ""}, args{apiv1.HSM}, keyvault.RSAHSM}, + {"empty", fields{"FOO", ""}, args{apiv1.UnspecifiedProtectionLevel}, ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := keyType{ + Kty: tt.fields.Kty, + Curve: tt.fields.Curve, + } + if got := k.KeyType(tt.args.pl); !reflect.DeepEqual(got, tt.want) { + t.Errorf("keyType.KeyType() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKeyVault_ValidateName(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"ok", args{"azurekms:name=my-key;vault=my-vault"}, false}, + {"ok hsm", args{"azurekms:name=my-key;vault=my-vault?hsm=true"}, false}, + {"fail scheme", args{"azure:name=my-key;vault=my-vault"}, true}, + {"fail parse uri", args{"azurekms:name=%ZZ;vault=my-vault"}, true}, + {"fail no name", args{"azurekms:vault=my-vault"}, true}, + {"fail no vault", args{"azurekms:name=my-key"}, true}, + {"fail empty", args{""}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := &KeyVault{} + if err := k.ValidateName(tt.args.s); (err != nil) != tt.wantErr { + t.Errorf("KeyVault.ValidateName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/kms/azurekms/signer.go b/kms/azurekms/signer.go new file mode 100644 index 00000000..b0349108 --- /dev/null +++ b/kms/azurekms/signer.go @@ -0,0 +1,182 @@ +package azurekms + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "encoding/base64" + "io" + "math/big" + "time" + + "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/pkg/errors" + "golang.org/x/crypto/cryptobyte" + "golang.org/x/crypto/cryptobyte/asn1" +) + +// Signer implements a crypto.Signer using the AWS KMS. +type Signer struct { + client KeyVaultClient + vaultBaseURL string + name string + version string + publicKey crypto.PublicKey +} + +// NewSigner creates a new signer using a key in the AWS KMS. +func NewSigner(client KeyVaultClient, signingKey string, defaults DefaultOptions) (crypto.Signer, error) { + vault, name, version, _, err := parseKeyName(signingKey, defaults) + if err != nil { + return nil, err + } + + // Make sure that the key exists. + signer := &Signer{ + client: client, + vaultBaseURL: vaultBaseURL(vault), + name: name, + version: version, + } + if err := signer.preloadKey(); err != nil { + return nil, err + } + + return signer, nil +} + +func (s *Signer) preloadKey() error { + ctx, cancel := defaultContext() + defer cancel() + + resp, err := s.client.GetKey(ctx, s.vaultBaseURL, s.name, s.version) + if err != nil { + return errors.Wrap(err, "keyVault GetKey failed") + } + + s.publicKey, err = convertKey(resp.Key) + return err +} + +// Public returns the public key of this signer or an error. +func (s *Signer) Public() crypto.PublicKey { + return s.publicKey +} + +// Sign signs digest with the private key stored in the AWS KMS. +func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + alg, err := getSigningAlgorithm(s.Public(), opts) + if err != nil { + return nil, err + } + + b64 := base64.RawURLEncoding.EncodeToString(digest) + + // Sign with retry if the key is not ready + resp, err := s.signWithRetry(alg, b64, 3) + if err != nil { + return nil, errors.Wrap(err, "keyVault Sign failed") + } + + sig, err := base64.RawURLEncoding.DecodeString(*resp.Result) + if err != nil { + return nil, errors.Wrap(err, "error decoding keyVault Sign result") + } + + var octetSize int + switch alg { + case keyvault.ES256: + octetSize = 32 // 256-bit, concat(R,S) = 64 bytes + case keyvault.ES384: + octetSize = 48 // 384-bit, concat(R,S) = 96 bytes + case keyvault.ES512: + octetSize = 66 // 528-bit, concat(R,S) = 132 bytes + default: + return sig, nil + } + + // Convert to asn1 + if len(sig) != octetSize*2 { + return nil, errors.Errorf("keyVault Sign failed: unexpected signature length") + } + var b cryptobyte.Builder + b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) { + b.AddASN1BigInt(new(big.Int).SetBytes(sig[:octetSize])) // R + b.AddASN1BigInt(new(big.Int).SetBytes(sig[octetSize:])) // S + }) + return b.Bytes() +} + +func (s *Signer) signWithRetry(alg keyvault.JSONWebKeySignatureAlgorithm, b64 string, retryAttempts int) (keyvault.KeyOperationResult, error) { +retry: + ctx, cancel := defaultContext() + defer cancel() + + resp, err := s.client.Sign(ctx, s.vaultBaseURL, s.name, s.version, keyvault.KeySignParameters{ + Algorithm: alg, + Value: &b64, + }) + if err != nil && retryAttempts > 0 { + var requestError *azure.RequestError + if errors.As(err, &requestError) { + if se := requestError.ServiceError; se != nil && se.InnerError != nil { + code, ok := se.InnerError["code"].(string) + if ok && code == "KeyNotYetValid" { + time.Sleep(time.Second / time.Duration(retryAttempts)) + retryAttempts-- + goto retry + } + } + } + } + return resp, err +} + +func getSigningAlgorithm(key crypto.PublicKey, opts crypto.SignerOpts) (keyvault.JSONWebKeySignatureAlgorithm, error) { + switch key.(type) { + case *rsa.PublicKey: + hashFunc := opts.HashFunc() + pss, isPSS := opts.(*rsa.PSSOptions) + // Random salt lengths are not supported + if isPSS && + pss.SaltLength != rsa.PSSSaltLengthAuto && + pss.SaltLength != rsa.PSSSaltLengthEqualsHash && + pss.SaltLength != hashFunc.Size() { + return "", errors.Errorf("unsupported RSA-PSS salt length %d", pss.SaltLength) + } + + switch h := hashFunc; h { + case crypto.SHA256: + if isPSS { + return keyvault.PS256, nil + } + return keyvault.RS256, nil + case crypto.SHA384: + if isPSS { + return keyvault.PS384, nil + } + return keyvault.RS384, nil + case crypto.SHA512: + if isPSS { + return keyvault.PS512, nil + } + return keyvault.RS512, nil + default: + return "", errors.Errorf("unsupported hash function %v", h) + } + case *ecdsa.PublicKey: + switch h := opts.HashFunc(); h { + case crypto.SHA256: + return keyvault.ES256, nil + case crypto.SHA384: + return keyvault.ES384, nil + case crypto.SHA512: + return keyvault.ES512, nil + default: + return "", errors.Errorf("unsupported hash function %v", h) + } + default: + return "", errors.Errorf("unsupported key type %T", key) + } +} diff --git a/kms/azurekms/signer_test.go b/kms/azurekms/signer_test.go new file mode 100644 index 00000000..bd072b25 --- /dev/null +++ b/kms/azurekms/signer_test.go @@ -0,0 +1,493 @@ +package azurekms + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "encoding/base64" + "io" + "reflect" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/golang/mock/gomock" + "github.com/smallstep/certificates/kms/apiv1" + "go.step.sm/crypto/keyutil" + "golang.org/x/crypto/cryptobyte" + "golang.org/x/crypto/cryptobyte/asn1" +) + +func TestNewSigner(t *testing.T) { + key, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + pub := key.Public() + jwk := createJWK(t, pub) + + client := mockClient(t) + client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "").Return(keyvault.KeyBundle{ + Key: jwk, + }, nil) + client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "my-version").Return(keyvault.KeyBundle{ + Key: jwk, + }, nil) + client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", "my-version").Return(keyvault.KeyBundle{ + Key: jwk, + }, nil) + client.EXPECT().GetKey(gomock.Any(), "https://my-vault.vault.azure.net/", "not-found", "my-version").Return(keyvault.KeyBundle{}, errTest) + + var noOptions DefaultOptions + type args struct { + client KeyVaultClient + signingKey string + defaults DefaultOptions + } + tests := []struct { + name string + args args + want crypto.Signer + wantErr bool + }{ + {"ok", args{client, "azurekms:vault=my-vault;name=my-key", noOptions}, &Signer{ + client: client, + vaultBaseURL: "https://my-vault.vault.azure.net/", + name: "my-key", + version: "", + publicKey: pub, + }, false}, + {"ok with version", args{client, "azurekms:name=my-key;vault=my-vault?version=my-version", noOptions}, &Signer{ + client: client, + vaultBaseURL: "https://my-vault.vault.azure.net/", + name: "my-key", + version: "my-version", + publicKey: pub, + }, false}, + {"ok with options", args{client, "azurekms:name=my-key?version=my-version", DefaultOptions{Vault: "my-vault", ProtectionLevel: apiv1.HSM}}, &Signer{ + client: client, + vaultBaseURL: "https://my-vault.vault.azure.net/", + name: "my-key", + version: "my-version", + publicKey: pub, + }, false}, + {"fail GetKey", args{client, "azurekms:name=not-found;vault=my-vault?version=my-version", noOptions}, nil, true}, + {"fail vault", args{client, "azurekms:name=not-found;vault=", noOptions}, nil, true}, + {"fail id", args{client, "azurekms:name=;vault=my-vault?version=my-version", noOptions}, nil, true}, + {"fail scheme", args{client, "kms:name=not-found;vault=my-vault?version=my-version", noOptions}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewSigner(tt.args.client, tt.args.signingKey, tt.args.defaults) + if (err != nil) != tt.wantErr { + t.Errorf("NewSigner() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewSigner() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSigner_Public(t *testing.T) { + key, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + pub := key.Public() + + type fields struct { + publicKey crypto.PublicKey + } + tests := []struct { + name string + fields fields + want crypto.PublicKey + }{ + {"ok", fields{pub}, pub}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Signer{ + publicKey: tt.fields.publicKey, + } + if got := s.Public(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Signer.Public() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSigner_Sign(t *testing.T) { + sign := func(kty, crv string, bits int, opts crypto.SignerOpts) (crypto.PublicKey, []byte, string, []byte) { + key, err := keyutil.GenerateSigner(kty, crv, bits) + if err != nil { + t.Fatal(err) + } + h := opts.HashFunc().New() + h.Write([]byte("random-data")) + sum := h.Sum(nil) + + var sig, resultSig []byte + if priv, ok := key.(*ecdsa.PrivateKey); ok { + r, s, err := ecdsa.Sign(rand.Reader, priv, sum) + if err != nil { + t.Fatal(err) + } + curveBits := priv.Params().BitSize + keyBytes := curveBits / 8 + if curveBits%8 > 0 { + keyBytes++ + } + rBytes := r.Bytes() + rBytesPadded := make([]byte, keyBytes) + copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) + + sBytes := s.Bytes() + sBytesPadded := make([]byte, keyBytes) + copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) + // nolint:gocritic + resultSig = append(rBytesPadded, sBytesPadded...) + + var b cryptobyte.Builder + b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) { + b.AddASN1BigInt(r) + b.AddASN1BigInt(s) + }) + sig, err = b.Bytes() + if err != nil { + t.Fatal(err) + } + } else { + sig, err = key.Sign(rand.Reader, sum, opts) + if err != nil { + t.Fatal(err) + } + resultSig = sig + } + + return key.Public(), h.Sum(nil), base64.RawURLEncoding.EncodeToString(resultSig), sig + } + + p256, p256Digest, p256ResultSig, p256Sig := sign("EC", "P-256", 0, crypto.SHA256) + p384, p384Digest, p386ResultSig, p384Sig := sign("EC", "P-384", 0, crypto.SHA384) + p521, p521Digest, p521ResultSig, p521Sig := sign("EC", "P-521", 0, crypto.SHA512) + rsaSHA256, rsaSHA256Digest, rsaSHA256ResultSig, rsaSHA256Sig := sign("RSA", "", 2048, crypto.SHA256) + rsaSHA384, rsaSHA384Digest, rsaSHA384ResultSig, rsaSHA384Sig := sign("RSA", "", 2048, crypto.SHA384) + rsaSHA512, rsaSHA512Digest, rsaSHA512ResultSig, rsaSHA512Sig := sign("RSA", "", 2048, crypto.SHA512) + rsaPSSSHA256, rsaPSSSHA256Digest, rsaPSSSHA256ResultSig, rsaPSSSHA256Sig := sign("RSA", "", 2048, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA256, + }) + rsaPSSSHA384, rsaPSSSHA384Digest, rsaPSSSHA384ResultSig, rsaPSSSHA384Sig := sign("RSA", "", 2048, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA512, + }) + rsaPSSSHA512, rsaPSSSHA512Digest, rsaPSSSHA512ResultSig, rsaPSSSHA512Sig := sign("RSA", "", 2048, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA512, + }) + + ed25519Key, err := keyutil.GenerateSigner("OKP", "Ed25519", 0) + if err != nil { + t.Fatal(err) + } + + client := mockClient(t) + expects := []struct { + name string + keyVersion string + alg keyvault.JSONWebKeySignatureAlgorithm + digest []byte + result keyvault.KeyOperationResult + err error + }{ + {"P-256", "", keyvault.ES256, p256Digest, keyvault.KeyOperationResult{ + Result: &p256ResultSig, + }, nil}, + {"P-384", "my-version", keyvault.ES384, p384Digest, keyvault.KeyOperationResult{ + Result: &p386ResultSig, + }, nil}, + {"P-521", "my-version", keyvault.ES512, p521Digest, keyvault.KeyOperationResult{ + Result: &p521ResultSig, + }, nil}, + {"RSA SHA256", "", keyvault.RS256, rsaSHA256Digest, keyvault.KeyOperationResult{ + Result: &rsaSHA256ResultSig, + }, nil}, + {"RSA SHA384", "", keyvault.RS384, rsaSHA384Digest, keyvault.KeyOperationResult{ + Result: &rsaSHA384ResultSig, + }, nil}, + {"RSA SHA512", "", keyvault.RS512, rsaSHA512Digest, keyvault.KeyOperationResult{ + Result: &rsaSHA512ResultSig, + }, nil}, + {"RSA-PSS SHA256", "", keyvault.PS256, rsaPSSSHA256Digest, keyvault.KeyOperationResult{ + Result: &rsaPSSSHA256ResultSig, + }, nil}, + {"RSA-PSS SHA384", "", keyvault.PS384, rsaPSSSHA384Digest, keyvault.KeyOperationResult{ + Result: &rsaPSSSHA384ResultSig, + }, nil}, + {"RSA-PSS SHA512", "", keyvault.PS512, rsaPSSSHA512Digest, keyvault.KeyOperationResult{ + Result: &rsaPSSSHA512ResultSig, + }, nil}, + // Errors + {"fail Sign", "", keyvault.RS256, rsaSHA256Digest, keyvault.KeyOperationResult{}, errTest}, + {"fail sign length", "", keyvault.ES256, p256Digest, keyvault.KeyOperationResult{ + Result: &rsaSHA256ResultSig, + }, nil}, + {"fail base64", "", keyvault.ES256, p256Digest, keyvault.KeyOperationResult{ + Result: func() *string { + v := "😎" + return &v + }(), + }, nil}, + } + for _, e := range expects { + value := base64.RawURLEncoding.EncodeToString(e.digest) + client.EXPECT().Sign(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", e.keyVersion, keyvault.KeySignParameters{ + Algorithm: e.alg, + Value: &value, + }).Return(e.result, e.err) + } + + type fields struct { + client KeyVaultClient + vaultBaseURL string + name string + version string + publicKey crypto.PublicKey + } + type args struct { + rand io.Reader + digest []byte + opts crypto.SignerOpts + } + tests := []struct { + name string + fields fields + args args + want []byte + wantErr bool + }{ + {"ok P-256", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", p256}, args{ + rand.Reader, p256Digest, crypto.SHA256, + }, p256Sig, false}, + {"ok P-384", fields{client, "https://my-vault.vault.azure.net/", "my-key", "my-version", p384}, args{ + rand.Reader, p384Digest, crypto.SHA384, + }, p384Sig, false}, + {"ok P-521", fields{client, "https://my-vault.vault.azure.net/", "my-key", "my-version", p521}, args{ + rand.Reader, p521Digest, crypto.SHA512, + }, p521Sig, false}, + {"ok RSA SHA256", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaSHA256}, args{ + rand.Reader, rsaSHA256Digest, crypto.SHA256, + }, rsaSHA256Sig, false}, + {"ok RSA SHA384", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaSHA384}, args{ + rand.Reader, rsaSHA384Digest, crypto.SHA384, + }, rsaSHA384Sig, false}, + {"ok RSA SHA512", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaSHA512}, args{ + rand.Reader, rsaSHA512Digest, crypto.SHA512, + }, rsaSHA512Sig, false}, + {"ok RSA-PSS SHA256", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaPSSSHA256}, args{ + rand.Reader, rsaPSSSHA256Digest, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: crypto.SHA256, + }, + }, rsaPSSSHA256Sig, false}, + {"ok RSA-PSS SHA384", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaPSSSHA384}, args{ + rand.Reader, rsaPSSSHA384Digest, &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthEqualsHash, + Hash: crypto.SHA384, + }, + }, rsaPSSSHA384Sig, false}, + {"ok RSA-PSS SHA512", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaPSSSHA512}, args{ + rand.Reader, rsaPSSSHA512Digest, &rsa.PSSOptions{ + SaltLength: 64, + Hash: crypto.SHA512, + }, + }, rsaPSSSHA512Sig, false}, + {"fail Sign", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaSHA256}, args{ + rand.Reader, rsaSHA256Digest, crypto.SHA256, + }, nil, true}, + {"fail sign length", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", p256}, args{ + rand.Reader, p256Digest, crypto.SHA256, + }, nil, true}, + {"fail base64", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", p256}, args{ + rand.Reader, p256Digest, crypto.SHA256, + }, nil, true}, + {"fail RSA-PSS salt length", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaPSSSHA256}, args{ + rand.Reader, rsaPSSSHA256Digest, &rsa.PSSOptions{ + SaltLength: 64, + Hash: crypto.SHA256, + }, + }, nil, true}, + {"fail RSA Hash", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", rsaSHA256}, args{ + rand.Reader, rsaSHA256Digest, crypto.SHA1, + }, nil, true}, + {"fail ECDSA Hash", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", p256}, args{ + rand.Reader, p256Digest, crypto.MD5, + }, nil, true}, + {"fail Ed25519", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", ed25519Key}, args{ + rand.Reader, []byte("message"), crypto.Hash(0), + }, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Signer{ + client: tt.fields.client, + vaultBaseURL: tt.fields.vaultBaseURL, + name: tt.fields.name, + version: tt.fields.version, + publicKey: tt.fields.publicKey, + } + got, err := s.Sign(tt.args.rand, tt.args.digest, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("Signer.Sign() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Signer.Sign() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSigner_Sign_signWithRetry(t *testing.T) { + sign := func(kty, crv string, bits int, opts crypto.SignerOpts) (crypto.PublicKey, []byte, string, []byte) { + key, err := keyutil.GenerateSigner(kty, crv, bits) + if err != nil { + t.Fatal(err) + } + h := opts.HashFunc().New() + h.Write([]byte("random-data")) + sum := h.Sum(nil) + + var sig, resultSig []byte + if priv, ok := key.(*ecdsa.PrivateKey); ok { + r, s, err := ecdsa.Sign(rand.Reader, priv, sum) + if err != nil { + t.Fatal(err) + } + curveBits := priv.Params().BitSize + keyBytes := curveBits / 8 + if curveBits%8 > 0 { + keyBytes++ + } + rBytes := r.Bytes() + rBytesPadded := make([]byte, keyBytes) + copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) + + sBytes := s.Bytes() + sBytesPadded := make([]byte, keyBytes) + copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) + // nolint:gocritic + resultSig = append(rBytesPadded, sBytesPadded...) + + var b cryptobyte.Builder + b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) { + b.AddASN1BigInt(r) + b.AddASN1BigInt(s) + }) + sig, err = b.Bytes() + if err != nil { + t.Fatal(err) + } + } else { + sig, err = key.Sign(rand.Reader, sum, opts) + if err != nil { + t.Fatal(err) + } + resultSig = sig + } + + return key.Public(), h.Sum(nil), base64.RawURLEncoding.EncodeToString(resultSig), sig + } + + p256, p256Digest, p256ResultSig, p256Sig := sign("EC", "P-256", 0, crypto.SHA256) + okResult := keyvault.KeyOperationResult{ + Result: &p256ResultSig, + } + failResult := keyvault.KeyOperationResult{} + retryError := autorest.DetailedError{ + Original: &azure.RequestError{ + ServiceError: &azure.ServiceError{ + InnerError: map[string]interface{}{ + "code": "KeyNotYetValid", + }, + }, + }, + } + + client := mockClient(t) + expects := []struct { + name string + keyVersion string + alg keyvault.JSONWebKeySignatureAlgorithm + digest []byte + result keyvault.KeyOperationResult + err error + }{ + {"ok 1", "", keyvault.ES256, p256Digest, failResult, retryError}, + {"ok 2", "", keyvault.ES256, p256Digest, failResult, retryError}, + {"ok 3", "", keyvault.ES256, p256Digest, failResult, retryError}, + {"ok 4", "", keyvault.ES256, p256Digest, okResult, nil}, + {"fail", "fail-version", keyvault.ES256, p256Digest, failResult, retryError}, + {"fail", "fail-version", keyvault.ES256, p256Digest, failResult, retryError}, + {"fail", "fail-version", keyvault.ES256, p256Digest, failResult, retryError}, + {"fail", "fail-version", keyvault.ES256, p256Digest, failResult, retryError}, + } + for _, e := range expects { + value := base64.RawURLEncoding.EncodeToString(e.digest) + client.EXPECT().Sign(gomock.Any(), "https://my-vault.vault.azure.net/", "my-key", e.keyVersion, keyvault.KeySignParameters{ + Algorithm: e.alg, + Value: &value, + }).Return(e.result, e.err) + } + + type fields struct { + client KeyVaultClient + vaultBaseURL string + name string + version string + publicKey crypto.PublicKey + } + type args struct { + rand io.Reader + digest []byte + opts crypto.SignerOpts + } + tests := []struct { + name string + fields fields + args args + want []byte + wantErr bool + }{ + {"ok", fields{client, "https://my-vault.vault.azure.net/", "my-key", "", p256}, args{ + rand.Reader, p256Digest, crypto.SHA256, + }, p256Sig, false}, + {"fail", fields{client, "https://my-vault.vault.azure.net/", "my-key", "fail-version", p256}, args{ + rand.Reader, p256Digest, crypto.SHA256, + }, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Signer{ + client: tt.fields.client, + vaultBaseURL: tt.fields.vaultBaseURL, + name: tt.fields.name, + version: tt.fields.version, + publicKey: tt.fields.publicKey, + } + got, err := s.Sign(tt.args.rand, tt.args.digest, tt.args.opts) + if (err != nil) != tt.wantErr { + t.Errorf("Signer.Sign() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Signer.Sign() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kms/azurekms/utils.go b/kms/azurekms/utils.go new file mode 100644 index 00000000..d4201907 --- /dev/null +++ b/kms/azurekms/utils.go @@ -0,0 +1,98 @@ +package azurekms + +import ( + "context" + "crypto" + "encoding/json" + "net/url" + "time" + + "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + "github.com/pkg/errors" + "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/certificates/kms/uri" + "go.step.sm/crypto/jose" +) + +// defaultContext returns the default context used in requests to azure. +func defaultContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), 15*time.Second) +} + +// getKeyName returns the uri of the key vault key. +func getKeyName(vault, name string, bundle keyvault.KeyBundle) string { + if bundle.Key != nil && bundle.Key.Kid != nil { + sm := keyIDRegexp.FindAllStringSubmatch(*bundle.Key.Kid, 1) + if len(sm) == 1 && len(sm[0]) == 4 { + m := sm[0] + u := uri.New(Scheme, url.Values{ + "vault": []string{m[1]}, + "name": []string{m[2]}, + }) + u.RawQuery = url.Values{"version": []string{m[3]}}.Encode() + return u.String() + } + } + // Fallback to URI without id. + return uri.New(Scheme, url.Values{ + "vault": []string{vault}, + "name": []string{name}, + }).String() +} + +// parseKeyName returns the key vault, name and version from URIs like: +// +// - azurekms:vault=key-vault;name=key-name +// - azurekms:vault=key-vault;name=key-name?version=key-id +// - azurekms:vault=key-vault;name=key-name?version=key-id&hsm=true +// +// The key-id defines the version of the key, if it is not passed the latest +// version will be used. +// +// HSM can also be passed to define the protection level if this is not given in +// CreateQuery. +func parseKeyName(rawURI string, defaults DefaultOptions) (vault, name, version string, hsm bool, err error) { + var u *uri.URI + + u, err = uri.ParseWithScheme(Scheme, rawURI) + if err != nil { + return + } + if name = u.Get("name"); name == "" { + err = errors.Errorf("key uri %s is not valid: name is missing", rawURI) + return + } + if vault = u.Get("vault"); vault == "" { + if defaults.Vault == "" { + name = "" + err = errors.Errorf("key uri %s is not valid: vault is missing", rawURI) + return + } + vault = defaults.Vault + } + if u.Get("hsm") == "" { + hsm = (defaults.ProtectionLevel == apiv1.HSM) + } else { + hsm = u.GetBool("hsm") + } + + version = u.Get("version") + + return +} + +func vaultBaseURL(vault string) string { + return "https://" + vault + ".vault.azure.net/" +} + +func convertKey(key *keyvault.JSONWebKey) (crypto.PublicKey, error) { + b, err := json.Marshal(key) + if err != nil { + return nil, errors.Wrap(err, "error marshaling key") + } + var jwk jose.JSONWebKey + if err := jwk.UnmarshalJSON(b); err != nil { + return nil, errors.Wrap(err, "error unmarshaling key") + } + return jwk.Key, nil +} diff --git a/kms/azurekms/utils_test.go b/kms/azurekms/utils_test.go new file mode 100644 index 00000000..cded50ea --- /dev/null +++ b/kms/azurekms/utils_test.go @@ -0,0 +1,96 @@ +package azurekms + +import ( + "testing" + + "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault" + "github.com/smallstep/certificates/kms/apiv1" +) + +func Test_getKeyName(t *testing.T) { + getBundle := func(kid string) keyvault.KeyBundle { + return keyvault.KeyBundle{ + Key: &keyvault.JSONWebKey{ + Kid: &kid, + }, + } + } + + type args struct { + vault string + name string + bundle keyvault.KeyBundle + } + tests := []struct { + name string + args args + want string + }{ + {"ok", args{"my-vault", "my-key", getBundle("https://my-vault.vault.azure.net/keys/my-key/my-version")}, "azurekms:name=my-key;vault=my-vault?version=my-version"}, + {"ok default", args{"my-vault", "my-key", getBundle("https://my-vault.foo.net/keys/my-key/my-version")}, "azurekms:name=my-key;vault=my-vault"}, + {"ok too short", args{"my-vault", "my-key", getBundle("https://my-vault.vault.azure.net/keys/my-version")}, "azurekms:name=my-key;vault=my-vault"}, + {"ok too long", args{"my-vault", "my-key", getBundle("https://my-vault.vault.azure.net/keys/my-key/my-version/sign")}, "azurekms:name=my-key;vault=my-vault"}, + {"ok nil key", args{"my-vault", "my-key", keyvault.KeyBundle{}}, "azurekms:name=my-key;vault=my-vault"}, + {"ok nil kid", args{"my-vault", "my-key", keyvault.KeyBundle{Key: &keyvault.JSONWebKey{}}}, "azurekms:name=my-key;vault=my-vault"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getKeyName(tt.args.vault, tt.args.name, tt.args.bundle); got != tt.want { + t.Errorf("getKeyName() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseKeyName(t *testing.T) { + var noOptions DefaultOptions + type args struct { + rawURI string + defaults DefaultOptions + } + tests := []struct { + name string + args args + wantVault string + wantName string + wantVersion string + wantHsm bool + wantErr bool + }{ + {"ok", args{"azurekms:name=my-key;vault=my-vault?version=my-version", noOptions}, "my-vault", "my-key", "my-version", false, false}, + {"ok opaque version", args{"azurekms:name=my-key;vault=my-vault;version=my-version", noOptions}, "my-vault", "my-key", "my-version", false, false}, + {"ok no version", args{"azurekms:name=my-key;vault=my-vault", noOptions}, "my-vault", "my-key", "", false, false}, + {"ok hsm", args{"azurekms:name=my-key;vault=my-vault?hsm=true", noOptions}, "my-vault", "my-key", "", true, false}, + {"ok hsm false", args{"azurekms:name=my-key;vault=my-vault?hsm=false", noOptions}, "my-vault", "my-key", "", false, false}, + {"ok default vault", args{"azurekms:name=my-key?version=my-version", DefaultOptions{Vault: "my-vault"}}, "my-vault", "my-key", "my-version", false, false}, + {"ok default hsm", args{"azurekms:name=my-key;vault=my-vault?version=my-version", DefaultOptions{Vault: "other-vault", ProtectionLevel: apiv1.HSM}}, "my-vault", "my-key", "my-version", true, false}, + {"fail scheme", args{"azure:name=my-key;vault=my-vault", noOptions}, "", "", "", false, true}, + {"fail parse uri", args{"azurekms:name=%ZZ;vault=my-vault", noOptions}, "", "", "", false, true}, + {"fail no name", args{"azurekms:vault=my-vault", noOptions}, "", "", "", false, true}, + {"fail empty name", args{"azurekms:name=;vault=my-vault", noOptions}, "", "", "", false, true}, + {"fail no vault", args{"azurekms:name=my-key", noOptions}, "", "", "", false, true}, + {"fail empty vault", args{"azurekms:name=my-key;vault=", noOptions}, "", "", "", false, true}, + {"fail empty", args{"", noOptions}, "", "", "", false, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotVault, gotName, gotVersion, gotHsm, err := parseKeyName(tt.args.rawURI, tt.args.defaults) + if (err != nil) != tt.wantErr { + t.Errorf("parseKeyName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotVault != tt.wantVault { + t.Errorf("parseKeyName() gotVault = %v, want %v", gotVault, tt.wantVault) + } + if gotName != tt.wantName { + t.Errorf("parseKeyName() gotName = %v, want %v", gotName, tt.wantName) + } + if gotVersion != tt.wantVersion { + t.Errorf("parseKeyName() gotVersion = %v, want %v", gotVersion, tt.wantVersion) + } + if gotHsm != tt.wantHsm { + t.Errorf("parseKeyName() gotHsm = %v, want %v", gotHsm, tt.wantHsm) + } + }) + } +} diff --git a/kms/cloudkms/cloudkms.go b/kms/cloudkms/cloudkms.go index cfbf8235..65d06048 100644 --- a/kms/cloudkms/cloudkms.go +++ b/kms/cloudkms/cloudkms.go @@ -3,6 +3,7 @@ package cloudkms import ( "context" "crypto" + "crypto/x509" "log" "strings" "time" @@ -46,8 +47,8 @@ var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{ 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, }, apiv1.SHA512WithRSA: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ - 0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, - 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256, + 0: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512, + 4096: kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512, }, apiv1.SHA256WithRSAPSS: map[int]kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm{ 0: kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256, @@ -63,6 +64,19 @@ var signatureAlgorithmMapping = map[apiv1.SignatureAlgorithm]interface{}{ apiv1.ECDSAWithSHA384: kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384, } +var cryptoKeyVersionMapping = map[kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm]x509.SignatureAlgorithm{ + kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256: x509.ECDSAWithSHA256, + kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384: x509.ECDSAWithSHA384, + kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256: x509.SHA256WithRSA, + kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256: x509.SHA256WithRSA, + kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256: x509.SHA256WithRSA, + kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512: x509.SHA512WithRSA, + kmspb.CryptoKeyVersion_RSA_SIGN_PSS_2048_SHA256: x509.SHA256WithRSAPSS, + kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256: x509.SHA256WithRSAPSS, + kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA256: x509.SHA256WithRSAPSS, + kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512: x509.SHA512WithRSAPSS, +} + // KeyManagementClient defines the methods on KeyManagementClient that this // package will use. This interface will be used for unit testing. type KeyManagementClient interface { diff --git a/kms/cloudkms/signer.go b/kms/cloudkms/signer.go index 686aca25..5a5443cf 100644 --- a/kms/cloudkms/signer.go +++ b/kms/cloudkms/signer.go @@ -2,6 +2,7 @@ package cloudkms import ( "crypto" + "crypto/x509" "io" "github.com/pkg/errors" @@ -13,6 +14,7 @@ import ( type Signer struct { client KeyManagementClient signingKey string + algorithm x509.SignatureAlgorithm publicKey crypto.PublicKey } @@ -40,7 +42,7 @@ func (s *Signer) preloadKey(signingKey string) error { if err != nil { return errors.Wrap(err, "cloudKMS GetPublicKey failed") } - + s.algorithm = cryptoKeyVersionMapping[response.Algorithm] s.publicKey, err = pemutil.ParseKey([]byte(response.Pem)) return err } @@ -84,3 +86,10 @@ func (s *Signer) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([] return response.Signature, nil } + +// SignatureAlgorithm returns the algorithm that must be specified in a +// certificate to sign. This is specially important to distinguish RSA and +// RSAPSS schemas. +func (s *Signer) SignatureAlgorithm() x509.SignatureAlgorithm { + return s.algorithm +} diff --git a/kms/cloudkms/signer_test.go b/kms/cloudkms/signer_test.go index fa730fe3..a8f964f1 100644 --- a/kms/cloudkms/signer_test.go +++ b/kms/cloudkms/signer_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto" "crypto/rand" + "crypto/x509" "fmt" "io" "io/ioutil" @@ -156,3 +157,79 @@ func Test_signer_Sign(t *testing.T) { }) } } + +func TestSigner_SignatureAlgorithm(t *testing.T) { + pemBytes, err := ioutil.ReadFile("testdata/pub.pem") + if err != nil { + t.Fatal(err) + } + + client := &MockClient{ + getPublicKey: func(_ context.Context, req *kmspb.GetPublicKeyRequest, _ ...gax.CallOption) (*kmspb.PublicKey, error) { + var algorithm kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm + switch req.Name { + case "ECDSA-SHA256": + algorithm = kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256 + case "ECDSA-SHA384": + algorithm = kmspb.CryptoKeyVersion_EC_SIGN_P384_SHA384 + case "SHA256-RSA-2048": + algorithm = kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256 + case "SHA256-RSA-3072": + algorithm = kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256 + case "SHA256-RSA-4096": + algorithm = kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256 + case "SHA512-RSA-4096": + algorithm = kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512 + case "SHA256-RSAPSS-2048": + algorithm = kmspb.CryptoKeyVersion_RSA_SIGN_PSS_2048_SHA256 + case "SHA256-RSAPSS-3072": + algorithm = kmspb.CryptoKeyVersion_RSA_SIGN_PSS_3072_SHA256 + case "SHA256-RSAPSS-4096": + algorithm = kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA256 + case "SHA512-RSAPSS-4096": + algorithm = kmspb.CryptoKeyVersion_RSA_SIGN_PSS_4096_SHA512 + } + return &kmspb.PublicKey{ + Pem: string(pemBytes), + Algorithm: algorithm, + }, nil + }, + } + + if err != nil { + t.Fatal(err) + } + + type fields struct { + client KeyManagementClient + signingKey string + } + tests := []struct { + name string + fields fields + want x509.SignatureAlgorithm + }{ + {"ECDSA-SHA256", fields{client, "ECDSA-SHA256"}, x509.ECDSAWithSHA256}, + {"ECDSA-SHA384", fields{client, "ECDSA-SHA384"}, x509.ECDSAWithSHA384}, + {"SHA256-RSA-2048", fields{client, "SHA256-RSA-2048"}, x509.SHA256WithRSA}, + {"SHA256-RSA-3072", fields{client, "SHA256-RSA-3072"}, x509.SHA256WithRSA}, + {"SHA256-RSA-4096", fields{client, "SHA256-RSA-4096"}, x509.SHA256WithRSA}, + {"SHA512-RSA-4096", fields{client, "SHA512-RSA-4096"}, x509.SHA512WithRSA}, + {"SHA256-RSAPSS-2048", fields{client, "SHA256-RSAPSS-2048"}, x509.SHA256WithRSAPSS}, + {"SHA256-RSAPSS-3072", fields{client, "SHA256-RSAPSS-3072"}, x509.SHA256WithRSAPSS}, + {"SHA256-RSAPSS-4096", fields{client, "SHA256-RSAPSS-4096"}, x509.SHA256WithRSAPSS}, + {"SHA512-RSAPSS-4096", fields{client, "SHA512-RSAPSS-4096"}, x509.SHA512WithRSAPSS}, + {"unknown", fields{client, "UNKNOWN"}, x509.UnknownSignatureAlgorithm}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + signer, err := NewSigner(tt.fields.client, tt.fields.signingKey) + if err != nil { + t.Errorf("NewSigner() error = %v", err) + } + if got := signer.SignatureAlgorithm(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Signer.SignatureAlgorithm() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kms/kms.go b/kms/kms.go index 3eddca93..92b544df 100644 --- a/kms/kms.go +++ b/kms/kms.go @@ -8,7 +8,7 @@ import ( "github.com/smallstep/certificates/kms/apiv1" // Enable default implementation - _ "github.com/smallstep/certificates/kms/softkms" + "github.com/smallstep/certificates/kms/softkms" ) // KeyManager is the interface implemented by all the KMS. @@ -18,6 +18,12 @@ type KeyManager = apiv1.KeyManager // store x509.Certificates. type CertificateManager = apiv1.CertificateManager +// Options are the KMS options. They represent the kms object in the ca.json. +type Options = apiv1.Options + +// Default is the implementation of the default KMS. +var Default = &softkms.SoftKMS{} + // New initializes a new KMS from the given type. func New(ctx context.Context, opts apiv1.Options) (KeyManager, error) { if err := opts.Validate(); err != nil { diff --git a/kms/pkcs11/benchmark_test.go b/kms/pkcs11/benchmark_test.go index 30e21117..c567872f 100644 --- a/kms/pkcs11/benchmark_test.go +++ b/kms/pkcs11/benchmark_test.go @@ -1,3 +1,4 @@ +//go:build cgo // +build cgo package pkcs11 diff --git a/kms/pkcs11/opensc_test.go b/kms/pkcs11/opensc_test.go index f3b61932..b365e614 100644 --- a/kms/pkcs11/opensc_test.go +++ b/kms/pkcs11/opensc_test.go @@ -1,3 +1,4 @@ +//go:build opensc // +build opensc package pkcs11 diff --git a/kms/pkcs11/other_test.go b/kms/pkcs11/other_test.go index 835587f7..9f4ab4a8 100644 --- a/kms/pkcs11/other_test.go +++ b/kms/pkcs11/other_test.go @@ -1,3 +1,4 @@ +//go:build cgo && !softhsm2 && !yubihsm2 && !opensc // +build cgo,!softhsm2,!yubihsm2,!opensc package pkcs11 @@ -79,10 +80,23 @@ func (s *stubPKCS11) FindCertificate(id, label []byte, serial *big.Int) (*x509.C } +func (s *stubPKCS11) ImportCertificateWithAttributes(template crypto11.AttributeSet, cert *x509.Certificate) error { + var id, label []byte + if v := template[crypto11.CkaId]; v != nil { + id = v.Value + } + if v := template[crypto11.CkaLabel]; v != nil { + label = v.Value + } + return s.ImportCertificateWithLabel(id, label, cert) +} + func (s *stubPKCS11) ImportCertificateWithLabel(id, label []byte, cert *x509.Certificate) error { switch { - case id == nil && label == nil: - return errors.New("id and label cannot both be nil") + case id == nil: + return errors.New("id cannot both be nil") + case label == nil: + return errors.New("label cannot both be nil") case cert == nil: return errors.New("certificate cannot be nil") } @@ -110,6 +124,17 @@ func (s *stubPKCS11) DeleteCertificate(id, label []byte, serial *big.Int) error return nil } +func (s *stubPKCS11) GenerateRSAKeyPairWithAttributes(public, private crypto11.AttributeSet, bits int) (crypto11.SignerDecrypter, error) { + var id, label []byte + if v := public[crypto11.CkaId]; v != nil { + id = v.Value + } + if v := public[crypto11.CkaLabel]; v != nil { + label = v.Value + } + return s.GenerateRSAKeyPairWithLabel(id, label, bits) +} + func (s *stubPKCS11) GenerateRSAKeyPairWithLabel(id, label []byte, bits int) (crypto11.SignerDecrypter, error) { if id == nil && label == nil { return nil, errors.New("id and label cannot both be nil") @@ -130,6 +155,17 @@ func (s *stubPKCS11) GenerateRSAKeyPairWithLabel(id, label []byte, bits int) (cr return k, nil } +func (s *stubPKCS11) GenerateECDSAKeyPairWithAttributes(public, private crypto11.AttributeSet, curve elliptic.Curve) (crypto11.Signer, error) { + var id, label []byte + if v := public[crypto11.CkaId]; v != nil { + id = v.Value + } + if v := public[crypto11.CkaLabel]; v != nil { + label = v.Value + } + return s.GenerateECDSAKeyPairWithLabel(id, label, curve) +} + func (s *stubPKCS11) GenerateECDSAKeyPairWithLabel(id, label []byte, curve elliptic.Curve) (crypto11.Signer, error) { if id == nil && label == nil { return nil, errors.New("id and label cannot both be nil") @@ -165,10 +201,10 @@ func (s *privateKey) Delete() error { return nil } -func (s *privateKey) Decrypt(rand io.Reader, msg []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) { +func (s *privateKey) Decrypt(rnd io.Reader, msg []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) { k, ok := s.Signer.(*rsa.PrivateKey) if !ok { return nil, errors.New("key is not an rsa key") } - return k.Decrypt(rand, msg, opts) + return k.Decrypt(rnd, msg, opts) } diff --git a/kms/pkcs11/pkcs11.go b/kms/pkcs11/pkcs11.go index 47c298a5..cec05d33 100644 --- a/kms/pkcs11/pkcs11.go +++ b/kms/pkcs11/pkcs11.go @@ -1,3 +1,4 @@ +//go:build cgo // +build cgo package pkcs11 @@ -31,10 +32,10 @@ const DefaultRSASize = 3072 type P11 interface { FindKeyPair(id, label []byte) (crypto11.Signer, error) FindCertificate(id, label []byte, serial *big.Int) (*x509.Certificate, error) - ImportCertificateWithLabel(id, label []byte, cert *x509.Certificate) error + ImportCertificateWithAttributes(template crypto11.AttributeSet, certificate *x509.Certificate) error DeleteCertificate(id, label []byte, serial *big.Int) error - GenerateRSAKeyPairWithLabel(id, label []byte, bits int) (crypto11.SignerDecrypter, error) - GenerateECDSAKeyPairWithLabel(id, label []byte, curve elliptic.Curve) (crypto11.Signer, error) + GenerateRSAKeyPairWithAttributes(public, private crypto11.AttributeSet, bits int) (crypto11.SignerDecrypter, error) + GenerateECDSAKeyPairWithAttributes(public, private crypto11.AttributeSet, curve elliptic.Curve) (crypto11.Signer, error) Close() error } @@ -144,8 +145,7 @@ func (k *PKCS11) CreateKey(req *apiv1.CreateKeyRequest) (*apiv1.CreateKeyRespons // CreateSigner creates a signer using the key present in the PKCS#11 MODULE signature // slot. func (k *PKCS11) CreateSigner(req *apiv1.CreateSignerRequest) (crypto.Signer, error) { - switch { - case req.SigningKey == "": + if req.SigningKey == "" { return nil, errors.New("createSignerRequest 'signingKey' cannot be empty") } @@ -185,6 +185,12 @@ func (k *PKCS11) StoreCertificate(req *apiv1.StoreCertificateRequest) error { return errors.Wrap(err, "storeCertificate failed") } + // Enforce the use of both id and labels. This is not strictly necessary in + // PKCS #11, but it's a good practice. + if len(id) == 0 || len(object) == 0 { + return errors.Errorf("key with uri %s is not valid, id and object are required", req.Name) + } + cert, err := k.p11.FindCertificate(id, object, nil) if err != nil { return errors.Wrap(err, "storeCertificate failed") @@ -195,7 +201,15 @@ func (k *PKCS11) StoreCertificate(req *apiv1.StoreCertificateRequest) error { }, "storeCertificate failed") } - if err := k.p11.ImportCertificateWithLabel(id, object, req.Certificate); err != nil { + // Import certificate with the necessary attributes. + template, err := crypto11.NewAttributeSetWithIDAndLabel(id, object) + if err != nil { + return errors.Wrap(err, "storeCertificate failed") + } + if req.Extractable { + template.Set(crypto11.CkaExtractable, true) + } + if err := k.p11.ImportCertificateWithAttributes(template, req.Certificate); err != nil { return errors.Wrap(err, "storeCertificate failed") } @@ -203,8 +217,8 @@ func (k *PKCS11) StoreCertificate(req *apiv1.StoreCertificateRequest) error { } // DeleteKey is a utility function to delete a key given an uri. -func (k *PKCS11) DeleteKey(uri string) error { - id, object, err := parseObject(uri) +func (k *PKCS11) DeleteKey(u string) error { + id, object, err := parseObject(u) if err != nil { return errors.Wrap(err, "deleteKey failed") } @@ -222,8 +236,8 @@ func (k *PKCS11) DeleteKey(uri string) error { } // DeleteCertificate is a utility function to delete a certificate given an uri. -func (k *PKCS11) DeleteCertificate(uri string) error { - id, object, err := parseObject(uri) +func (k *PKCS11) DeleteCertificate(u string) error { + id, object, err := parseObject(u) if err != nil { return errors.Wrap(err, "deleteCertificate failed") } @@ -284,6 +298,16 @@ func generateKey(ctx P11, req *apiv1.CreateKeyRequest) (crypto11.Signer, error) return nil, errors.Errorf("key with uri %s is not valid, id and object are required", req.Name) } + // Create template for public and private keys + public, err := crypto11.NewAttributeSetWithIDAndLabel(id, object) + if err != nil { + return nil, err + } + private := public.Copy() + if req.Extractable { + private.Set(crypto11.CkaExtractable, true) + } + bits := req.Bits if bits == 0 { bits = DefaultRSASize @@ -291,17 +315,17 @@ func generateKey(ctx P11, req *apiv1.CreateKeyRequest) (crypto11.Signer, error) switch req.SignatureAlgorithm { case apiv1.UnspecifiedSignAlgorithm: - return ctx.GenerateECDSAKeyPairWithLabel(id, object, elliptic.P256()) + return ctx.GenerateECDSAKeyPairWithAttributes(public, private, elliptic.P256()) case apiv1.SHA256WithRSA, apiv1.SHA384WithRSA, apiv1.SHA512WithRSA: - return ctx.GenerateRSAKeyPairWithLabel(id, object, bits) + return ctx.GenerateRSAKeyPairWithAttributes(public, private, bits) case apiv1.SHA256WithRSAPSS, apiv1.SHA384WithRSAPSS, apiv1.SHA512WithRSAPSS: - return ctx.GenerateRSAKeyPairWithLabel(id, object, bits) + return ctx.GenerateRSAKeyPairWithAttributes(public, private, bits) case apiv1.ECDSAWithSHA256: - return ctx.GenerateECDSAKeyPairWithLabel(id, object, elliptic.P256()) + return ctx.GenerateECDSAKeyPairWithAttributes(public, private, elliptic.P256()) case apiv1.ECDSAWithSHA384: - return ctx.GenerateECDSAKeyPairWithLabel(id, object, elliptic.P384()) + return ctx.GenerateECDSAKeyPairWithAttributes(public, private, elliptic.P384()) case apiv1.ECDSAWithSHA512: - return ctx.GenerateECDSAKeyPairWithLabel(id, object, elliptic.P521()) + return ctx.GenerateECDSAKeyPairWithAttributes(public, private, elliptic.P521()) case apiv1.PureEd25519: return nil, fmt.Errorf("signature algorithm %s is not supported", req.SignatureAlgorithm) default: diff --git a/kms/pkcs11/pkcs11_no_cgo.go b/kms/pkcs11/pkcs11_no_cgo.go index 87c9a36b..6fa51dff 100644 --- a/kms/pkcs11/pkcs11_no_cgo.go +++ b/kms/pkcs11/pkcs11_no_cgo.go @@ -1,3 +1,4 @@ +//go:build !cgo // +build !cgo package pkcs11 diff --git a/kms/pkcs11/pkcs11_test.go b/kms/pkcs11/pkcs11_test.go index 77277366..409cfb3f 100644 --- a/kms/pkcs11/pkcs11_test.go +++ b/kms/pkcs11/pkcs11_test.go @@ -1,3 +1,4 @@ +//go:build cgo // +build cgo package pkcs11 @@ -207,6 +208,16 @@ func TestPKCS11_CreateKey(t *testing.T) { SigningKey: testObject, }, }, false}, + {"default extractable", args{&apiv1.CreateKeyRequest{ + Name: testObject, + Extractable: true, + }}, &apiv1.CreateKeyResponse{ + Name: testObject, + PublicKey: &ecdsa.PublicKey{}, + CreateSignerRequest: apiv1.CreateSignerRequest{ + SigningKey: testObject, + }, + }, false}, {"RSA SHA256WithRSA", args{&apiv1.CreateKeyRequest{ Name: testObject, SignatureAlgorithm: apiv1.SHA256WithRSA, @@ -562,6 +573,7 @@ func TestPKCS11_StoreCertificate(t *testing.T) { // Make sure to delete the created certificate t.Cleanup(func() { k.DeleteCertificate(testObject) + k.DeleteCertificate(testObjectAlt) }) type args struct { @@ -576,6 +588,11 @@ func TestPKCS11_StoreCertificate(t *testing.T) { Name: testObject, Certificate: cert, }}, false}, + {"ok extractable", args{&apiv1.StoreCertificateRequest{ + Name: testObjectAlt, + Certificate: cert, + Extractable: true, + }}, false}, {"fail already exists", args{&apiv1.StoreCertificateRequest{ Name: testObject, Certificate: cert, @@ -592,13 +609,22 @@ func TestPKCS11_StoreCertificate(t *testing.T) { Name: "http:id=7770;object=create-cert", Certificate: cert, }}, true}, - {"fail ImportCertificateWithLabel", args{&apiv1.StoreCertificateRequest{ - Name: "pkcs11:foo=bar", + {"fail missing id", args{&apiv1.StoreCertificateRequest{ + Name: "pkcs11:object=create-cert", + Certificate: cert, + }}, true}, + {"fail missing object", args{&apiv1.StoreCertificateRequest{ + Name: "pkcs11:id=7770;object=", Certificate: cert, }}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + if tt.args.req.Extractable { + if testModule == "SoftHSM2" { + t.Skip("Extractable certificates are not supported on SoftHSM2") + } + } if err := k.StoreCertificate(tt.args.req); (err != nil) != tt.wantErr { t.Errorf("PKCS11.StoreCertificate() error = %v, wantErr %v", err, tt.wantErr) } diff --git a/kms/pkcs11/setup_test.go b/kms/pkcs11/setup_test.go index c9ff9311..902d89ac 100644 --- a/kms/pkcs11/setup_test.go +++ b/kms/pkcs11/setup_test.go @@ -1,3 +1,4 @@ +//go:build cgo // +build cgo package pkcs11 @@ -17,6 +18,7 @@ import ( var ( testModule = "" testObject = "pkcs11:id=7370;object=test-name" + testObjectAlt = "pkcs11:id=7377;object=alt-test-name" testObjectByID = "pkcs11:id=7370" testObjectByLabel = "pkcs11:object=test-name" testKeys = []struct { @@ -104,7 +106,7 @@ func setup(t TBTesting, k *PKCS11) { }); err != nil && !errors.Is(errors.Cause(err), apiv1.ErrAlreadyExists{ Message: c.Name + " already exists", }) { - t.Errorf("PKCS1.StoreCertificate() error = %+v", err) + t.Errorf("PKCS1.StoreCertificate() error = %v", err) continue } testCerts[i].Certificates = append(testCerts[i].Certificates, cert) diff --git a/kms/pkcs11/softhsm2_test.go b/kms/pkcs11/softhsm2_test.go index 37aa667d..ed2ff208 100644 --- a/kms/pkcs11/softhsm2_test.go +++ b/kms/pkcs11/softhsm2_test.go @@ -1,3 +1,4 @@ +//go:build cgo && softhsm2 // +build cgo,softhsm2 package pkcs11 diff --git a/kms/pkcs11/yubihsm2_test.go b/kms/pkcs11/yubihsm2_test.go index 6d02a420..281aff54 100644 --- a/kms/pkcs11/yubihsm2_test.go +++ b/kms/pkcs11/yubihsm2_test.go @@ -1,3 +1,4 @@ +//go:build cgo && yubihsm2 // +build cgo,yubihsm2 package pkcs11 diff --git a/kms/sshagentkms/sshagentkms_test.go b/kms/sshagentkms/sshagentkms_test.go index 30edd5d1..d3a9e9f5 100644 --- a/kms/sshagentkms/sshagentkms_test.go +++ b/kms/sshagentkms/sshagentkms_test.go @@ -378,6 +378,7 @@ func TestSSHAgentKMS_CreateSigner(t *testing.T) { t.Errorf("SSHAgentKMS.CreateSigner() error = %v, wantErr %v", err, tt.wantErr) return } + // nolint:gocritic switch s := got.(type) { case *WrappedSSHSigner: gotPkS := s.Sshsigner.PublicKey().(*agent.Key).String() + "\n" @@ -562,6 +563,7 @@ func TestSSHAgentKMS_GetPublicKey(t *testing.T) { t.Errorf("SSHAgentKMS.GetPublicKey() error = %v, wantErr %v", err, tt.wantErr) return } + // nolint:gocritic switch tt.want.(type) { case ssh.PublicKey: // If we want a ssh.PublicKey, protote got to a diff --git a/kms/uri/uri.go b/kms/uri/uri.go index 94009c47..36e15e7d 100644 --- a/kms/uri/uri.go +++ b/kms/uri/uri.go @@ -59,7 +59,9 @@ func Parse(rawuri string) (*URI, error) { if u.Scheme == "" { return nil, errors.Errorf("error parsing %s: scheme is missing", rawuri) } - v, err := url.ParseQuery(u.Opaque) + // Starting with Go 1.17 url.ParseQuery returns an error using semicolon as + // separator. + v, err := url.ParseQuery(strings.ReplaceAll(u.Opaque, ";", "&")) if err != nil { return nil, errors.Wrapf(err, "error parsing %s", rawuri) } @@ -93,6 +95,16 @@ func (u *URI) Get(key string) string { return v } +// GetBool returns true if a given key has the value "true". It returns false +// otherwise. +func (u *URI) GetBool(key string) bool { + v := u.Values.Get(key) + if v == "" { + v = u.URL.Query().Get(key) + } + return strings.EqualFold(v, "true") +} + // GetEncoded returns the first value in the uri with the given key, it will // return empty nil if that field is not present or is empty. If the return // value is hex encoded it will decode it and return it. diff --git a/kms/uri/uri_test.go b/kms/uri/uri_test.go index aa420db4..01fbad0f 100644 --- a/kms/uri/uri_test.go +++ b/kms/uri/uri_test.go @@ -212,6 +212,40 @@ func TestURI_Get(t *testing.T) { } } +func TestURI_GetBool(t *testing.T) { + mustParse := func(s string) *URI { + u, err := Parse(s) + if err != nil { + t.Fatal(err) + } + return u + } + type args struct { + key string + } + tests := []struct { + name string + uri *URI + args args + want bool + }{ + {"true", mustParse("azurekms:name=foo;vault=bar;hsm=true"), args{"hsm"}, true}, + {"TRUE", mustParse("azurekms:name=foo;vault=bar;hsm=TRUE"), args{"hsm"}, true}, + {"tRUe query", mustParse("azurekms:name=foo;vault=bar?hsm=tRUe"), args{"hsm"}, true}, + {"false", mustParse("azurekms:name=foo;vault=bar;hsm=false"), args{"hsm"}, false}, + {"false query", mustParse("azurekms:name=foo;vault=bar?hsm=false"), args{"hsm"}, false}, + {"empty", mustParse("azurekms:name=foo;vault=bar;hsm=?bar=true"), args{"hsm"}, false}, + {"missing", mustParse("azurekms:name=foo;vault=bar"), args{"hsm"}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.uri.GetBool(tt.args.key); got != tt.want { + t.Errorf("URI.GetBool() = %v, want %v", got, tt.want) + } + }) + } +} + func TestURI_GetEncoded(t *testing.T) { mustParse := func(s string) *URI { u, err := Parse(s) @@ -274,3 +308,28 @@ func TestURI_Pin(t *testing.T) { }) } } + +func TestURI_String(t *testing.T) { + mustParse := func(s string) *URI { + u, err := Parse(s) + if err != nil { + t.Fatal(err) + } + return u + } + tests := []struct { + name string + uri *URI + want string + }{ + {"ok new", New("yubikey", url.Values{"slot-id": []string{"9a"}, "foo": []string{"bar"}}), "yubikey:foo=bar;slot-id=9a"}, + {"ok parse", mustParse("yubikey:slot-id=9a;foo=bar?bar=zar"), "yubikey:slot-id=9a;foo=bar?bar=zar"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.uri.String(); got != tt.want { + t.Errorf("URI.String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kms/yubikey/yubikey.go b/kms/yubikey/yubikey.go index 2dde244a..b1d5f7e3 100644 --- a/kms/yubikey/yubikey.go +++ b/kms/yubikey/yubikey.go @@ -1,3 +1,4 @@ +//go:build cgo // +build cgo package yubikey diff --git a/kms/yubikey/yubikey_no_cgo.go b/kms/yubikey/yubikey_no_cgo.go index 6ed7c630..24a76174 100644 --- a/kms/yubikey/yubikey_no_cgo.go +++ b/kms/yubikey/yubikey_no_cgo.go @@ -1,3 +1,4 @@ +//go:build !cgo // +build !cgo package yubikey diff --git a/make/docker.mk b/make/docker.mk index 8ed25219..edb82423 100644 --- a/make/docker.mk +++ b/make/docker.mk @@ -54,6 +54,8 @@ define DOCKER_BUILDX # $(1) -- Image Tag # $(2) -- Push (empty is no push | --push will push to dockerhub) docker buildx build . --progress plain -t $(DOCKER_IMAGE_NAME):$(1) -f docker/Dockerfile.step-ca --platform="$(DOCKER_PLATFORMS)" $(2) + echo -n "$(COSIGN_PWD)" | cosign sign -key /tmp/cosign.key -r $(DOCKER_IMAGE_NAME):$(1) + endef # For non-master builds don't build the docker containers. diff --git a/pki/helm.go b/pki/helm.go new file mode 100644 index 00000000..0a2f7f02 --- /dev/null +++ b/pki/helm.go @@ -0,0 +1,155 @@ +package pki + +import ( + "io" + "text/template" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" + authconfig "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/templates" + "go.step.sm/linkedca" +) + +type helmVariables struct { + *linkedca.Configuration + Defaults *linkedca.Defaults + Password string + EnableSSH bool + TLS authconfig.TLSOptions + Provisioners []provisioner.Interface +} + +// WriteHelmTemplate a helm template to configure the +// smallstep/step-certificates helm chart. +func (p *PKI) WriteHelmTemplate(w io.Writer) error { + tmpl, err := template.New("helm").Funcs(templates.StepFuncMap()).Parse(helmTemplate) + if err != nil { + return errors.Wrap(err, "error writing helm template") + } + + // Delete ssh section if it is not enabled + if !p.options.enableSSH { + p.Ssh = nil + } + + // Convert provisioner to ca.json + provisioners := make([]provisioner.Interface, len(p.Authority.Provisioners)) + for i, p := range p.Authority.Provisioners { + pp, err := authority.ProvisionerToCertificates(p) + if err != nil { + return err + } + provisioners[i] = pp + } + + if err := tmpl.Execute(w, helmVariables{ + Configuration: &p.Configuration, + Defaults: &p.Defaults, + Password: "", + EnableSSH: p.options.enableSSH, + TLS: authconfig.DefaultTLSOptions, + Provisioners: provisioners, + }); err != nil { + return errors.Wrap(err, "error executing helm template") + } + return nil +} + +const helmTemplate = `# Helm template +inject: + enabled: true + # Config contains the configuration files ca.json and defaults.json + config: + files: + ca.json: + root: {{ first .Root }} + federateRoots: [] + crt: {{ .Intermediate }} + key: {{ .IntermediateKey }} + {{- if .EnableSSH }} + ssh: + hostKey: {{ .Ssh.HostKey }} + userKey: {{ .Ssh.UserKey }} + {{- end }} + address: {{ .Address }} + dnsNames: + {{- range .DnsNames }} + - {{ . }} + {{- end }} + logger: + format: json + db: + type: badgerv2 + dataSource: /home/step/db + authority: + provisioners: + {{- range .Provisioners }} + - {{ . | toJson }} + {{- end }} + tls: + cipherSuites: + {{- range .TLS.CipherSuites }} + - {{ . }} + {{- end }} + minVersion: {{ .TLS.MinVersion }} + maxVersion: {{ .TLS.MaxVersion }} + renegotiation: {{ .TLS.Renegotiation }} + + defaults.json: + ca-url: {{ .Defaults.CaUrl }} + ca-config: {{ .Defaults.CaConfig }} + fingerprint: {{ .Defaults.Fingerprint }} + root: {{ .Defaults.Root }} + + # Certificates contains the root and intermediate certificate and + # optionally the SSH host and user public keys + certificates: + # intermediate_ca contains the text of the intermediate CA Certificate + intermediate_ca: | + {{- index .Files .Intermediate | toString | nindent 6 }} + + # root_ca contains the text of the root CA Certificate + root_ca: | + {{- first .Root | index .Files | toString | nindent 6 }} + + {{- if .Ssh }} + # ssh_host_ca contains the text of the public ssh key for the SSH root CA + ssh_host_ca: {{ index .Files .Ssh.HostPublicKey | toString }} + + # ssh_user_ca contains the text of the public ssh key for the SSH root CA + ssh_user_ca: {{ index .Files .Ssh.UserPublicKey | toString }} + {{- end }} + + # Secrets contains the root and intermediate keys and optionally the SSH + # private keys + secrets: + # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key + # This value must be base64 encoded. + ca_password: {{ .Password | b64enc }} + provisioner_password: {{ .Password | b64enc}} + + x509: + # intermediate_ca_key contains the contents of your encrypted intermediate CA key + intermediate_ca_key: | + {{- index .Files .IntermediateKey | toString | nindent 8 }} + + # root_ca_key contains the contents of your encrypted root CA key + # Note that this value can be omitted without impacting the functionality of step-certificates + # If supplied, this should be encrypted using a unique password that is not used for encrypting + # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key. + root_ca_key: | + {{- first .RootKey | index .Files | toString | nindent 8 }} + + {{- if .Ssh }} + ssh: + # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key + host_ca_key: | + {{- index .Files .Ssh.HostKey | toString | nindent 8 }} + + # ssh_user_ca_key contains the contents of your encrypted SSH User CA key + user_ca_key: | + {{- index .Files .Ssh.UserKey | toString | nindent 8 }} + {{- end }} +` diff --git a/pki/pki.go b/pki/pki.go index e4e7bad3..61e20b6b 100644 --- a/pki/pki.go +++ b/pki/pki.go @@ -10,31 +10,65 @@ import ( "encoding/json" "encoding/pem" "fmt" - "html" "net" "os" "path/filepath" - "strconv" "strings" "time" "github.com/pkg/errors" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/admin" + admindb "github.com/smallstep/certificates/authority/admin/db/nosql" authconfig "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/cas" "github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/db" + "github.com/smallstep/certificates/kms" + kmsapi "github.com/smallstep/certificates/kms/apiv1" + "github.com/smallstep/nosql" "go.step.sm/cli-utils/config" "go.step.sm/cli-utils/errs" "go.step.sm/cli-utils/fileutil" "go.step.sm/cli-utils/ui" "go.step.sm/crypto/jose" - "go.step.sm/crypto/keyutil" "go.step.sm/crypto/pemutil" + "go.step.sm/linkedca" "golang.org/x/crypto/ssh" ) +// DeploymentType defines witch type of deployment a user is initializing +type DeploymentType int + +const ( + // StandaloneDeployment is a deployment where all the components like keys, + // provisioners, admins, certificates and others are managed by the user. + StandaloneDeployment DeploymentType = iota + // LinkedDeployment is a deployment where the keys are managed by the user, + // but provisioners, admins and the record of certificates are managed in + // the cloud. + LinkedDeployment + // HostedDeployment is a deployment where all the components are managed in + // the cloud by smallstep.com/certificate-manager. + HostedDeployment +) + +// String returns the string version of the deployment type. +func (d DeploymentType) String() string { + switch d { + case StandaloneDeployment: + return "standalone" + case LinkedDeployment: + return "linked" + case HostedDeployment: + return "hosted" + default: + return "unknown" + } +} + const ( // ConfigPath is the directory name under the step path where the configuration // files will be stored. @@ -95,7 +129,7 @@ func GetTemplatesPath() string { // GetProvisioners returns the map of provisioners on the given CA. func GetProvisioners(caURL, rootFile string) (provisioner.List, error) { - if len(rootFile) == 0 { + if rootFile == "" { rootFile = GetRootCAPath() } client, err := ca.NewClient(caURL, ca.WithRootFile(rootFile)) @@ -120,7 +154,7 @@ func GetProvisioners(caURL, rootFile string) (provisioner.List, error) { // GetProvisionerKey returns the encrypted provisioner key with the for the // given kid. func GetProvisionerKey(caURL, rootFile, kid string) (string, error) { - if len(rootFile) == 0 { + if rootFile == "" { rootFile = GetRootCAPath() } client, err := ca.NewClient(caURL, ca.WithRootFile(rootFile)) @@ -134,43 +168,150 @@ func GetProvisionerKey(caURL, rootFile, kid string) (string, error) { return resp.Key, nil } +type options struct { + provisioner string + pkiOnly bool + enableACME bool + enableSSH bool + enableAdmin bool + noDB bool + isHelm bool + deploymentType DeploymentType + rootKeyURI string + intermediateKeyURI string + hostKeyURI string + userKeyURI string +} + +// Option is the type of a configuration option on the pki constructor. +type Option func(p *PKI) + +// WithAddress sets the listen address of step-ca. +func WithAddress(s string) Option { + return func(p *PKI) { + p.Address = s + } +} + +// WithCaURL sets the default ca-url of step-ca. +func WithCaURL(s string) Option { + return func(p *PKI) { + p.Defaults.CaUrl = s + } +} + +// WithDNSNames sets the SANs of step-ca. +func WithDNSNames(s []string) Option { + return func(p *PKI) { + p.DnsNames = s + } +} + +// WithProvisioner defines the name of the default provisioner. +func WithProvisioner(s string) Option { + return func(p *PKI) { + p.options.provisioner = s + } +} + +// WithPKIOnly will only generate the PKI without the step-ca config files. +func WithPKIOnly() Option { + return func(p *PKI) { + p.options.pkiOnly = true + } +} + +// WithACME enables acme provisioner in step-ca. +func WithACME() Option { + return func(p *PKI) { + p.options.enableACME = true + } +} + +// WithSSH enables ssh in step-ca. +func WithSSH() Option { + return func(p *PKI) { + p.options.enableSSH = true + } +} + +// WithAdmin enables the admin api in step-ca. +func WithAdmin() Option { + return func(p *PKI) { + p.options.enableAdmin = true + } +} + +// WithNoDB disables the db in step-ca. +func WithNoDB() Option { + return func(p *PKI) { + p.options.noDB = true + } +} + +// WithHelm configures the pki to create a helm values.yaml. +func WithHelm() Option { + return func(p *PKI) { + p.options.isHelm = true + } +} + +// WithDeploymentType defines the deployment type of step-ca. +func WithDeploymentType(dt DeploymentType) Option { + return func(p *PKI) { + p.options.deploymentType = dt + } +} + +// WithKMS enables the kms with the given name. +func WithKMS(name string) Option { + return func(p *PKI) { + typ := linkedca.KMS_Type_value[strings.ToUpper(name)] + p.Configuration.Kms = &linkedca.KMS{ + Type: linkedca.KMS_Type(typ), + } + } +} + +// WithKeyURIs defines the key uris for X.509 and SSH keys. +func WithKeyURIs(rootKey, intermediateKey, hostKey, userKey string) Option { + return func(p *PKI) { + p.options.rootKeyURI = rootKey + p.options.intermediateKeyURI = intermediateKey + p.options.hostKeyURI = hostKey + p.options.userKeyURI = userKey + } +} + // PKI represents the Public Key Infrastructure used by a certificate authority. type PKI struct { - casOptions apiv1.Options - caCreator apiv1.CertificateAuthorityCreator - root, rootKey, rootFingerprint string - intermediate, intermediateKey string - sshHostPubKey, sshHostKey string - sshUserPubKey, sshUserKey string - config, defaults string - ottPublicKey *jose.JSONWebKey - ottPrivateKey *jose.JSONWebEncryption - provisioner string - address string - dnsNames []string - caURL string - enableSSH bool + linkedca.Configuration + Defaults linkedca.Defaults + casOptions apiv1.Options + caService apiv1.CertificateAuthorityService + caCreator apiv1.CertificateAuthorityCreator + keyManager kmsapi.KeyManager + config string + defaults string + ottPublicKey *jose.JSONWebKey + ottPrivateKey *jose.JSONWebEncryption + options *options } // New creates a new PKI configuration. -func New(opts apiv1.Options) (*PKI, error) { - caCreator, err := cas.NewCreator(context.Background(), opts) +func New(o apiv1.Options, opts ...Option) (*PKI, error) { + caService, err := cas.New(context.Background(), o) if err != nil { return nil, err } - public := GetPublicPath() - private := GetSecretsPath() - config := GetConfigPath() - - // Create directories - dirs := []string{public, private, config, GetTemplatesPath()} - for _, name := range dirs { - if _, err := os.Stat(name); os.IsNotExist(err) { - if err = os.MkdirAll(name, 0700); err != nil { - return nil, errs.FileError(err, name) - } + var caCreator apiv1.CertificateAuthorityCreator + if o.IsCreator { + creator, ok := caService.(apiv1.CertificateAuthorityCreator) + if !ok { + return nil, errors.Errorf("cas type '%s' does not implements CertificateAuthorityCreator", o.Type) } + caCreator = creator } // get absolute path for dir/name @@ -180,45 +321,105 @@ func New(opts apiv1.Options) (*PKI, error) { } p := &PKI{ - casOptions: opts, - caCreator: caCreator, - provisioner: "step-cli", - address: "127.0.0.1:9000", - dnsNames: []string{"127.0.0.1"}, + Configuration: linkedca.Configuration{ + Address: "127.0.0.1:9000", + DnsNames: []string{"127.0.0.1"}, + Ssh: &linkedca.SSH{}, + Authority: &linkedca.Authority{}, + Files: make(map[string][]byte), + }, + casOptions: o, + caService: caService, + caCreator: caCreator, + keyManager: o.KeyManager, + options: &options{ + provisioner: "step-cli", + }, } - if p.root, err = getPath(public, "root_ca.crt"); err != nil { - return nil, err + for _, fn := range opts { + fn(p) } - if p.rootKey, err = getPath(private, "root_ca_key"); err != nil { - return nil, err + + // Use default key manager + if p.keyManager == nil { + p.keyManager = kms.Default } - if p.intermediate, err = getPath(public, "intermediate_ca.crt"); err != nil { - return nil, err - } - if p.intermediateKey, err = getPath(private, "intermediate_ca_key"); err != nil { - return nil, err - } - if p.sshHostPubKey, err = getPath(public, "ssh_host_ca_key.pub"); err != nil { - return nil, err - } - if p.sshUserPubKey, err = getPath(public, "ssh_user_ca_key.pub"); err != nil { - return nil, err - } - if p.sshHostKey, err = getPath(private, "ssh_host_ca_key"); err != nil { - return nil, err - } - if p.sshUserKey, err = getPath(private, "ssh_user_ca_key"); err != nil { - return nil, err - } - if len(config) > 0 { - if p.config, err = getPath(config, "ca.json"); err != nil { - return nil, err - } - if p.defaults, err = getPath(config, "defaults.json"); err != nil { - return nil, err + + // Use /home/step as the step path in helm configurations. + // Use the current step path when creating pki in files. + var public, private, cfg string + if p.options.isHelm { + public = "/home/step/certs" + private = "/home/step/secrets" + cfg = "/home/step/config" + } else { + public = GetPublicPath() + private = GetSecretsPath() + cfg = GetConfigPath() + // Create directories + dirs := []string{public, private, cfg, GetTemplatesPath()} + for _, name := range dirs { + if _, err := os.Stat(name); os.IsNotExist(err) { + if err = os.MkdirAll(name, 0700); err != nil { + return nil, errs.FileError(err, name) + } + } } } + if p.Defaults.CaUrl == "" { + p.Defaults.CaUrl = p.DnsNames[0] + _, port, err := net.SplitHostPort(p.Address) + if err != nil { + return nil, errors.Wrapf(err, "error parsing %s", p.Address) + } + // On k8s we usually access through a service, and this is configured on + // port 443 by default. + if port == "443" || p.options.isHelm { + p.Defaults.CaUrl = fmt.Sprintf("https://%s", p.Defaults.CaUrl) + } else { + p.Defaults.CaUrl = fmt.Sprintf("https://%s:%s", p.Defaults.CaUrl, port) + } + } + + root, err := getPath(public, "root_ca.crt") + if err != nil { + return nil, err + } + rootKey, err := getPath(private, "root_ca_key") + if err != nil { + return nil, err + } + p.Root = []string{root} + p.RootKey = []string{rootKey} + p.Defaults.Root = root + + if p.Intermediate, err = getPath(public, "intermediate_ca.crt"); err != nil { + return nil, err + } + if p.IntermediateKey, err = getPath(private, "intermediate_ca_key"); err != nil { + return nil, err + } + if p.Ssh.HostPublicKey, err = getPath(public, "ssh_host_ca_key.pub"); err != nil { + return nil, err + } + if p.Ssh.UserPublicKey, err = getPath(public, "ssh_user_ca_key.pub"); err != nil { + return nil, err + } + if p.Ssh.HostKey, err = getPath(private, "ssh_host_ca_key"); err != nil { + return nil, err + } + if p.Ssh.UserKey, err = getPath(private, "ssh_user_ca_key"); err != nil { + return nil, err + } + if p.defaults, err = getPath(cfg, "defaults.json"); err != nil { + return nil, err + } + if p.config, err = getPath(cfg, "ca.json"); err != nil { + return nil, err + } + p.Defaults.CaConfig = p.config + return p, nil } @@ -229,27 +430,7 @@ func (p *PKI) GetCAConfigPath() string { // GetRootFingerprint returns the root fingerprint. func (p *PKI) GetRootFingerprint() string { - return p.rootFingerprint -} - -// SetProvisioner sets the provisioner name of the OTT keys. -func (p *PKI) SetProvisioner(s string) { - p.provisioner = s -} - -// SetAddress sets the listening address of the CA. -func (p *PKI) SetAddress(s string) { - p.address = s -} - -// SetDNSNames sets the dns names of the CA. -func (p *PKI) SetDNSNames(s []string) { - p.dnsNames = s -} - -// SetCAURL sets the ca-url to use in the defaults.json. -func (p *PKI) SetCAURL(s string) { - p.caURL = s + return p.Defaults.Fingerprint } // GenerateKeyPairs generates the key pairs used by the certificate authority. @@ -261,17 +442,56 @@ func (p *PKI) GenerateKeyPairs(pass []byte) error { return err } + var claims *linkedca.Claims + if p.options.enableSSH { + claims = &linkedca.Claims{ + Ssh: &linkedca.SSHClaims{ + Enabled: true, + }, + } + } + + // Add JWK provisioner to the configuration. + publicKey, err := json.Marshal(p.ottPublicKey) + if err != nil { + return errors.Wrap(err, "error marshaling public key") + } + encryptedKey, err := p.ottPrivateKey.CompactSerialize() + if err != nil { + return errors.Wrap(err, "error serializing private key") + } + p.Authority.Provisioners = append(p.Authority.Provisioners, &linkedca.Provisioner{ + Type: linkedca.Provisioner_JWK, + Name: p.options.provisioner, + Claims: claims, + Details: &linkedca.ProvisionerDetails{ + Data: &linkedca.ProvisionerDetails_JWK{ + JWK: &linkedca.JWKProvisioner{ + PublicKey: publicKey, + EncryptedPrivateKey: []byte(encryptedKey), + }, + }, + }, + }) + return nil } // GenerateRootCertificate generates a root certificate with the given name // and using the default key type. func (p *PKI) GenerateRootCertificate(name, org, resource string, pass []byte) (*apiv1.CreateCertificateAuthorityResponse, error) { + if uri := p.options.rootKeyURI; uri != "" { + p.RootKey[0] = uri + } + resp, err := p.caCreator.CreateCertificateAuthority(&apiv1.CreateCertificateAuthorityRequest{ - Name: resource + "-Root-CA", - Type: apiv1.RootCA, - Lifetime: 10 * 365 * 24 * time.Hour, - CreateKey: nil, // use default + Name: resource + "-Root-CA", + Type: apiv1.RootCA, + Lifetime: 10 * 365 * 24 * time.Hour, + CreateKey: &apiv1.CreateKeyRequest{ + Name: p.RootKey[0], + SignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm, + }, Template: &x509.Certificate{ Subject: pkix.Name{ CommonName: name + " Root CA", @@ -288,6 +508,13 @@ func (p *PKI) GenerateRootCertificate(name, org, resource string, pass []byte) ( return nil, err } + // Replace key name with the one from the key manager if available. On + // softcas this will be the original filename, on any other kms will be the + // uri to the key. + if resp.KeyName != "" { + p.RootKey[0] = resp.KeyName + } + // PrivateKey will only be set if we have access to it (SoftCAS). if err := p.WriteRootCertificate(resp.Certificate, resp.PrivateKey, pass); err != nil { return nil, err @@ -296,14 +523,36 @@ func (p *PKI) GenerateRootCertificate(name, org, resource string, pass []byte) ( return resp, nil } +// WriteRootCertificate writes to the buffer the given certificate and key if given. +func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error { + p.Files[p.Root[0]] = encodeCertificate(rootCrt) + if rootKey != nil { + var err error + p.Files[p.RootKey[0]], err = encodePrivateKey(rootKey, pass) + if err != nil { + return err + } + } + sum := sha256.Sum256(rootCrt.Raw) + p.Defaults.Fingerprint = strings.ToLower(hex.EncodeToString(sum[:])) + return nil +} + // GenerateIntermediateCertificate generates an intermediate certificate with // the given name and using the default key type. func (p *PKI) GenerateIntermediateCertificate(name, org, resource string, parent *apiv1.CreateCertificateAuthorityResponse, pass []byte) error { + if uri := p.options.intermediateKeyURI; uri != "" { + p.IntermediateKey = uri + } + resp, err := p.caCreator.CreateCertificateAuthority(&apiv1.CreateCertificateAuthorityRequest{ - Name: resource + "-Intermediate-CA", - Type: apiv1.IntermediateCA, - Lifetime: 10 * 365 * 24 * time.Hour, - CreateKey: nil, // use default + Name: resource + "-Intermediate-CA", + Type: apiv1.IntermediateCA, + Lifetime: 10 * 365 * 24 * time.Hour, + CreateKey: &apiv1.CreateKeyRequest{ + Name: p.IntermediateKey, + SignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm, + }, Template: &x509.Certificate{ Subject: pkix.Name{ CommonName: name + " Intermediate CA", @@ -322,46 +571,21 @@ func (p *PKI) GenerateIntermediateCertificate(name, org, resource string, parent } p.casOptions.CertificateAuthority = resp.Name - return p.WriteIntermediateCertificate(resp.Certificate, resp.PrivateKey, pass) -} + p.Files[p.Intermediate] = encodeCertificate(resp.Certificate) -// WriteRootCertificate writes to disk the given certificate and key. -func (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error { - if err := fileutil.WriteFile(p.root, pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: rootCrt.Raw, - }), 0600); err != nil { - return err + // Replace the key name with the one from the key manager. On softcas this + // will be the original filename, on any other kms will be the uri to the + // key. + if resp.KeyName != "" { + p.IntermediateKey = resp.KeyName } - if rootKey != nil { - _, err := pemutil.Serialize(rootKey, pemutil.WithPassword(pass), pemutil.ToFile(p.rootKey, 0600)) - if err != nil { - return err - } + // If a kms is used it will not have the private key + if resp.PrivateKey != nil { + p.Files[p.IntermediateKey], err = encodePrivateKey(resp.PrivateKey, pass) } - sum := sha256.Sum256(rootCrt.Raw) - p.rootFingerprint = strings.ToLower(hex.EncodeToString(sum[:])) - - return nil -} - -// WriteIntermediateCertificate writes to disk the given certificate and key. -func (p *PKI) WriteIntermediateCertificate(crt *x509.Certificate, key interface{}, pass []byte) error { - if err := fileutil.WriteFile(p.intermediate, pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: crt.Raw, - }), 0600); err != nil { - return err - } - if key != nil { - _, err := pemutil.Serialize(key, pemutil.WithPassword(pass), pemutil.ToFile(p.intermediateKey, 0600)) - if err != nil { - return err - } - } - return nil + return err } // CreateCertificateAuthorityResponse returns a @@ -379,7 +603,7 @@ func (p *PKI) CreateCertificateAuthorityResponse(cert *x509.Certificate, key cry // GetCertificateAuthority attempts to load the certificate authority from the // RA. func (p *PKI) GetCertificateAuthority() error { - srv, ok := p.caCreator.(apiv1.CertificateAuthorityGetter) + srv, ok := p.caService.(apiv1.CertificateAuthorityGetter) if !ok { return nil } @@ -396,8 +620,8 @@ func (p *PKI) GetCertificateAuthority() error { } // Issuer is in the RA - p.intermediate = "" - p.intermediateKey = "" + p.Intermediate = "" + p.IntermediateKey = "" return nil } @@ -405,71 +629,120 @@ func (p *PKI) GetCertificateAuthority() error { // GenerateSSHSigningKeys generates and encrypts a private key used for signing // SSH user certificates and a private key used for signing host certificates. func (p *PKI) GenerateSSHSigningKeys(password []byte) error { - var pubNames = []string{p.sshHostPubKey, p.sshUserPubKey} - var privNames = []string{p.sshHostKey, p.sshUserKey} - for i := 0; i < 2; i++ { - pub, priv, err := keyutil.GenerateDefaultKeyPair() + // Enable SSH + p.options.enableSSH = true + + // Create SSH key used to sign host certificates. Using + // kmsapi.UnspecifiedSignAlgorithm will default to the default algorithm. + name := p.Ssh.HostKey + if uri := p.options.hostKeyURI; uri != "" { + name = uri + } + resp, err := p.keyManager.CreateKey(&kmsapi.CreateKeyRequest{ + Name: name, + SignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm, + }) + if err != nil { + return err + } + sshKey, err := ssh.NewPublicKey(resp.PublicKey) + if err != nil { + return errors.Wrapf(err, "error converting public key") + } + p.Files[p.Ssh.HostPublicKey] = ssh.MarshalAuthorizedKey(sshKey) + + // On softkms we will have the private key + if resp.PrivateKey != nil { + p.Files[p.Ssh.HostKey], err = encodePrivateKey(resp.PrivateKey, password) if err != nil { return err } - if _, ok := priv.(crypto.Signer); !ok { - return errors.Errorf("key of type %T is not a crypto.Signer", priv) - } - sshKey, err := ssh.NewPublicKey(pub) - if err != nil { - return errors.Wrapf(err, "error converting public key") - } - _, err = pemutil.Serialize(priv, pemutil.WithFilename(privNames[i]), pemutil.WithPassword(password)) + } else { + p.Ssh.HostKey = resp.Name + } + + // Create SSH key used to sign user certificates. Using + // kmsapi.UnspecifiedSignAlgorithm will default to the default algorithm. + name = p.Ssh.UserKey + if uri := p.options.userKeyURI; uri != "" { + name = uri + } + resp, err = p.keyManager.CreateKey(&kmsapi.CreateKeyRequest{ + Name: name, + SignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm, + }) + if err != nil { + return err + } + sshKey, err = ssh.NewPublicKey(resp.PublicKey) + if err != nil { + return errors.Wrapf(err, "error converting public key") + } + p.Files[p.Ssh.UserPublicKey] = ssh.MarshalAuthorizedKey(sshKey) + + // On softkms we will have the private key + if resp.PrivateKey != nil { + p.Files[p.Ssh.UserKey], err = encodePrivateKey(resp.PrivateKey, password) if err != nil { return err } - if err = fileutil.WriteFile(pubNames[i], ssh.MarshalAuthorizedKey(sshKey), 0600); err != nil { + } else { + p.Ssh.UserKey = resp.Name + } + + return nil +} + +// WriteFiles writes on disk the previously generated files. +func (p *PKI) WriteFiles() error { + for fn, b := range p.Files { + if err := fileutil.WriteFile(fn, b, 0600); err != nil { return err } } - p.enableSSH = true return nil } func (p *PKI) askFeedback() { ui.Println() - ui.Printf("\033[1mFEEDBACK\033[0m %s %s\n", - html.UnescapeString("&#"+strconv.Itoa(128525)+";"), - html.UnescapeString("&#"+strconv.Itoa(127867)+";")) - ui.Println(" The \033[1mstep\033[0m utility is not instrumented for usage statistics. It does not") - ui.Println(" phone home. But your feedback is extremely valuable. Any information you") - ui.Println(" can provide regarding how you’re using `step` helps. Please send us a") - ui.Println(" sentence or two, good or bad: \033[1mfeedback@smallstep.com\033[0m or join") - ui.Println(" \033[1mhttps://github.com/smallstep/certificates/discussions\033[0m.") -} + ui.Println("\033[1mFEEDBACK\033[0m 😍 🍻") + ui.Println(" The \033[1mstep\033[0m utility is not instrumented for usage statistics. It does not phone") + ui.Println(" home. But your feedback is extremely valuable. Any information you can provide") + ui.Println(" regarding how you’re using `step` helps. Please send us a sentence or two,") + ui.Println(" good or bad at \033[1mfeedback@smallstep.com\033[0m or join GitHub Discussions") + ui.Println(" \033[1mhttps://github.com/smallstep/certificates/discussions\033[0m and our Discord ") + ui.Println(" \033[1mhttps://u.step.sm/discord\033[0m.") -// TellPKI outputs the locations of public and private keys generated -// generated for a new PKI. Generally this will consist of a root certificate -// and key and an intermediate certificate and key. -func (p *PKI) TellPKI() { - p.tellPKI() - p.askFeedback() + if p.options.deploymentType == LinkedDeployment { + ui.Println() + ui.Println("\033[1mNEXT STEPS\033[0m") + ui.Println(" 1. Log in or create a Certificate Manager account at \033[1mhttps://u.step.sm/linked\033[0m") + ui.Println(" 2. Add a new authority and select \"Link a step-ca instance\"") + ui.Println(" 3. Follow instructions in browser to start `step-ca` using the `--token` flag") + ui.Println() + } } func (p *PKI) tellPKI() { ui.Println() - if p.casOptions.Is(apiv1.SoftCAS) { - ui.PrintSelected("Root certificate", p.root) - ui.PrintSelected("Root private key", p.rootKey) - ui.PrintSelected("Root fingerprint", p.rootFingerprint) - ui.PrintSelected("Intermediate certificate", p.intermediate) - ui.PrintSelected("Intermediate private key", p.intermediateKey) - } else if p.rootFingerprint != "" { - ui.PrintSelected("Root certificate", p.root) - ui.PrintSelected("Root fingerprint", p.rootFingerprint) - } else { + switch { + case p.casOptions.Is(apiv1.SoftCAS): + ui.PrintSelected("Root certificate", p.Root[0]) + ui.PrintSelected("Root private key", p.RootKey[0]) + ui.PrintSelected("Root fingerprint", p.Defaults.Fingerprint) + ui.PrintSelected("Intermediate certificate", p.Intermediate) + ui.PrintSelected("Intermediate private key", p.IntermediateKey) + case p.Defaults.Fingerprint != "": + ui.PrintSelected("Root certificate", p.Root[0]) + ui.PrintSelected("Root fingerprint", p.Defaults.Fingerprint) + default: ui.Printf(`{{ "%s" | red }} {{ "Root certificate:" | bold }} failed to retrieve it from RA`+"\n", ui.IconBad) } - if p.enableSSH { - ui.PrintSelected("SSH user root certificate", p.sshUserPubKey) - ui.PrintSelected("SSH user root private key", p.sshUserKey) - ui.PrintSelected("SSH host root certificate", p.sshHostPubKey) - ui.PrintSelected("SSH host root private key", p.sshHostKey) + if p.options.enableSSH { + ui.PrintSelected("SSH user public key", p.Ssh.UserPublicKey) + ui.PrintSelected("SSH user private key", p.Ssh.UserKey) + ui.PrintSelected("SSH host public key", p.Ssh.HostPublicKey) + ui.PrintSelected("SSH host private key", p.Ssh.HostKey) } } @@ -480,176 +753,237 @@ type caDefaults struct { Root string `json:"root"` } -// Option is the type for modifiers over the auth config object. -type Option func(c *authconfig.Config) error - -// WithDefaultDB is a configuration modifier that adds a default DB stanza to -// the authority config. -func WithDefaultDB() Option { - return func(c *authconfig.Config) error { - c.DB = &db.Config{ - Type: "badger", - DataSource: GetDBPath(), - } - return nil - } -} - -// WithoutDB is a configuration modifier that adds a default DB stanza to -// the authority config. -func WithoutDB() Option { - return func(c *authconfig.Config) error { - c.DB = nil - return nil - } -} +// ConfigOption is the type for modifiers over the auth config object. +type ConfigOption func(c *authconfig.Config) error // GenerateConfig returns the step certificates configuration. -func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) { - key, err := p.ottPrivateKey.CompactSerialize() - if err != nil { - return nil, errors.Wrap(err, "error serializing private key") - } - - prov := &provisioner.JWK{ - Name: p.provisioner, - Type: "JWK", - Key: p.ottPublicKey, - EncryptedKey: key, - } - +func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) { var authorityOptions *apiv1.Options if !p.casOptions.Is(apiv1.SoftCAS) { authorityOptions = &p.casOptions } - config := &authconfig.Config{ - Root: []string{p.root}, - FederatedRoots: []string{}, - IntermediateCert: p.intermediate, - IntermediateKey: p.intermediateKey, - Address: p.address, - DNSNames: p.dnsNames, + cfg := &authconfig.Config{ + Root: p.Root, + FederatedRoots: p.FederatedRoots, + IntermediateCert: p.Intermediate, + IntermediateKey: p.IntermediateKey, + Address: p.Address, + DNSNames: p.DnsNames, Logger: []byte(`{"format": "text"}`), DB: &db.Config{ - Type: "badger", + Type: "badgerv2", DataSource: GetDBPath(), }, AuthorityConfig: &authconfig.AuthConfig{ Options: authorityOptions, DisableIssuedAtCheck: false, - Provisioners: provisioner.List{prov}, - }, - TLS: &authconfig.TLSOptions{ - MinVersion: authconfig.DefaultTLSMinVersion, - MaxVersion: authconfig.DefaultTLSMaxVersion, - Renegotiation: authconfig.DefaultTLSRenegotiation, - CipherSuites: authconfig.DefaultTLSCipherSuites, + EnableAdmin: false, }, + TLS: &authconfig.DefaultTLSOptions, Templates: p.getTemplates(), } - if p.enableSSH { - enableSSHCA := true - config.SSH = &authconfig.SSHConfig{ - HostKey: p.sshHostKey, - UserKey: p.sshUserKey, + + // Add linked as a deployment type to detect it on start and provide a + // message if the token is not given. + if p.options.deploymentType == LinkedDeployment { + cfg.AuthorityConfig.DeploymentType = LinkedDeployment.String() + } + + // Enable KMS if necessary + if p.Kms != nil { + cfg.KMS = &kmsapi.Options{ + Type: strings.ToLower(p.Kms.Type.String()), } - // Enable SSH authorization for default JWK provisioner - prov.Claims = &provisioner.Claims{ - EnableSSHCA: &enableSSHCA, + } + + // On standalone deployments add the provisioners to either the ca.json or + // the database. + var provisioners []provisioner.Interface + if p.options.deploymentType == StandaloneDeployment { + key, err := p.ottPrivateKey.CompactSerialize() + if err != nil { + return nil, errors.Wrap(err, "error serializing private key") } - // Add default SSHPOP provisioner - sshpop := &provisioner.SSHPOP{ - Type: "SSHPOP", - Name: "sshpop", - Claims: &provisioner.Claims{ + + prov := &provisioner.JWK{ + Name: p.options.provisioner, + Type: "JWK", + Key: p.ottPublicKey, + EncryptedKey: key, + } + provisioners = append(provisioners, prov) + + // Add default ACME provisioner if enabled + if p.options.enableACME { + provisioners = append(provisioners, &provisioner.ACME{ + Type: "ACME", + Name: "acme", + }) + } + + if p.options.enableSSH { + enableSSHCA := true + cfg.SSH = &authconfig.SSHConfig{ + HostKey: p.Ssh.HostKey, + UserKey: p.Ssh.UserKey, + } + // Enable SSH authorization for default JWK provisioner + prov.Claims = &provisioner.Claims{ EnableSSHCA: &enableSSHCA, - }, + } + + // Add default SSHPOP provisioner + provisioners = append(provisioners, &provisioner.SSHPOP{ + Type: "SSHPOP", + Name: "sshpop", + Claims: &provisioner.Claims{ + EnableSSHCA: &enableSSHCA, + }, + }) } - config.AuthorityConfig.Provisioners = append(config.AuthorityConfig.Provisioners, sshpop) } // Apply configuration modifiers for _, o := range opt { - if err = o(config); err != nil { + if err := o(cfg); err != nil { return nil, err } } - return config, nil + // Set authority.enableAdmin to true + if p.options.enableAdmin { + cfg.AuthorityConfig.EnableAdmin = true + } + + if p.options.deploymentType == StandaloneDeployment { + if !cfg.AuthorityConfig.EnableAdmin { + cfg.AuthorityConfig.Provisioners = provisioners + } else { + // At this moment this code path is never used because `step ca + // init` will always set enableAdmin to false for a standalone + // deployment. Once we move `step beta` commands out of the beta we + // should probably default to this route. + // + // Note that we might want to be able to define the database as a + // flag in `step ca init` so we can write to the proper place. + _db, err := db.New(cfg.DB) + if err != nil { + return nil, err + } + adminDB, err := admindb.New(_db.(nosql.DB), admin.DefaultAuthorityID) + if err != nil { + return nil, err + } + // Add all the provisioners to the db. + var adminID string + for i, p := range provisioners { + prov, err := authority.ProvisionerToLinkedca(p) + if err != nil { + return nil, err + } + if err := adminDB.CreateProvisioner(context.Background(), prov); err != nil { + return nil, err + } + if i == 0 { + adminID = prov.Id + } + } + // Add the first provisioner as an admin. + if err := adminDB.CreateAdmin(context.Background(), &linkedca.Admin{ + AuthorityId: admin.DefaultAuthorityID, + Subject: "step", + Type: linkedca.Admin_SUPER_ADMIN, + ProvisionerId: adminID, + }); err != nil { + return nil, err + } + } + } + + return cfg, nil } // Save stores the pki on a json file that will be used as the certificate // authority configuration. -func (p *PKI) Save(opt ...Option) error { +func (p *PKI) Save(opt ...ConfigOption) error { + // Write generated files + if err := p.WriteFiles(); err != nil { + return err + } + + // Display the files written p.tellPKI() // Generate and write ca.json - config, err := p.GenerateConfig(opt...) - if err != nil { - return err - } - - b, err := json.MarshalIndent(config, "", "\t") - if err != nil { - return errors.Wrapf(err, "error marshaling %s", p.config) - } - if err = fileutil.WriteFile(p.config, b, 0644); err != nil { - return errs.FileError(err, p.config) - } - - // Generate the CA URL. - if p.caURL == "" { - p.caURL = p.dnsNames[0] - var port string - _, port, err = net.SplitHostPort(p.address) + if !p.options.pkiOnly { + cfg, err := p.GenerateConfig(opt...) if err != nil { - return errors.Wrapf(err, "error parsing %s", p.address) + return err } - if port == "443" { - p.caURL = fmt.Sprintf("https://%s", p.caURL) - } else { - p.caURL = fmt.Sprintf("https://%s:%s", p.caURL, port) + + b, err := json.MarshalIndent(cfg, "", "\t") + if err != nil { + return errors.Wrapf(err, "error marshaling %s", p.config) + } + if err = fileutil.WriteFile(p.config, b, 0644); err != nil { + return errs.FileError(err, p.config) } - } - // Generate and write defaults.json - defaults := &caDefaults{ - Root: p.root, - CAConfig: p.config, - CAUrl: p.caURL, - Fingerprint: p.rootFingerprint, - } - b, err = json.MarshalIndent(defaults, "", "\t") - if err != nil { - return errors.Wrapf(err, "error marshaling %s", p.defaults) - } - if err = fileutil.WriteFile(p.defaults, b, 0644); err != nil { - return errs.FileError(err, p.defaults) - } + // Generate and write defaults.json + defaults := &caDefaults{ + Root: p.Defaults.Root, + CAConfig: p.Defaults.CaConfig, + CAUrl: p.Defaults.CaUrl, + Fingerprint: p.Defaults.Fingerprint, + } + b, err = json.MarshalIndent(defaults, "", "\t") + if err != nil { + return errors.Wrapf(err, "error marshaling %s", p.defaults) + } + if err = fileutil.WriteFile(p.defaults, b, 0644); err != nil { + return errs.FileError(err, p.defaults) + } - // Generate and write templates - if err := generateTemplates(config.Templates); err != nil { - return err - } + // Generate and write templates + if err := generateTemplates(cfg.Templates); err != nil { + return err + } - if config.DB != nil { - ui.PrintSelected("Database folder", config.DB.DataSource) - } - if config.Templates != nil { - ui.PrintSelected("Templates folder", GetTemplatesPath()) - } + if cfg.DB != nil { + ui.PrintSelected("Database folder", cfg.DB.DataSource) + } + if cfg.Templates != nil { + ui.PrintSelected("Templates folder", GetTemplatesPath()) + } - ui.PrintSelected("Default configuration", p.defaults) - ui.PrintSelected("Certificate Authority configuration", p.config) - ui.Println() - if p.casOptions.Is(apiv1.SoftCAS) { - ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.") - } else { - ui.Println("Your registration authority is ready to go. To generate certificates for individual services see 'step help ca'.") + ui.PrintSelected("Default configuration", p.defaults) + ui.PrintSelected("Certificate Authority configuration", p.config) + if p.options.deploymentType != LinkedDeployment { + ui.Println() + if p.casOptions.Is(apiv1.SoftCAS) { + ui.Println("Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.") + } else { + ui.Println("Your registration authority is ready to go. To generate certificates for individual services see 'step help ca'.") + } + } } p.askFeedback() - return nil } + +func encodeCertificate(c *x509.Certificate) []byte { + return pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: c.Raw, + }) +} + +func encodePrivateKey(key crypto.PrivateKey, pass []byte) ([]byte, error) { + block, err := pemutil.Serialize(key, pemutil.WithPassword(pass)) + if err != nil { + return nil, err + } + return pem.EncodeToMemory(block), nil +} diff --git a/pki/templates.go b/pki/templates.go index 4c5309bb..3506a96d 100644 --- a/pki/templates.go +++ b/pki/templates.go @@ -13,7 +13,7 @@ import ( // getTemplates returns all the templates enabled func (p *PKI) getTemplates() *templates.Templates { - if !p.enableSSH { + if !p.options.enableSSH { return nil } return &templates.Templates{ diff --git a/scep/api/api.go b/scep/api/api.go index e64eef83..4e02d4a1 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -198,14 +198,14 @@ func (h *Handler) lookupProvisioner(next nextHTTP) nextHTTP { return } - provisioner, ok := p.(*provisioner.SCEP) + prov, ok := p.(*provisioner.SCEP) if !ok { api.WriteError(w, errors.New("provisioner must be of type SCEP")) return } ctx := r.Context() - ctx = context.WithValue(ctx, scep.ProvisionerContextKey, scep.Provisioner(provisioner)) + ctx = context.WithValue(ctx, scep.ProvisionerContextKey, scep.Provisioner(prov)) next(w, r.WithContext(ctx)) } } diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..80d3cdba --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,4 @@ +# Scripts folder + +Please note that `install-step-ra.sh` is referenced on the `files.smallstep.com` S3 website bucket as a redirect to `raw.githubusercontent.com`. If you move it, please update the S3 redirect. + diff --git a/scripts/install-step-ra.sh b/scripts/install-step-ra.sh new file mode 100644 index 00000000..1da64ed6 --- /dev/null +++ b/scripts/install-step-ra.sh @@ -0,0 +1,278 @@ +#!/bin/bash +set -e + +# TODO: +# - Parse params using argbash (argbash.io). Here's a template that I have tested but have not implemented yet: +# +# ARG_OPTIONAL_SINGLE([ca-url], , [the URL of the upstream (issuing) step-ca server]) +# ARG_OPTIONAL_SINGLE([fingerprint], , [the SHA256 fingerprint of the upstream peer step-ca server]) +# ARG_OPTIONAL_SINGLE([provisioner-name], , [the name of a JWK provisioner on the upstream CA that this RA will use]) +# ARG_OPTIONAL_SINGLE([provisioner-password-file], , [the name a file containing the upstream JWK provisioner password]) +# ARG_OPTIONAL_REPEATED([dns-name], , [DNS name of this RA that will appear on its TLS certificate; you may pass this flag multiple times]) +# ARG_OPTIONAL_SINGLE([listen-address], , [the address (and port #) this RA will listen on, eg. :443 or 127.0.0.1:4443]) +# ARG_HELP([This script will install and configure a Registration Authority that connects to an upstream CA running step-ca.]) +# ARGBASH_GO + +echo "This script will install and start a step-ca server running in Registration Authority (RA) mode." +echo "" +echo "You will need an upstream CA (URL and fingerprint)" +echo "Don't have a CA? Sign up for a hosted CA at smallstep.com β€” or run your own." +echo "" + +# Fail if this script is not run as root. +if ! [ $(id -u) = 0 ]; then + echo "This script must be run as root" + exit 1 +fi + +# Architecture detection +arch=$(uname -m) +case $arch in + x86_64) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; +esac + +if [ "$arch" = "armv5" ]; then + echo "This script doesn't work on armv5 machines" + exit 1 +fi + +if ! hash jq &> /dev/null; then + echo "This script requires the jq commmand; please install it." + exit 1 +fi + +if ! hash curl &> /dev/null; then + echo "This script requires the curl commmand; please install it." + exit 1 +fi + +if ! hash tar &> /dev/null; then + echo "This script requires the tar commmand; please install it." + exit 1 +fi + +while [ $# -gt 0 ]; do + case "$1" in + --ca-url) + CA_URL="$2" + shift + shift + ;; + --fingerprint) + CA_FINGERPRINT="$2" + shift + shift + ;; + --provisioner-name) + CA_PROVISIONER_NAME="$2" + shift + shift + ;; + --provisioner-password-file) + CA_PROVISIONER_JWK_PASSWORD_FILE="$2" + shift + shift + ;; + --dns-names) + RA_DNS_NAMES="$2" + shift + shift + ;; + --listen-address) + RA_ADDRESS="$2" + shift + shift + ;; + *) + shift + ;; + esac +done + +# Install step +if ! hash step &> /dev/null; then + echo "Installing 'step' in /usr/bin..." + STEP_VERSION=$(curl -s https://api.github.com/repos/smallstep/cli/releases/latest | jq -r '.tag_name') + + curl -sLO https://github.com/smallstep/cli/releases/download/$STEP_VERSION/step_linux_${STEP_VERSION:1}_$arch.tar.gz + tar xvzf step_linux_${STEP_VERSION:1}_$arch.tar.gz + install -m 0755 -t /usr/bin step_${STEP_VERSION:1}/bin/step + + rm step_linux_${STEP_VERSION:1}_$arch.tar.gz + rm -rf step_${STEP_VERSION:1} +fi + +# Prompt for required parameters +if [ -z "$CA_URL" ]; then + CA_URL="" + while [[ $CA_URL = "" ]]; do + read -p "Issuing CA URL: " CA_URL < /dev/tty + done +fi + +if [ -z "$CA_FINGERPRINT" ]; then + CA_FINGERPRINT="" + while [[ $CA_FINGERPRINT = "" ]]; do + read -p "Issuing CA Fingerprint: " CA_FINGERPRINT < /dev/tty + done +fi + +echo "Bootstrapping with the CA..." +export STEPPATH=$(mktemp -d) + +step ca bootstrap --ca-url $CA_URL --fingerprint $CA_FINGERPRINT + +if [ -z "$CA_PROVISIONER_NAME" ]; then + declare -a provisioners + readarray -t provisioners < <(step ca provisioner list | jq -r '.[] | select(.type == "JWK") | .name') + printf '%s\n' "${provisioners[@]}" + + printf "%b" "\nSelect a JWK provisioner:\n" >&2 + select provisioner in "${provisioners[@]}"; do + if [ -n "$provisioner" ]; then + echo "Using existing provisioner $provisioner." + CA_PROVISIONER_NAME=$provisioner + break + else + echo "Invalid selection!" + fi + done +fi + +if [ -z "$RA_DNS_NAMES" ]; then + RA_DNS_NAMES="" + while [[ $RA_DNS_NAMES = "" ]]; do + echo "What DNS names or IP addresses will your RA use?" + read -p "(e.g. acme.example.com[,1.1.1.1,etc.]): " RA_DNS_NAMES < /dev/tty + done +fi + + +count=0 +ra_dns_names_quoted="" + +for i in ${RA_DNS_NAMES//,/ } +do + if [ "$count" = "0" ]; then + ra_dns_names_quoted="\"$i\"" + else + ra_dns_names_quoted="${ra_dns_names_quoted}, \"$i\"" + fi + count=$((count+1)) +done + +if [ "$count" = "0" ]; then + echo "You must supply at least one RA DNS name" + exit 1 +fi + +echo "Got here" + +if [ -z "$RA_ADDRESS" ]; then + RA_ADDRESS="" + while [[ $RA_ADDRESS = "" ]] ; do + echo "What address should your RA listen on?" + read -p "(e.g. :443 or 10.2.1.201:4430): " RA_ADDRESS < /dev/tty + done +fi + +if [ -z "$CA_PROVISIONER_JWK_PASSWORD_FILE" ]; then + read -s -p "Enter the CA Provisioner Password: " CA_PROVISIONER_JWK_PASSWORD < /dev/tty + printf "%b" "\n" +fi + +echo "Installing 'step-ca' in /usr/bin..." +CA_VERSION=$(curl -s https://api.github.com/repos/smallstep/certificates/releases/latest | jq -r '.tag_name') + +curl -sLO https://github.com/smallstep/certificates/releases/download/$CA_VERSION/step-ca_linux_${CA_VERSION:1}_$arch.tar.gz +tar -xf step-ca_linux_${CA_VERSION:1}_$arch.tar.gz +install -m 0755 -t /usr/bin step-ca_${CA_VERSION:1}/bin/step-ca +setcap CAP_NET_BIND_SERVICE=+eip $(which step-ca) +rm step-ca_linux_${CA_VERSION:1}_$arch.tar.gz +rm -rf step-ca_${CA_VERSION:1} + +echo "Creating 'step' user..." +export STEPPATH=/etc/step-ca + +useradd --system --home $(step path) --shell /bin/false step + +echo "Creating RA configuration..." +mkdir -p $(step path)/db +mkdir -p $(step path)/config + +cat < $(step path)/config/ca.json +{ + "address": "$RA_ADDRESS", + "dnsNames": [$ra_dns_names_quoted], + "db": { + "type": "badgerV2", + "dataSource": "/etc/step-ca/db" + }, + "logger": {"format": "text"}, + "authority": { + "type": "stepcas", + "certificateAuthority": "$CA_URL", + "certificateAuthorityFingerprint": "$CA_FINGERPRINT", + "certificateIssuer": { + "type" : "jwk", + "provisioner": "$CA_PROVISIONER_NAME" + }, + "provisioners": [{ + "type": "ACME", + "name": "acme" + }] + }, + "tls": { + "cipherSuites": [ + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" + ], + "minVersion": 1.2, + "maxVersion": 1.3, + "renegotiation": false + } +} +EOF + +if ! [ -z "$CA_PROVISIONER_JWK_PASSWORD" ]; then + echo "Saving provisoiner password to $(step path)/password.txt..." + echo $CA_PROVISIONER_JWK_PASSWORD > $(step path)/password.txt +else + echo "Copying provisioner password file to $(step path)/password.txt..." + cp $CA_PROVISIONER_JWK_PASSWORD_FILE $(step path)/password.txt +fi +chmod 440 $(step path)/password.txt + +# Add a service to systemd for the RA. +echo "Creating systemd service step-ca.service..." +curl -sL https://raw.githubusercontent.com/smallstep/certificates/master/systemd/step-ca.service \ + -o /etc/systemd/system/step-ca.service + +echo "Creating RA mode override /etc/systemd/system/step-ca.service.d/local.conf..." +mkdir /etc/systemd/system/step-ca.service.d +cat < /etc/systemd/system/step-ca.service.d/local.conf +[Service] +; The empty ExecStart= clears the inherited ExecStart= value +ExecStart= +ExecStart=/usr/bin/step-ca config/ca.json --issuer-password-file password.txt +EOF + +echo "Starting step-ca.service..." +systemctl daemon-reload + +chown -R step:step $(step path) + +systemctl enable --now step-ca + +echo "Adding STEPPATH export to /root/.bash_profile..." +echo "export STEPPATH=$STEPPATH" >> /root/.bash_profile + +echo "Finished. Check the journal with journalctl -fu step-ca.service" + diff --git a/server/server.go b/server/server.go index d3968c4a..2b864148 100644 --- a/server/server.go +++ b/server/server.go @@ -72,10 +72,10 @@ func (srv *Server) Serve(ln net.Listener) error { // Start server if srv.TLSConfig == nil || (len(srv.TLSConfig.Certificates) == 0 && srv.TLSConfig.GetCertificate == nil) { log.Printf("Serving HTTP on %s ...", srv.Addr) - err = srv.Server.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) + err = srv.Server.Serve(ln) } else { log.Printf("Serving HTTPS on %s ...", srv.Addr) - err = srv.Server.ServeTLS(tcpKeepAliveListener{ln.(*net.TCPListener)}, "", "") + err = srv.Server.ServeTLS(ln, "", "") } // log unexpected errors @@ -155,21 +155,3 @@ func (srv *Server) Forbidden(w http.ResponseWriter) { w.WriteHeader(http.StatusForbidden) w.Write([]byte("Forbidden.\n")) } - -// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted -// connections. It's used by ListenAndServe and ListenAndServeTLS so -// dead TCP connections (e.g. closing laptop mid-download) eventually -// go away. -type tcpKeepAliveListener struct { - *net.TCPListener -} - -func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { - tc, err := ln.AcceptTCP() - if err != nil { - return - } - tc.SetKeepAlive(true) - tc.SetKeepAlivePeriod(3 * time.Minute) - return tc, nil -} diff --git a/systemd/cert-renewer@.service b/systemd/cert-renewer@.service index f38951b5..a9962c2e 100644 --- a/systemd/cert-renewer@.service +++ b/systemd/cert-renewer@.service @@ -12,17 +12,13 @@ Environment=STEPPATH=/etc/step-ca \ CERT_LOCATION=/etc/step/certs/%i.crt \ KEY_LOCATION=/etc/step/certs/%i.key -; ExecStartPre checks if the certificate is ready for renewal, +; ExecCondition checks if the certificate is ready for renewal, ; based on the exit status of the command. -; (In systemd 243 and above, you can use ExecCondition= here.) -ExecStartPre=/usr/bin/env bash -c \ - 'step certificate inspect $CERT_LOCATION --format json --roots "$STEPPATH/certs/root_ca.crt" | \ - jq -e "(((.validity.start | fromdate) + \ - ((.validity.end | fromdate) - (.validity.start | fromdate)) * 0.66) \ - - now) <= 0" > /dev/null' +; (In systemd 242 or below, you can use ExecStartPre= here.) +ExecCondition=/usr/bin/step certificate needs-renewal ${CERT_LOCATION} ; ExecStart renews the certificate, if ExecStartPre was successful. -ExecStart=/usr/bin/step ca renew --force $CERT_LOCATION $KEY_LOCATION +ExecStart=/usr/bin/step ca renew --force ${CERT_LOCATION} ${KEY_LOCATION} ; Try to reload or restart the systemd service that relies on this cert-renewer ; If the relying service doesn't exist, forge ahead. diff --git a/templates/templates.go b/templates/templates.go index f98fb866..09416b68 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "text/template" "github.com/Masterminds/sprig/v3" @@ -182,7 +183,7 @@ func (t *Template) Load() error { // the template fails. func (t *Template) LoadBytes(b []byte) error { t.backfill(b) - tmpl, err := template.New(t.Name).Funcs(sprig.TxtFuncMap()).Parse(string(b)) + tmpl, err := template.New(t.Name).Funcs(StepFuncMap()).Parse(string(b)) if err != nil { return errors.Wrapf(err, "error parsing template %s", t.Name) } @@ -226,14 +227,11 @@ func (t *Template) Output(data interface{}) (Output, error) { // backfill updates old templates with the required data. func (t *Template) backfill(b []byte) { - switch t.Name { - case "sshd_config.tpl": - if len(t.RequiredData) == 0 { - a := bytes.TrimSpace(b) - b := bytes.TrimSpace([]byte(DefaultSSHTemplateData[t.Name])) - if bytes.Equal(a, b) { - t.RequiredData = []string{"Certificate", "Key"} - } + if strings.EqualFold(t.Name, "sshd_config.tpl") && len(t.RequiredData) == 0 { + a := bytes.TrimSpace(b) + b := bytes.TrimSpace([]byte(DefaultSSHTemplateData[t.Name])) + if bytes.Equal(a, b) { + t.RequiredData = []string{"Certificate", "Key"} } } } @@ -272,3 +270,12 @@ func mkdir(path string, perm os.FileMode) error { } return nil } + +// StepFuncMap returns sprig.TxtFuncMap but removing the "env" and "expandenv" +// functions to avoid any leak of information. +func StepFuncMap() template.FuncMap { + m := sprig.TxtFuncMap() + delete(m, "env") + delete(m, "expandenv") + return m +}