Merge branch 'master' into herman/improve-scep-marshaling
This commit is contained in:
commit
f9ec62f46c
77 changed files with 6086 additions and 3247 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -23,4 +23,5 @@ jobs:
|
|||
os-dependencies: "libpcsclite-dev"
|
||||
run-gitleaks: true
|
||||
run-codeql: true
|
||||
make-test: true # run `make test` instead of the default test workflow
|
||||
secrets: inherit
|
||||
|
|
22
.github/workflows/dependabot-auto-merge.yml
vendored
Normal file
22
.github/workflows/dependabot-auto-merge.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
name: Dependabot auto-merge
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v1.1.1
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Enable auto-merge for Dependabot PRs
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
|
@ -21,6 +21,7 @@ jobs:
|
|||
version: ${{ steps.extract-tag.outputs.VERSION }}
|
||||
is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
|
||||
docker_tags: ${{ env.DOCKER_TAGS }}
|
||||
docker_tags_hsm: ${{ env.DOCKER_TAGS_HSM }}
|
||||
steps:
|
||||
- name: Is Pre-release
|
||||
id: is_prerelease
|
||||
|
@ -36,10 +37,12 @@ jobs:
|
|||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
|
||||
echo "DOCKER_TAGS=${{ env.DOCKER_IMAGE }}:${VERSION}" >> ${GITHUB_ENV}
|
||||
echo "DOCKER_TAGS_HSM=${{ env.DOCKER_IMAGE }}:${VERSION}-hsm" >> ${GITHUB_ENV}
|
||||
- name: Add Latest Tag
|
||||
if: steps.is_prerelease.outputs.IS_PRERELEASE == 'false'
|
||||
run: |
|
||||
echo "DOCKER_TAGS=${{ env.DOCKER_TAGS }},${{ env.DOCKER_IMAGE }}:latest" >> ${GITHUB_ENV}
|
||||
echo "DOCKER_TAGS_HSM=${{ env.DOCKER_TAGS_HSM }},${{ env.DOCKER_IMAGE }}:hsm" >> ${GITHUB_ENV}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
|
@ -79,7 +82,7 @@ jobs:
|
|||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: 'latest'
|
||||
args: release --rm-dist
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GORELEASER_PAT }}
|
||||
RELEASE_DATE: ${{ env.RELEASE_DATE }}
|
||||
|
@ -96,5 +99,19 @@ jobs:
|
|||
platforms: linux/amd64,linux/386,linux/arm,linux/arm64
|
||||
tags: ${{ needs.create_release.outputs.docker_tags }}
|
||||
docker_image: smallstep/step-ca
|
||||
docker_file: docker/Dockerfile.step-ca
|
||||
docker_file: docker/Dockerfile
|
||||
secrets: inherit
|
||||
|
||||
build_upload_docker_hsm:
|
||||
name: Build & Upload HSM Enabled Docker Images
|
||||
needs: create_release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm,linux/arm64
|
||||
tags: ${{ needs.create_release.outputs.docker_tags_hsm }}
|
||||
docker_image: smallstep/step-ca
|
||||
docker_file: docker/Dockerfile.hsm
|
||||
secrets: inherit
|
||||
|
|
|
@ -36,6 +36,7 @@ archives:
|
|||
# Most common use case is to archive as zip on Windows.
|
||||
# Default is empty.
|
||||
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
|
||||
rlcp: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
@ -78,6 +79,7 @@ nfpms:
|
|||
|
||||
source:
|
||||
enabled: true
|
||||
rlcp: true
|
||||
name_template: '{{ .ProjectName }}_{{ .Version }}'
|
||||
|
||||
checksum:
|
||||
|
@ -140,7 +142,7 @@ release:
|
|||
|
||||
#### Windows
|
||||
|
||||
- 📦 [step-ca_windows_{{ .Version }}_arm64.zip](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_windows_{{ .Version }}_amd64.zip)
|
||||
- 📦 [step-ca_windows_{{ .Version }}_amd64.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).
|
||||
|
@ -154,9 +156,11 @@ release:
|
|||
Below is an example using `cosign` to verify a release artifact:
|
||||
|
||||
```
|
||||
COSIGN_EXPERIMENTAL=1 cosign verify-blob \
|
||||
cosign verify-blob \
|
||||
--certificate ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig.pem \
|
||||
--signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig \
|
||||
--certificate-identity-regexp "https://github\.com/smallstep/certificates/.*" \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz
|
||||
```
|
||||
|
||||
|
@ -185,3 +189,40 @@ release:
|
|||
# - glob: ./path/to/file.txt
|
||||
# - glob: ./glob/**/to/**/file/**/*
|
||||
# - glob: ./glob/foo/to/bar/file/foobar/override_from_previous
|
||||
|
||||
scoop:
|
||||
# Template for the url which is determined by the given Token (github or gitlab)
|
||||
# Default for github is "https://github.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
# Default for gitlab is "https://gitlab.com/<repo_owner>/<repo_name>/uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}"
|
||||
# Default for gitea is "https://gitea.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
url_template: "http://github.com/smallstep/certificates/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
|
||||
# Repository to push the app manifest to.
|
||||
bucket:
|
||||
owner: smallstep
|
||||
name: scoop-bucket
|
||||
|
||||
# Git author used to commit to the repository.
|
||||
# Defaults are shown.
|
||||
commit_author:
|
||||
name: goreleaserbot
|
||||
email: goreleaser@smallstep.com
|
||||
|
||||
# The project name and current git tag are used in the format string.
|
||||
commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}"
|
||||
|
||||
# Your app's homepage.
|
||||
# Default is empty.
|
||||
homepage: "https://smallstep.com/docs/step-ca"
|
||||
|
||||
# Skip uploads for prerelease.
|
||||
skip_upload: auto
|
||||
|
||||
# Your app's description.
|
||||
# Default is empty.
|
||||
description: "A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management, so you can use TLS everywhere & SSO for SSH."
|
||||
|
||||
# Your app's license
|
||||
# Default is empty.
|
||||
license: "Apache-2.0"
|
||||
|
||||
|
|
77
CHANGELOG.md
77
CHANGELOG.md
|
@ -25,12 +25,85 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
## [v0.24.0] - 2023-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add ACME `device-attest-01` support with TPM 2.0
|
||||
(smallstep/certificates#1063).
|
||||
- Add support for new Azure SDK, sovereign clouds, and HSM keys on Azure KMS
|
||||
(smallstep/crypto#192, smallstep/crypto#197, smallstep/crypto#198,
|
||||
smallstep/certificates#1323, smallstep/certificates#1309).
|
||||
- Add support for ASN.1 functions on certificate templates
|
||||
(smallstep/crypto#208, smallstep/certificates#1345)
|
||||
- Add `DOCKER_STEPCA_INIT_ADDRESS` to configure the address to use in a docker
|
||||
container (smallstep/certificates#1262).
|
||||
- Make sure that the CSR used matches the attested key when using AME
|
||||
`device-attest-01` challenge (smallstep/certificates#1265).
|
||||
- Add support for compacting the Badger DB (smallstep/certificates#1298).
|
||||
- Build and release cleanups (smallstep/certificates#1322,
|
||||
smallstep/certificates#1329, smallstep/certificates#1340).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix support for PKCS #7 RSA-OAEP decryption through
|
||||
[smallstep/pkcs7#4](https://github.com/smallstep/pkcs7/pull/4), as used in
|
||||
SCEP.
|
||||
- Fix RA installation using `scripts/install-step-ra.sh`
|
||||
(smallstep/certificates#1255).
|
||||
- Clarify error messages on policy errors (smallstep/certificates#1287,
|
||||
smallstep/certificates#1278).
|
||||
- Clarify error message on OIDC email validation (smallstep/certificates#1290).
|
||||
- Mark the IDP critical in the generated CRL data (smallstep/certificates#1293).
|
||||
- Disable database if CA is initialized with the `--no-db` flag
|
||||
(smallstep/certificates#1294).
|
||||
|
||||
## [v0.23.2] - 2023-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- Added [`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) to
|
||||
docker images, and a new image, `smallstep/step-ca-hsm`, compiled with cgo
|
||||
(smallstep/certificates#1243).
|
||||
- Added [`scoop`](https://scoop.sh) packages back to the release
|
||||
(smallstep/certificates#1250).
|
||||
- Added optional flag `--pidfile` which allows passing a filename where step-ca
|
||||
will write its process id (smallstep/certificates#1251).
|
||||
- Added helpful message on CA startup when config can't be opened
|
||||
(smallstep/certificates#1252).
|
||||
- Improved validation and error messages on `device-attest-01` orders
|
||||
(smallstep/certificates#1235).
|
||||
|
||||
### Removed
|
||||
|
||||
- The deprecated CLI utils `step-awskms-init`, `step-cloudkms-init`,
|
||||
`step-pkcs11-init`, `step-yubikey-init` have been removed.
|
||||
[`step`](https://github.com/smallstep/cli) and
|
||||
[`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) should be
|
||||
used instead (smallstep/certificates#1240).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed remote management flags in docker images (smallstep/certificates#1228).
|
||||
|
||||
## [v0.23.1] - 2023-01-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added configuration property `.crl.idpURL` to be able to set a custom Issuing
|
||||
Distribution Point in the CRL.
|
||||
Distribution Point in the CRL (smallstep/certificates#1178).
|
||||
- Added WithContext methods to the CA client (smallstep/certificates#1211).
|
||||
- Docker: Added environment variables for enabling Remote Management and ACME
|
||||
provisioner (smallstep/certificates#1201).
|
||||
- Docker: The entrypoint script now generates and displays an initial JWK
|
||||
provisioner password by default when the CA is being initialized
|
||||
(smallstep/certificates#1223).
|
||||
|
||||
### Changed
|
||||
|
||||
- Ignore SSH principals validation when using an OIDC provisioner. The
|
||||
provisioner will ignore the principals passed and set the defaults or the ones
|
||||
including using WebHooks or templates (smallstep/certificates#1206).
|
||||
|
||||
## [v0.23.0] - 2022-11-11
|
||||
|
||||
|
|
99
Makefile
99
Makefile
|
@ -1,21 +1,11 @@
|
|||
PKG?=github.com/smallstep/certificates/cmd/step-ca
|
||||
BINNAME?=step-ca
|
||||
CLOUDKMS_BINNAME?=step-cloudkms-init
|
||||
CLOUDKMS_PKG?=github.com/smallstep/certificates/cmd/step-cloudkms-init
|
||||
AWSKMS_BINNAME?=step-awskms-init
|
||||
AWSKMS_PKG?=github.com/smallstep/certificates/cmd/step-awskms-init
|
||||
YUBIKEY_BINNAME?=step-yubikey-init
|
||||
YUBIKEY_PKG?=github.com/smallstep/certificates/cmd/step-yubikey-init
|
||||
PKCS11_BINNAME?=step-pkcs11-init
|
||||
PKCS11_PKG?=github.com/smallstep/certificates/cmd/step-pkcs11-init
|
||||
|
||||
# Set V to 1 for verbose output from the Makefile
|
||||
Q=$(if $V,,@)
|
||||
PREFIX?=
|
||||
SRC=$(shell find . -type f -name '*.go' -not -path "./vendor/*")
|
||||
GOOS_OVERRIDE ?=
|
||||
OUTPUT_ROOT=output/
|
||||
RELEASE=./.releases
|
||||
|
||||
all: lint test build
|
||||
|
||||
|
@ -31,6 +21,8 @@ bootstra%:
|
|||
$Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin latest
|
||||
$Q go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
$Q go install gotest.tools/gotestsum@latest
|
||||
$Q go install github.com/goreleaser/goreleaser@latest
|
||||
$Q go install github.com/sigstore/cosign/v2/cmd/cosign@latest
|
||||
|
||||
.PHONY: bootstra%
|
||||
|
||||
|
@ -38,17 +30,8 @@ bootstra%:
|
|||
# Determine the type of `push` and `version`
|
||||
#################################################
|
||||
|
||||
# If TRAVIS_TAG is set then we know this ref has been tagged.
|
||||
ifdef TRAVIS_TAG
|
||||
VERSION ?= $(TRAVIS_TAG)
|
||||
NOT_RC := $(shell echo $(VERSION) | grep -v -e -rc)
|
||||
ifeq ($(NOT_RC),)
|
||||
PUSHTYPE := release-candidate
|
||||
else
|
||||
PUSHTYPE := release
|
||||
endif
|
||||
# GITHUB Actions
|
||||
else ifdef GITHUB_REF
|
||||
ifdef GITHUB_REF
|
||||
VERSION ?= $(shell echo $(GITHUB_REF) | sed 's/^refs\/tags\///')
|
||||
NOT_RC := $(shell echo $(VERSION) | grep -v -e -rc)
|
||||
ifeq ($(NOT_RC),)
|
||||
|
@ -61,21 +44,14 @@ VERSION ?= $(shell [ -d .git ] && git describe --tags --always --dirty="-dev")
|
|||
# If we are not in an active git dir then try reading the version from .VERSION.
|
||||
# .VERSION contains a slug populated by `git archive`.
|
||||
VERSION := $(or $(VERSION),$(shell ./.version.sh .VERSION))
|
||||
ifeq ($(TRAVIS_BRANCH),master)
|
||||
PUSHTYPE := master
|
||||
else
|
||||
PUSHTYPE := branch
|
||||
endif
|
||||
endif
|
||||
|
||||
VERSION := $(shell echo $(VERSION) | sed 's/^v//')
|
||||
DEB_VERSION := $(shell echo $(VERSION) | sed 's/-/./g')
|
||||
|
||||
ifdef V
|
||||
$(info TRAVIS_TAG is $(TRAVIS_TAG))
|
||||
$(info GITHUB_REF is $(GITHUB_REF))
|
||||
$(info VERSION is $(VERSION))
|
||||
$(info DEB_VERSION is $(DEB_VERSION))
|
||||
$(info PUSHTYPE is $(PUSHTYPE))
|
||||
endif
|
||||
|
||||
|
@ -90,29 +66,13 @@ GOFLAGS := CGO_ENABLED=0
|
|||
download:
|
||||
$Q go mod download
|
||||
|
||||
build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(AWSKMS_BINNAME) $(PREFIX)bin/$(YUBIKEY_BINNAME) $(PREFIX)bin/$(PKCS11_BINNAME)
|
||||
build: $(PREFIX)bin/$(BINNAME)
|
||||
@echo "Build Complete!"
|
||||
|
||||
$(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG)
|
||||
|
||||
$(PREFIX)bin/$(CLOUDKMS_BINNAME): download $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(LDFLAGS) $(CLOUDKMS_PKG)
|
||||
|
||||
$(PREFIX)bin/$(AWSKMS_BINNAME): download $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(AWSKMS_BINNAME) $(LDFLAGS) $(AWSKMS_PKG)
|
||||
|
||||
$(PREFIX)bin/$(YUBIKEY_BINNAME): download $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(YUBIKEY_BINNAME) $(LDFLAGS) $(YUBIKEY_PKG)
|
||||
|
||||
$(PREFIX)bin/$(PKCS11_BINNAME): download $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(PKCS11_BINNAME) $(LDFLAGS) $(PKCS11_PKG)
|
||||
|
||||
# Target to force a build of step-ca without running tests
|
||||
simple: build
|
||||
|
||||
|
@ -130,14 +90,21 @@ generate:
|
|||
#########################################
|
||||
# Test
|
||||
#########################################
|
||||
test:
|
||||
$Q $(GOFLAGS) gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...
|
||||
test: testdefault testtpmsimulator combinecoverage
|
||||
|
||||
testdefault:
|
||||
$Q $(GOFLAGS) gotestsum -- -coverprofile=defaultcoverage.out -short -covermode=atomic ./...
|
||||
|
||||
testtpmsimulator:
|
||||
$Q CGO_ENALBED=1 gotestsum -- -coverprofile=tpmsimulatorcoverage.out -short -covermode=atomic -tags tpmsimulator ./acme
|
||||
|
||||
testcgo:
|
||||
$Q gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...
|
||||
|
||||
.PHONY: test testcgo
|
||||
combinecoverage:
|
||||
cat defaultcoverage.out tpmsimulatorcoverage.out > coverage.out
|
||||
|
||||
.PHONY: test testdefault testtpmsimulator testcgo combinecoverage
|
||||
|
||||
integrate: integration
|
||||
|
||||
|
@ -166,15 +133,11 @@ lint:
|
|||
|
||||
INSTALL_PREFIX?=/usr/
|
||||
|
||||
install: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(AWSKMS_BINNAME)
|
||||
install: $(PREFIX)bin/$(BINNAME)
|
||||
$Q install -D $(PREFIX)bin/$(BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(BINNAME)
|
||||
$Q install -D $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(CLOUDKMS_BINNAME)
|
||||
$Q install -D $(PREFIX)bin/$(AWSKMS_BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(AWSKMS_BINNAME)
|
||||
|
||||
uninstall:
|
||||
$Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BINNAME)
|
||||
$Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(CLOUDKMS_BINNAME)
|
||||
$Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(AWSKMS_BINNAME)
|
||||
|
||||
.PHONY: install uninstall
|
||||
|
||||
|
@ -186,18 +149,6 @@ clean:
|
|||
ifneq ($(BINNAME),"")
|
||||
$Q rm -f bin/$(BINNAME)
|
||||
endif
|
||||
ifneq ($(CLOUDKMS_BINNAME),"")
|
||||
$Q rm -f bin/$(CLOUDKMS_BINNAME)
|
||||
endif
|
||||
ifneq ($(AWSKMS_BINNAME),"")
|
||||
$Q rm -f bin/$(AWSKMS_BINNAME)
|
||||
endif
|
||||
ifneq ($(YUBIKEY_BINNAME),"")
|
||||
$Q rm -f bin/$(YUBIKEY_BINNAME)
|
||||
endif
|
||||
ifneq ($(PKCS11_BINNAME),"")
|
||||
$Q rm -f bin/$(PKCS11_BINNAME)
|
||||
endif
|
||||
|
||||
.PHONY: clean
|
||||
|
||||
|
@ -210,23 +161,3 @@ run:
|
|||
|
||||
.PHONY: run
|
||||
|
||||
#########################################
|
||||
# Debian
|
||||
#########################################
|
||||
|
||||
changelog:
|
||||
$Q echo "step-ca ($(DEB_VERSION)) unstable; urgency=medium" > debian/changelog
|
||||
$Q echo >> debian/changelog
|
||||
$Q echo " * See https://github.com/smallstep/certificates/releases" >> debian/changelog
|
||||
$Q echo >> debian/changelog
|
||||
$Q echo " -- Smallstep Labs, Inc. <techadmin@smallstep.com> $(shell date -uR)" >> debian/changelog
|
||||
|
||||
debian: changelog
|
||||
$Q mkdir -p $(RELEASE); \
|
||||
OUTPUT=../step-ca*.deb; \
|
||||
rm $$OUTPUT; \
|
||||
dpkg-buildpackage -b -rfakeroot -us -uc && cp $$OUTPUT $(RELEASE)/
|
||||
|
||||
distclean: clean
|
||||
|
||||
.PHONY: changelog debian distclean
|
||||
|
|
|
@ -135,7 +135,6 @@ func TestExternalAccountKey_BindTo(t *testing.T) {
|
|||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.Equals(t, ae.Type, tt.err.Type)
|
||||
assert.Equals(t, ae.Detail, tt.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tt.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -388,7 +388,6 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -828,7 +827,6 @@ func TestHandler_NewAccount(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1032,7 +1030,6 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
|
|
@ -866,7 +866,6 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
|||
assert.Equals(t, ae.Status, tc.err.Status)
|
||||
assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
}
|
||||
} else {
|
||||
|
@ -1145,7 +1144,6 @@ func Test_validateEABJWS(t *testing.T) {
|
|||
assert.Equals(t, tc.err.Status, err.Status)
|
||||
assert.HasPrefix(t, err.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, tc.err.Detail, err.Detail)
|
||||
assert.Equals(t, tc.err.Identifier, err.Identifier)
|
||||
assert.Equals(t, tc.err.Subproblems, err.Subproblems)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
|
|
|
@ -193,7 +193,6 @@ func TestHandler_GetDirectory(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -366,7 +365,6 @@ func TestHandler_GetAuthorization(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -509,7 +507,6 @@ func TestHandler_GetCertificate(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.HasPrefix(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -768,7 +765,6 @@ func TestHandler_GetChallenge(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
|
|
@ -93,7 +93,6 @@ func TestHandler_addNonce(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -147,7 +146,6 @@ func TestHandler_addDirLink(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -252,7 +250,6 @@ func TestHandler_verifyContentType(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -320,7 +317,6 @@ func TestHandler_isPostAsGet(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -410,7 +406,6 @@ func TestHandler_parseJWS(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -606,7 +601,6 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -808,7 +802,6 @@ func TestHandler_lookupJWK(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1008,7 +1001,6 @@ func TestHandler_extractJWK(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1384,7 +1376,6 @@ func TestHandler_validateJWS(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1567,7 +1558,6 @@ func TestHandler_extractOrLookupJWK(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1652,7 +1642,6 @@ func TestHandler_checkPrerequisites(t *testing.T) {
|
|||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
|
|
@ -486,7 +486,6 @@ func TestHandler_GetOrder(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1846,7 +1845,6 @@ func TestHandler_NewOrder(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -2144,7 +2142,6 @@ func TestHandler_FinalizeOrder(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
|
|
@ -1090,7 +1090,6 @@ func TestHandler_RevokeCert(t *testing.T) {
|
|||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
|
@ -1230,7 +1229,6 @@ func TestHandler_isAccountAuthorized(t *testing.T) {
|
|||
assert.Equals(t, acmeErr.Type, tc.err.Type)
|
||||
assert.Equals(t, acmeErr.Status, tc.err.Status)
|
||||
assert.Equals(t, acmeErr.Detail, tc.err.Detail)
|
||||
assert.Equals(t, acmeErr.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, acmeErr.Subproblems, tc.err.Subproblems)
|
||||
|
||||
})
|
||||
|
@ -1323,7 +1321,6 @@ func Test_wrapUnauthorizedError(t *testing.T) {
|
|||
assert.Equals(t, acmeErr.Type, tc.want.Type)
|
||||
assert.Equals(t, acmeErr.Status, tc.want.Status)
|
||||
assert.Equals(t, acmeErr.Detail, tc.want.Detail)
|
||||
assert.Equals(t, acmeErr.Identifier, tc.want.Identifier)
|
||||
assert.Equals(t, acmeErr.Subproblems, tc.want.Subproblems)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,15 +8,16 @@ import (
|
|||
|
||||
// Authorization representst an ACME Authorization.
|
||||
type Authorization struct {
|
||||
ID string `json:"-"`
|
||||
AccountID string `json:"-"`
|
||||
Token string `json:"-"`
|
||||
Identifier Identifier `json:"identifier"`
|
||||
Status Status `json:"status"`
|
||||
Challenges []*Challenge `json:"challenges"`
|
||||
Wildcard bool `json:"wildcard"`
|
||||
ExpiresAt time.Time `json:"expires"`
|
||||
Error *Error `json:"error,omitempty"`
|
||||
ID string `json:"-"`
|
||||
AccountID string `json:"-"`
|
||||
Token string `json:"-"`
|
||||
Fingerprint string `json:"-"`
|
||||
Identifier Identifier `json:"identifier"`
|
||||
Status Status `json:"status"`
|
||||
Challenges []*Challenge `json:"challenges"`
|
||||
Wildcard bool `json:"wildcard"`
|
||||
ExpiresAt time.Time `json:"expires"`
|
||||
Error *Error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ToLog enables response logging.
|
||||
|
|
|
@ -26,9 +26,16 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/google/go-attestation/attest"
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
type ChallengeType string
|
||||
|
@ -79,10 +86,9 @@ func (ch *Challenge) ToLog() (interface{}, error) {
|
|||
return string(b), nil
|
||||
}
|
||||
|
||||
// Validate attempts to validate the challenge. Stores changes to the Challenge
|
||||
// type using the DB interface.
|
||||
// satisfactorily validated, the 'status' and 'validated' attributes are
|
||||
// updated.
|
||||
// Validate attempts to validate the Challenge. Stores changes to the Challenge
|
||||
// type using the DB interface. If the Challenge is validated, the 'status' and
|
||||
// 'validated' attributes are updated.
|
||||
func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, payload []byte) error {
|
||||
// If already valid or invalid then return without performing validation.
|
||||
if ch.Status != StatusPending {
|
||||
|
@ -335,20 +341,26 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK
|
|||
return nil
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
type payloadType struct {
|
||||
AttObj string `json:"attObj"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type AttestationObject struct {
|
||||
type attestationObject struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}
|
||||
|
||||
// TODO(bweeks): move attestation verification to a shared package.
|
||||
// TODO(bweeks): define new error type for failed attestation validation.
|
||||
func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error {
|
||||
var p Payload
|
||||
// Load authorization to store the key fingerprint.
|
||||
az, err := db.GetAuthorization(ctx, ch.AuthorizationID)
|
||||
if err != nil {
|
||||
return WrapErrorISE(err, "error loading authorization")
|
||||
}
|
||||
|
||||
// Parse payload.
|
||||
var p payloadType
|
||||
if err := json.Unmarshal(payload, &p); err != nil {
|
||||
return WrapErrorISE(err, "error unmarshalling JSON")
|
||||
}
|
||||
|
@ -362,7 +374,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
return WrapErrorISE(err, "error base64 decoding attObj")
|
||||
}
|
||||
|
||||
att := AttestationObject{}
|
||||
att := attestationObject{}
|
||||
if err := cbor.Unmarshal(attObj, &att); err != nil {
|
||||
return WrapErrorISE(err, "error unmarshalling CBOR")
|
||||
}
|
||||
|
@ -386,7 +398,6 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
}
|
||||
return WrapErrorISE(err, "error validating attestation")
|
||||
}
|
||||
|
||||
// Validate nonce with SHA-256 of the token.
|
||||
if len(data.Nonce) != 0 {
|
||||
sum := sha256.Sum256([]byte(ch.Token))
|
||||
|
@ -402,6 +413,9 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
if data.UDID != ch.Value && data.SerialNumber != ch.Value {
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match"))
|
||||
}
|
||||
|
||||
// Update attestation key fingerprint to compare against the CSR
|
||||
az.Fingerprint = data.Fingerprint
|
||||
case "step":
|
||||
data, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att)
|
||||
if err != nil {
|
||||
|
@ -415,13 +429,53 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
return WrapErrorISE(err, "error validating attestation")
|
||||
}
|
||||
|
||||
// Validate Apple's ClientIdentifier (Identifier.Value) with device
|
||||
// identifiers.
|
||||
// Validate the YubiKey serial number from the attestation
|
||||
// certificate with the challenged Order value.
|
||||
//
|
||||
// Note: We might want to use an external service for this.
|
||||
if data.SerialNumber != ch.Value {
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match"))
|
||||
subproblem := NewSubproblemWithIdentifier(
|
||||
ErrorMalformedType,
|
||||
Identifier{Type: "permanent-identifier", Value: ch.Value},
|
||||
"challenge identifier %q doesn't match the attested hardware identifier %q", ch.Value, data.SerialNumber,
|
||||
)
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem))
|
||||
}
|
||||
|
||||
// Update attestation key fingerprint to compare against the CSR
|
||||
az.Fingerprint = data.Fingerprint
|
||||
|
||||
case "tpm":
|
||||
data, err := doTPMAttestationFormat(ctx, prov, ch, jwk, &att)
|
||||
if err != nil {
|
||||
// TODO(hs): we should provide more details in the error reported to the client;
|
||||
// "Attestation statement cannot be verified" is VERY generic. Also holds true for the other formats.
|
||||
var acmeError *Error
|
||||
if errors.As(err, &acmeError) {
|
||||
if acmeError.Status == 500 {
|
||||
return acmeError
|
||||
}
|
||||
return storeError(ctx, db, ch, true, acmeError)
|
||||
}
|
||||
return WrapErrorISE(err, "error validating attestation")
|
||||
}
|
||||
|
||||
// TODO(hs): currently this will allow a request for which no PermanentIdentifiers have been
|
||||
// extracted from the AK certificate. This is currently the case for AK certs from the CLI, as we
|
||||
// haven't implemented a way for AK certs requested by the CLI to always contain the requested
|
||||
// PermanentIdentifier. Omitting the check below doesn't allow just any request, as the Order can
|
||||
// still fail if the challenge value isn't equal to the CSR subject.
|
||||
if len(data.PermanentIdentifiers) > 0 && !slices.Contains(data.PermanentIdentifiers, ch.Value) { // TODO(hs): add support for HardwareModuleName
|
||||
subproblem := NewSubproblemWithIdentifier(
|
||||
ErrorMalformedType,
|
||||
Identifier{Type: "permanent-identifier", Value: ch.Value},
|
||||
"challenge identifier %q doesn't match any of the attested hardware identifiers %q", ch.Value, data.PermanentIdentifiers,
|
||||
)
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, "permanent identifier does not match").AddSubproblems(subproblem))
|
||||
}
|
||||
|
||||
// Update attestation key fingerprint to compare against the CSR
|
||||
az.Fingerprint = data.Fingerprint
|
||||
default:
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "unexpected attestation object format"))
|
||||
}
|
||||
|
@ -431,12 +485,316 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
ch.Error = nil
|
||||
ch.ValidatedAt = clock.Now().Format(time.RFC3339)
|
||||
|
||||
// Store the fingerprint in the authorization.
|
||||
//
|
||||
// TODO: add method to update authorization and challenge atomically.
|
||||
if az.Fingerprint != "" {
|
||||
if err := db.UpdateAuthorization(ctx, az); err != nil {
|
||||
return WrapErrorISE(err, "error updating authorization")
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.UpdateChallenge(ctx, ch); err != nil {
|
||||
return WrapErrorISE(err, "error updating challenge")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
oidSubjectAlternativeName = asn1.ObjectIdentifier{2, 5, 29, 17}
|
||||
)
|
||||
|
||||
type tpmAttestationData struct {
|
||||
Certificate *x509.Certificate
|
||||
VerifiedChains [][]*x509.Certificate
|
||||
PermanentIdentifiers []string
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
// coseAlgorithmIdentifier models a COSEAlgorithmIdentifier.
|
||||
// Also see https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier.
|
||||
type coseAlgorithmIdentifier int32
|
||||
|
||||
const (
|
||||
coseAlgES256 coseAlgorithmIdentifier = -7
|
||||
coseAlgRS256 coseAlgorithmIdentifier = -257
|
||||
)
|
||||
|
||||
func doTPMAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*tpmAttestationData, error) {
|
||||
ver, ok := att.AttStatement["ver"].(string)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "ver not present")
|
||||
}
|
||||
if ver != "2.0" {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "version %q is not supported", ver)
|
||||
}
|
||||
|
||||
x5c, ok := att.AttStatement["x5c"].([]interface{})
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c not present")
|
||||
}
|
||||
if len(x5c) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c is empty")
|
||||
}
|
||||
|
||||
akCertBytes, ok := x5c[0].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed")
|
||||
}
|
||||
akCert, err := x509.ParseCertificate(akCertBytes)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed")
|
||||
}
|
||||
|
||||
intermediates := x509.NewCertPool()
|
||||
for _, v := range x5c[1:] {
|
||||
intCertBytes, vok := v.([]byte)
|
||||
if !vok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed")
|
||||
}
|
||||
intCert, err := x509.ParseCertificate(intCertBytes)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed")
|
||||
}
|
||||
intermediates.AddCert(intCert)
|
||||
}
|
||||
|
||||
// TODO(hs): this can be removed when permanent-identifier/hardware-module-name are handled correctly in
|
||||
// the stdlib in https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/crypto/x509/parser.go;drc=b5b2cf519fe332891c165077f3723ee74932a647;l=362,
|
||||
// but I doubt that will happen.
|
||||
if len(akCert.UnhandledCriticalExtensions) > 0 {
|
||||
unhandledCriticalExtensions := akCert.UnhandledCriticalExtensions[:0]
|
||||
for _, extOID := range akCert.UnhandledCriticalExtensions {
|
||||
if !extOID.Equal(oidSubjectAlternativeName) {
|
||||
// critical extensions other than the Subject Alternative Name remain unhandled
|
||||
unhandledCriticalExtensions = append(unhandledCriticalExtensions, extOID)
|
||||
}
|
||||
}
|
||||
akCert.UnhandledCriticalExtensions = unhandledCriticalExtensions
|
||||
}
|
||||
|
||||
roots, ok := prov.GetAttestationRoots()
|
||||
if !ok {
|
||||
return nil, NewErrorISE("no root CA bundle available to verify the attestation certificate")
|
||||
}
|
||||
|
||||
// verify that the AK certificate was signed by a trusted root,
|
||||
// chained to by the intermediates provided by the client. As part
|
||||
// of building the verified certificate chain, the signature over the
|
||||
// AK certificate is checked to be a valid signature of one of the
|
||||
// provided intermediates. Signatures over the intermediates are in
|
||||
// turn also verified to be valid signatures from one of the trusted
|
||||
// roots.
|
||||
verifiedChains, err := akCert.Verify(x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
CurrentTime: time.Now().Truncate(time.Second),
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is not valid")
|
||||
}
|
||||
|
||||
// validate additional AK certificate requirements
|
||||
if err := validateAKCertificate(akCert); err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "AK certificate is not valid")
|
||||
}
|
||||
|
||||
// TODO(hs): implement revocation check; Verify() doesn't perform CRL check nor OCSP lookup.
|
||||
|
||||
sans, err := x509util.ParseSubjectAlternativeNames(akCert)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed parsing AK certificate Subject Alternative Names")
|
||||
}
|
||||
|
||||
permanentIdentifiers := make([]string, len(sans.PermanentIdentifiers))
|
||||
for i, pi := range sans.PermanentIdentifiers {
|
||||
permanentIdentifiers[i] = pi.Identifier
|
||||
}
|
||||
|
||||
// extract and validate pubArea, sig, certInfo and alg properties from the request body
|
||||
pubArea, ok := att.AttStatement["pubArea"].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid pubArea in attestation statement")
|
||||
}
|
||||
if len(pubArea) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "pubArea is empty")
|
||||
}
|
||||
|
||||
sig, ok := att.AttStatement["sig"].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid sig in attestation statement")
|
||||
}
|
||||
if len(sig) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "sig is empty")
|
||||
}
|
||||
|
||||
certInfo, ok := att.AttStatement["certInfo"].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid certInfo in attestation statement")
|
||||
}
|
||||
if len(certInfo) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "certInfo is empty")
|
||||
}
|
||||
|
||||
alg, ok := att.AttStatement["alg"].(int64)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid alg in attestation statement")
|
||||
}
|
||||
|
||||
// only RS256 and ES256 are allowed
|
||||
coseAlg := coseAlgorithmIdentifier(alg)
|
||||
if coseAlg != coseAlgRS256 && coseAlg != coseAlgES256 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid alg %d in attestation statement", alg)
|
||||
}
|
||||
|
||||
// set the hash algorithm to use to SHA256
|
||||
hash := crypto.SHA256
|
||||
|
||||
// recreate the generated key certification parameter values and verify
|
||||
// the attested key using the public key of the AK.
|
||||
certificationParameters := &attest.CertificationParameters{
|
||||
Public: pubArea, // the public key that was attested
|
||||
CreateAttestation: certInfo, // the attested properties of the key
|
||||
CreateSignature: sig, // signature over the attested properties
|
||||
}
|
||||
verifyOpts := attest.VerifyOpts{
|
||||
Public: akCert.PublicKey, // public key of the AK that attested the key
|
||||
Hash: hash,
|
||||
}
|
||||
if err = certificationParameters.Verify(verifyOpts); err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "invalid certification parameters")
|
||||
}
|
||||
|
||||
// decode the "certInfo" data. This won't fail, as it's also done as part of Verify().
|
||||
tpmCertInfo, err := tpm2.DecodeAttestationData(certInfo)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed decoding attestation data")
|
||||
}
|
||||
|
||||
keyAuth, err := KeyAuthorization(ch.Token, jwk)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed creating key auth digest")
|
||||
}
|
||||
hashedKeyAuth := sha256.Sum256([]byte(keyAuth))
|
||||
|
||||
// verify the WebAuthn object contains the expect key authorization digest, which is carried
|
||||
// within the encoded `certInfo` property of the attestation statement.
|
||||
if subtle.ConstantTimeCompare(hashedKeyAuth[:], []byte(tpmCertInfo.ExtraData)) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "key authorization does not match")
|
||||
}
|
||||
|
||||
// decode the (attested) public key and determine its fingerprint. This won't fail, as it's also done as part of Verify().
|
||||
pub, err := tpm2.DecodePublic(pubArea)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed decoding pubArea")
|
||||
}
|
||||
|
||||
publicKey, err := pub.Key()
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed getting public key")
|
||||
}
|
||||
|
||||
data := &tpmAttestationData{
|
||||
Certificate: akCert,
|
||||
VerifiedChains: verifiedChains,
|
||||
PermanentIdentifiers: permanentIdentifiers,
|
||||
}
|
||||
|
||||
if data.Fingerprint, err = keyutil.Fingerprint(publicKey); err != nil {
|
||||
return nil, WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
|
||||
// TODO(hs): pass more attestation data, so that that can be used/recorded too?
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var (
|
||||
oidExtensionExtendedKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}
|
||||
oidTCGKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3}
|
||||
)
|
||||
|
||||
// validateAKCertifiate validates the X.509 AK certificate to be
|
||||
// in accordance with the required properties. The requirements come from:
|
||||
// https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements.
|
||||
//
|
||||
// - Version MUST be set to 3.
|
||||
// - Subject field MUST be set to empty.
|
||||
// - The Subject Alternative Name extension MUST be set as defined
|
||||
// in [TPMv2-EK-Profile] section 3.2.9.
|
||||
// - The Extended Key Usage extension MUST contain the OID 2.23.133.8.3
|
||||
// ("joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)").
|
||||
// - The Basic Constraints extension MUST have the CA component set to false.
|
||||
// - An Authority Information Access (AIA) extension with entry id-ad-ocsp
|
||||
// and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as
|
||||
// the status of many attestation certificates is available through metadata
|
||||
// services. See, for example, the FIDO Metadata Service.
|
||||
func validateAKCertificate(c *x509.Certificate) error {
|
||||
if c.Version != 3 {
|
||||
return fmt.Errorf("AK certificate has invalid version %d; only version 3 is allowed", c.Version)
|
||||
}
|
||||
if c.Subject.String() != "" {
|
||||
return fmt.Errorf("AK certificate subject must be empty; got %q", c.Subject)
|
||||
}
|
||||
if c.IsCA {
|
||||
return errors.New("AK certificate must not be a CA")
|
||||
}
|
||||
if err := validateAKCertificateExtendedKeyUsage(c); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateAKCertificateSubjectAlternativeNames(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAKCertificateSubjectAlternativeNames checks if the AK certificate
|
||||
// has TPM hardware details set.
|
||||
func validateAKCertificateSubjectAlternativeNames(c *x509.Certificate) error {
|
||||
sans, err := x509util.ParseSubjectAlternativeNames(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing AK certificate Subject Alternative Names: %w", err)
|
||||
}
|
||||
|
||||
details := sans.TPMHardwareDetails
|
||||
manufacturer, model, version := details.Manufacturer, details.Model, details.Version
|
||||
|
||||
switch {
|
||||
case manufacturer == "":
|
||||
return errors.New("missing TPM manufacturer")
|
||||
case model == "":
|
||||
return errors.New("missing TPM model")
|
||||
case version == "":
|
||||
return errors.New("missing TPM version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAKCertificateExtendedKeyUsage checks if the AK certificate
|
||||
// has the "tcg-kp-AIKCertificate" Extended Key Usage set.
|
||||
func validateAKCertificateExtendedKeyUsage(c *x509.Certificate) error {
|
||||
var (
|
||||
valid = false
|
||||
ekus []asn1.ObjectIdentifier
|
||||
)
|
||||
for _, ext := range c.Extensions {
|
||||
if ext.Id.Equal(oidExtensionExtendedKeyUsage) {
|
||||
if _, err := asn1.Unmarshal(ext.Value, &ekus); err != nil || !ekus[0].Equal(oidTCGKpAIKCertificate) {
|
||||
return errors.New("AK certificate is missing Extended Key Usage value tcg-kp-AIKCertificate (2.23.133.8.3)")
|
||||
}
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return errors.New("AK certificate is missing Extended Key Usage extension")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apple Enterprise Attestation Root CA from
|
||||
// https://www.apple.com/certificateauthority/private/
|
||||
const appleEnterpriseAttestationRootCA = `-----BEGIN CERTIFICATE-----
|
||||
|
@ -467,9 +825,10 @@ type appleAttestationData struct {
|
|||
UDID string
|
||||
SEPVersion string
|
||||
Certificate *x509.Certificate
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *AttestationObject) (*appleAttestationData, error) {
|
||||
func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *attestationObject) (*appleAttestationData, error) {
|
||||
// Use configured or default attestation roots if none is configured.
|
||||
roots, ok := prov.GetAttestationRoots()
|
||||
if !ok {
|
||||
|
@ -523,6 +882,9 @@ func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challen
|
|||
data := &appleAttestationData{
|
||||
Certificate: leaf,
|
||||
}
|
||||
if data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil {
|
||||
return nil, WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
for _, ext := range leaf.Extensions {
|
||||
switch {
|
||||
case ext.Id.Equal(oidAppleSerialNumber):
|
||||
|
@ -568,9 +930,10 @@ var oidYubicoSerialNumber = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 41482, 3, 7}
|
|||
type stepAttestationData struct {
|
||||
Certificate *x509.Certificate
|
||||
SerialNumber string
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *AttestationObject) (*stepAttestationData, error) {
|
||||
func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*stepAttestationData, error) {
|
||||
// Use configured or default attestation roots if none is configured.
|
||||
roots, ok := prov.GetAttestationRoots()
|
||||
if !ok {
|
||||
|
@ -663,6 +1026,9 @@ func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challeng
|
|||
data := &stepAttestationData{
|
||||
Certificate: leaf,
|
||||
}
|
||||
if data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil {
|
||||
return nil, WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
for _, ext := range leaf.Extensions {
|
||||
if !ext.Id.Equal(oidYubicoSerialNumber) {
|
||||
continue
|
||||
|
@ -726,10 +1092,10 @@ func uitoa(val uint) string {
|
|||
var buf [20]byte // big enough for 64bit value base 10
|
||||
i := len(buf) - 1
|
||||
for val >= 10 {
|
||||
q := val / 10
|
||||
buf[i] = byte('0' + val - q*10)
|
||||
v := val / 10
|
||||
buf[i] = byte('0' + val - v*10)
|
||||
i--
|
||||
val = q
|
||||
val = v
|
||||
}
|
||||
// val < 10
|
||||
buf[i] = byte('0' + val)
|
||||
|
|
File diff suppressed because it is too large
Load diff
859
acme/challenge_tpmsimulator_test.go
Normal file
859
acme/challenge_tpmsimulator_test.go
Normal file
|
@ -0,0 +1,859 @@
|
|||
//go:build tpmsimulator
|
||||
// +build tpmsimulator
|
||||
|
||||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/google/go-attestation/attest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/minica"
|
||||
"go.step.sm/crypto/tpm"
|
||||
"go.step.sm/crypto/tpm/simulator"
|
||||
tpmstorage "go.step.sm/crypto/tpm/storage"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
func newSimulatedTPM(t *testing.T) *tpm.TPM {
|
||||
t.Helper()
|
||||
tmpDir := t.TempDir()
|
||||
tpm, err := tpm.New(withSimulator(t), tpm.WithStore(tpmstorage.NewDirstore(tmpDir))) // TODO: provide in-memory storage implementation instead
|
||||
require.NoError(t, err)
|
||||
return tpm
|
||||
}
|
||||
|
||||
func withSimulator(t *testing.T) tpm.NewTPMOption {
|
||||
t.Helper()
|
||||
var sim simulator.Simulator
|
||||
t.Cleanup(func() {
|
||||
if sim == nil {
|
||||
return
|
||||
}
|
||||
err := sim.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
sim = simulator.New()
|
||||
err := sim.Open()
|
||||
require.NoError(t, err)
|
||||
return tpm.WithSimulator(sim)
|
||||
}
|
||||
|
||||
func generateKeyID(t *testing.T, pub crypto.PublicKey) []byte {
|
||||
t.Helper()
|
||||
b, err := x509.MarshalPKIXPublicKey(pub)
|
||||
require.NoError(t, err)
|
||||
hash := sha256.Sum256(b)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func mustAttestTPM(t *testing.T, keyAuthorization string, permanentIdentifiers []string) ([]byte, crypto.Signer, *x509.Certificate) {
|
||||
t.Helper()
|
||||
aca, err := minica.New(
|
||||
minica.WithName("TPM Testing"),
|
||||
minica.WithGetSignerFunc(
|
||||
func() (crypto.Signer, error) {
|
||||
return keyutil.GenerateSigner("RSA", "", 2048)
|
||||
},
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// prepare simulated TPM and create an AK
|
||||
stpm := newSimulatedTPM(t)
|
||||
eks, err := stpm.GetEKs(context.Background())
|
||||
require.NoError(t, err)
|
||||
ak, err := stpm.CreateAK(context.Background(), "first-ak")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ak)
|
||||
|
||||
// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
|
||||
ap, err := ak.AttestationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create template and sign certificate for the AK public key
|
||||
keyID := generateKeyID(t, eks[0].Public())
|
||||
template := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
sans := []x509util.SubjectAlternativeName{}
|
||||
uris := []*url.URL{{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)}}
|
||||
for _, pi := range permanentIdentifiers {
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.PermanentIdentifierType,
|
||||
Value: pi,
|
||||
})
|
||||
}
|
||||
asn1Value := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.DirectoryNameType,
|
||||
ASN1Value: asn1Value,
|
||||
})
|
||||
ext, err := createSubjectAltNameExtension(nil, nil, nil, uris, sans, true)
|
||||
require.NoError(t, err)
|
||||
ext.Set(template)
|
||||
akCert, err := aca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, akCert)
|
||||
|
||||
// create a new key attested by the AK, while including
|
||||
// the key authorization bytes as qualifying data.
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
||||
config := tpm.AttestKeyConfig{
|
||||
Algorithm: "RSA",
|
||||
Size: 2048,
|
||||
QualifyingData: keyAuthSum[:],
|
||||
}
|
||||
key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
require.Equal(t, "first-key", key.Name())
|
||||
require.NotEqual(t, 0, len(key.Data()))
|
||||
require.Equal(t, "first-ak", key.AttestedBy())
|
||||
require.True(t, key.WasAttested())
|
||||
require.True(t, key.WasAttestedBy(ak))
|
||||
|
||||
signer, err := key.Signer(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// prepare the attestation object with the AK certificate chain,
|
||||
// the attested key, its metadata and the signature signed by the
|
||||
// AK.
|
||||
params, err := key.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
attObj, err := cbor.Marshal(struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// marshal the ACME payload
|
||||
payload, err := json.Marshal(struct {
|
||||
AttObj string `json:"attObj"`
|
||||
}{
|
||||
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return payload, signer, aca.Root
|
||||
}
|
||||
|
||||
func Test_deviceAttest01ValidateWithTPMSimulator(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
ch *Challenge
|
||||
db DB
|
||||
jwk *jose.JSONWebKey
|
||||
payload []byte
|
||||
}
|
||||
type test struct {
|
||||
args args
|
||||
wantErr *Error
|
||||
}
|
||||
tests := map[string]func(t *testing.T) test{
|
||||
"ok/doTPMAttestationFormat-storeError": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, _, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
|
||||
// parse payload, set invalid "ver", remarshal
|
||||
var p payloadType
|
||||
err := json.Unmarshal(payload, &p)
|
||||
require.NoError(t, err)
|
||||
attObj, err := base64.RawURLEncoding.DecodeString(p.AttObj)
|
||||
require.NoError(t, err)
|
||||
att := attestationObject{}
|
||||
err = cbor.Unmarshal(attObj, &att)
|
||||
require.NoError(t, err)
|
||||
att.AttStatement["ver"] = "bogus"
|
||||
attObj, err = cbor.Marshal(struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}{
|
||||
Format: "tpm",
|
||||
AttStatement: att.AttStatement,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
payload, err = json.Marshal(struct {
|
||||
AttObj string `json:"attObj"`
|
||||
}{
|
||||
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusInvalid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
|
||||
err := NewError(ErrorBadAttestationStatementType, `version "bogus" is not supported`)
|
||||
|
||||
assert.EqualError(t, updch.Error.Err, err.Err.Error())
|
||||
assert.Equal(t, err.Type, updch.Error.Type)
|
||||
assert.Equal(t, err.Detail, updch.Error.Detail)
|
||||
assert.Equal(t, err.Status, updch.Error.Status)
|
||||
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok with invalid PermanentIdentifier SAN": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, _, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.99999999",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusInvalid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.99999999", updch.Value)
|
||||
|
||||
err := NewError(ErrorRejectedIdentifierType, `permanent identifier does not match`).
|
||||
AddSubproblems(NewSubproblemWithIdentifier(
|
||||
ErrorMalformedType,
|
||||
Identifier{Type: "permanent-identifier", Value: "device.id.99999999"},
|
||||
`challenge identifier "device.id.99999999" doesn't match any of the attested hardware identifiers ["device.id.12345678"]`,
|
||||
))
|
||||
|
||||
assert.EqualError(t, updch.Error.Err, err.Err.Error())
|
||||
assert.Equal(t, err.Type, updch.Error.Type)
|
||||
assert.Equal(t, err.Detail, updch.Error.Detail)
|
||||
assert.Equal(t, err.Status, updch.Error.Status)
|
||||
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, signer, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "azID", az.ID)
|
||||
assert.Equal(t, fingerprint, az.Fingerprint)
|
||||
return nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusValid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok with PermanentIdentifier SAN": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, signer, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "azID", az.ID)
|
||||
assert.Equal(t, fingerprint, az.Fingerprint)
|
||||
return nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusValid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run(t)
|
||||
|
||||
if err := deviceAttest01Validate(tc.args.ctx, tc.args.ch, tc.args.db, tc.args.jwk, tc.args.payload); err != nil {
|
||||
assert.Error(t, tc.wantErr)
|
||||
assert.EqualError(t, err, tc.wantErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Nil(t, tc.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newBadAttestationStatementError(msg string) *Error {
|
||||
return &Error{
|
||||
Type: "urn:ietf:params:acme:error:badAttestationStatement",
|
||||
Status: 400,
|
||||
Err: errors.New(msg),
|
||||
}
|
||||
}
|
||||
|
||||
func newInternalServerError(msg string) *Error {
|
||||
return &Error{
|
||||
Type: "urn:ietf:params:acme:error:serverInternal",
|
||||
Status: 500,
|
||||
Err: errors.New(msg),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
oidPermanentIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}
|
||||
oidHardwareModuleNameIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 4}
|
||||
)
|
||||
|
||||
func Test_doTPMAttestationFormat(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
aca, err := minica.New(
|
||||
minica.WithName("TPM Testing"),
|
||||
minica.WithGetSignerFunc(
|
||||
func() (crypto.Signer, error) {
|
||||
return keyutil.GenerateSigner("RSA", "", 2048)
|
||||
},
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
acaRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: aca.Root.Raw})
|
||||
|
||||
// prepare simulated TPM and create an AK
|
||||
stpm := newSimulatedTPM(t)
|
||||
eks, err := stpm.GetEKs(context.Background())
|
||||
require.NoError(t, err)
|
||||
ak, err := stpm.CreateAK(context.Background(), "first-ak")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ak)
|
||||
|
||||
// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
|
||||
ap, err := ak.AttestationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create template and sign certificate for the AK public key
|
||||
keyID := generateKeyID(t, eks[0].Public())
|
||||
template := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
sans := []x509util.SubjectAlternativeName{}
|
||||
uris := []*url.URL{{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)}}
|
||||
asn1Value := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.DirectoryNameType,
|
||||
ASN1Value: asn1Value,
|
||||
})
|
||||
ext, err := createSubjectAltNameExtension(nil, nil, nil, uris, sans, true)
|
||||
require.NoError(t, err)
|
||||
ext.Set(template)
|
||||
akCert, err := aca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, akCert)
|
||||
|
||||
invalidTemplate := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
invalidAKCert, err := aca.Sign(invalidTemplate)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, invalidAKCert)
|
||||
|
||||
// generate a JWK and the key authorization value
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
keyAuthorization, err := KeyAuthorization("token", jwk)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a new key attested by the AK, while including
|
||||
// the key authorization bytes as qualifying data.
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
||||
config := tpm.AttestKeyConfig{
|
||||
Algorithm: "RSA",
|
||||
Size: 2048,
|
||||
QualifyingData: keyAuthSum[:],
|
||||
}
|
||||
key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
params, err := key.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
signer, err := key.Signer(context.Background())
|
||||
require.NoError(t, err)
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
// attest another key and get its certification parameters
|
||||
anotherKey, err := stpm.AttestKey(context.Background(), "first-ak", "another-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
anotherKeyParams, err := anotherKey.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
prov Provisioner
|
||||
ch *Challenge
|
||||
jwk *jose.JSONWebKey
|
||||
att *attestationObject
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *tpmAttestationData
|
||||
expErr *Error
|
||||
}{
|
||||
{"ok", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, nil},
|
||||
{"fail ver not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("ver not present")},
|
||||
{"fail ver type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": []interface{}{},
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("ver not present")},
|
||||
{"fail bogus ver", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "bogus",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError(`version "bogus" is not supported`)},
|
||||
{"fail x5c not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c not present")},
|
||||
{"fail x5c type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": [][]byte{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c not present")},
|
||||
{"fail x5c empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is empty")},
|
||||
{"fail leaf type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{"leaf", aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed")},
|
||||
{"fail leaf parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw[:100], aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
|
||||
{"fail intermediate type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, "intermediate"},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed")},
|
||||
{"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw[:100]},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
|
||||
{"fail roots", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newInternalServerError("no root CA bundle available to verify the attestation certificate")},
|
||||
{"fail verify", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is not valid: x509: certificate signed by unknown authority")},
|
||||
{"fail validateAKCertificate", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{invalidAKCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("AK certificate is not valid: missing TPM manufacturer")},
|
||||
{"fail pubArea not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
|
||||
{"fail pubArea type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": []interface{}{},
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
|
||||
{"fail pubArea empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": []byte{},
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("pubArea is empty")},
|
||||
{"fail sig not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
|
||||
{"fail sig type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": []interface{}{},
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
|
||||
{"fail sig empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": []byte{},
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("sig is empty")},
|
||||
{"fail certInfo not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
|
||||
{"fail certInfo type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": []interface{}{},
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
|
||||
{"fail certInfo empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": []byte{},
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("certInfo is empty")},
|
||||
{"fail alg not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid alg in attestation statement")},
|
||||
{"fail alg type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(0), // invalid alg
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid alg 0 in attestation statement")},
|
||||
{"fail attestation verification", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": anotherKeyParams.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certification parameters: certification refers to a different key")},
|
||||
{"fail keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, &jose.JSONWebKey{Key: []byte("not an asymmetric key")}, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newInternalServerError("failed creating key auth digest: error generating JWK thumbprint: square/go-jose: unknown key type '[]uint8'")},
|
||||
{"fail different keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "aDifferentToken"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), //
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("key authorization does not match")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := doTPMAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
|
||||
if tt.expErr != nil {
|
||||
var ae *Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.EqualError(t, err, tt.expErr.Error())
|
||||
assert.Equal(t, ae.StatusCode(), tt.expErr.StatusCode())
|
||||
assert.Equal(t, ae.Type, tt.expErr.Type)
|
||||
}
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, got) {
|
||||
assert.Equal(t, akCert, got.Certificate)
|
||||
assert.Equal(t, [][]*x509.Certificate{
|
||||
{
|
||||
akCert, aca.Intermediate, aca.Root,
|
||||
},
|
||||
}, got.VerifiedChains)
|
||||
assert.Equal(t, fingerprint, got.Fingerprint)
|
||||
assert.Empty(t, got.PermanentIdentifiers) // currently expected to be always empty
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ type dbAuthz struct {
|
|||
Identifier acme.Identifier `json:"identifier"`
|
||||
Status acme.Status `json:"status"`
|
||||
Token string `json:"token"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
ChallengeIDs []string `json:"challengeIDs"`
|
||||
Wildcard bool `json:"wildcard"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
|
@ -61,15 +62,16 @@ func (db *DB) GetAuthorization(ctx context.Context, id string) (*acme.Authorizat
|
|||
}
|
||||
}
|
||||
return &acme.Authorization{
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: chs,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Token: dbaz.Token,
|
||||
Error: dbaz.Error,
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: chs,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Token: dbaz.Token,
|
||||
Fingerprint: dbaz.Fingerprint,
|
||||
Error: dbaz.Error,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -97,6 +99,7 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) e
|
|||
Identifier: az.Identifier,
|
||||
ChallengeIDs: chIDs,
|
||||
Token: az.Token,
|
||||
Fingerprint: az.Fingerprint,
|
||||
Wildcard: az.Wildcard,
|
||||
}
|
||||
|
||||
|
@ -111,8 +114,8 @@ func (db *DB) UpdateAuthorization(ctx context.Context, az *acme.Authorization) e
|
|||
}
|
||||
|
||||
nu := old.clone()
|
||||
|
||||
nu.Status = az.Status
|
||||
nu.Fingerprint = az.Fingerprint
|
||||
nu.Error = az.Error
|
||||
return db.save(ctx, old.ID, nu, old, "authz", authzTable)
|
||||
}
|
||||
|
@ -136,15 +139,16 @@ func (db *DB) GetAuthorizationsByAccountID(ctx context.Context, accountID string
|
|||
continue
|
||||
}
|
||||
authzs = append(authzs, &acme.Authorization{
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: nil, // challenges not required for current use case
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Token: dbaz.Token,
|
||||
Error: dbaz.Error,
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: nil, // challenges not required for current use case
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Token: dbaz.Token,
|
||||
Fingerprint: dbaz.Fingerprint,
|
||||
Error: dbaz.Error,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -473,6 +473,7 @@ func TestDB_UpdateAuthorization(t *testing.T) {
|
|||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
ChallengeIDs: []string{"foo", "bar"},
|
||||
Wildcard: true,
|
||||
Fingerprint: "fingerprint",
|
||||
}
|
||||
b, err := json.Marshal(dbaz)
|
||||
assert.FatalError(t, err)
|
||||
|
@ -549,10 +550,11 @@ func TestDB_UpdateAuthorization(t *testing.T) {
|
|||
{ID: "foo"},
|
||||
{ID: "bar"},
|
||||
},
|
||||
Token: dbaz.Token,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
|
||||
Token: dbaz.Token,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Fingerprint: "fingerprint",
|
||||
Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
|
||||
}
|
||||
return test{
|
||||
az: updAz,
|
||||
|
@ -582,6 +584,7 @@ func TestDB_UpdateAuthorization(t *testing.T) {
|
|||
assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard)
|
||||
assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt)
|
||||
assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt)
|
||||
assert.Equals(t, dbNew.Fingerprint, dbaz.Fingerprint)
|
||||
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
|
||||
return nu, true, nil
|
||||
},
|
||||
|
|
|
@ -6,8 +6,10 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
|
||||
"github.com/smallstep/nosql"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
)
|
||||
|
||||
type dbChallenge struct {
|
||||
|
@ -19,7 +21,7 @@ type dbChallenge struct {
|
|||
Value string `json:"value"`
|
||||
ValidatedAt string `json:"validatedAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Error *acme.Error `json:"error"`
|
||||
Error *acme.Error `json:"error"` // TODO(hs): a bit dangerous; should become db-specific type
|
||||
}
|
||||
|
||||
func (dbc *dbChallenge) clone() *dbChallenge {
|
||||
|
|
|
@ -270,14 +270,34 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
// Error represents an ACME
|
||||
// Error represents an ACME Error
|
||||
type Error struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
Subproblems []interface{} `json:"subproblems,omitempty"`
|
||||
Identifier interface{} `json:"identifier,omitempty"`
|
||||
Err error `json:"-"`
|
||||
Status int `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
Subproblems []Subproblem `json:"subproblems,omitempty"`
|
||||
Err error `json:"-"`
|
||||
Status int `json:"-"`
|
||||
}
|
||||
|
||||
// Subproblem represents an ACME subproblem. It's fairly
|
||||
// similar to an ACME error, but differs in that it can't
|
||||
// include subproblems itself, the error is reflected
|
||||
// in the Detail property and doesn't have a Status.
|
||||
type Subproblem struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
// The "identifier" field MUST NOT be present at the top level in ACME
|
||||
// problem documents. It can only be present in subproblems.
|
||||
// Subproblems need not all have the same type, and they do not need to
|
||||
// match the top level type.
|
||||
Identifier *Identifier `json:"identifier,omitempty"`
|
||||
}
|
||||
|
||||
// AddSubproblems adds the Subproblems to Error. It
|
||||
// returns the Error, allowing for fluent addition.
|
||||
func (e *Error) AddSubproblems(subproblems ...Subproblem) *Error {
|
||||
e.Subproblems = append(e.Subproblems, subproblems...)
|
||||
return e
|
||||
}
|
||||
|
||||
// NewError creates a new Error type.
|
||||
|
@ -285,6 +305,26 @@ func NewError(pt ProblemType, msg string, args ...interface{}) *Error {
|
|||
return newError(pt, errors.Errorf(msg, args...))
|
||||
}
|
||||
|
||||
// NewSubproblem creates a new Subproblem. The msg and args
|
||||
// are used to create a new error, which is set as the Detail, allowing
|
||||
// for more detailed error messages to be returned to the ACME client.
|
||||
func NewSubproblem(pt ProblemType, msg string, args ...interface{}) Subproblem {
|
||||
e := newError(pt, fmt.Errorf(msg, args...))
|
||||
s := Subproblem{
|
||||
Type: e.Type,
|
||||
Detail: e.Err.Error(),
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// NewSubproblemWithIdentifier creates a new Subproblem with a specific ACME
|
||||
// Identifier. It calls NewSubproblem and sets the Identifier.
|
||||
func NewSubproblemWithIdentifier(pt ProblemType, identifier Identifier, msg string, args ...interface{}) Subproblem {
|
||||
s := NewSubproblem(pt, msg, args...)
|
||||
s.Identifier = &identifier
|
||||
return s
|
||||
}
|
||||
|
||||
func newError(pt ProblemType, err error) *Error {
|
||||
meta, ok := errorMap[pt]
|
||||
if !ok {
|
||||
|
|
|
@ -3,6 +3,7 @@ package acme
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"net"
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
|
@ -125,6 +127,27 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// getKeyFingerprint returns a fingerprint from the list of authorizations. This
|
||||
// fingerprint is used on the device-attest-01 flow to verify the attestation
|
||||
// certificate public key with the CSR public key.
|
||||
//
|
||||
// There's no point on reading all the authorizations as there will be only one
|
||||
// for a permanent identifier.
|
||||
func (o *Order) getAuthorizationFingerprint(ctx context.Context, db DB) (string, error) {
|
||||
for _, azID := range o.AuthorizationIDs {
|
||||
az, err := db.GetAuthorization(ctx, azID)
|
||||
if err != nil {
|
||||
return "", WrapErrorISE(err, "error getting authorization %q", azID)
|
||||
}
|
||||
// There's no point on reading all the authorizations as there will
|
||||
// be only one for a permanent identifier.
|
||||
if az.Fingerprint != "" {
|
||||
return az.Fingerprint, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Finalize signs a certificate if the necessary conditions for Order completion
|
||||
// have been met.
|
||||
//
|
||||
|
@ -150,6 +173,24 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
|
|||
return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID)
|
||||
}
|
||||
|
||||
// Get key fingerprint if any. And then compare it with the CSR fingerprint.
|
||||
//
|
||||
// In device-attest-01 challenges we should check that the keys in the CSR
|
||||
// and the attestation certificate are the same.
|
||||
fingerprint, err := o.getAuthorizationFingerprint(ctx, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fingerprint != "" {
|
||||
fp, err := keyutil.Fingerprint(csr.PublicKey)
|
||||
if err != nil {
|
||||
return WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(fingerprint), []byte(fp)) == 0 {
|
||||
return NewError(ErrorUnauthorizedType, "order %s csr does not match the attested key", o.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// canonicalize the CSR to allow for comparison
|
||||
csr = canonicalize(csr)
|
||||
|
||||
|
@ -165,6 +206,15 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
|
|||
for i := range o.Identifiers {
|
||||
if o.Identifiers[i].Type == PermanentIdentifier {
|
||||
permanentIdentifier = o.Identifiers[i].Value
|
||||
// the first (and only) Permanent Identifier that gets added to the certificate
|
||||
// should be equal to the Subject Common Name if it's set. If not equal, the CSR
|
||||
// is rejected, because the Common Name hasn't been challenged in that case. This
|
||||
// could result in unauthorized access if a relying system relies on the Common
|
||||
// Name in its authorization logic.
|
||||
if csr.Subject.CommonName != "" && csr.Subject.CommonName != permanentIdentifier {
|
||||
return NewError(ErrorBadCSRType, "CSR Subject Common Name does not match identifiers exactly: "+
|
||||
"CSR Subject Common Name = %s, Order Permanent Identifier = %s", csr.Subject.CommonName, permanentIdentifier)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,12 @@ package acme
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
@ -16,6 +19,7 @@ import (
|
|||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
|
@ -306,6 +310,14 @@ func (m *mockSignAuth) Revoke(context.Context, *authority.RevokeOptions) error {
|
|||
}
|
||||
|
||||
func TestOrder_Finalize(t *testing.T) {
|
||||
mustSigner := func(kty, crv string, size int) crypto.Signer {
|
||||
s, err := keyutil.GenerateSigner(kty, crv, size)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type test struct {
|
||||
o *Order
|
||||
err *Error
|
||||
|
@ -386,6 +398,72 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
err: NewErrorISE("unrecognized order status: %s", o.Status),
|
||||
}
|
||||
},
|
||||
"fail/non-matching-permanent-identifier-common-name": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
ID: "oID",
|
||||
AccountID: "accID",
|
||||
Status: StatusReady,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
AuthorizationIDs: []string{"a", "b"},
|
||||
Identifiers: []Identifier{
|
||||
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
|
||||
},
|
||||
}
|
||||
|
||||
signer := mustSigner("EC", "P-256", 0)
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csr := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "a-different-identifier",
|
||||
},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
return test{
|
||||
o: o,
|
||||
csr: csr,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
switch id {
|
||||
case "a":
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
case "b":
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Fingerprint: fingerprint,
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected authorization %s", id))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
MockUpdateOrder: func(ctx context.Context, o *Order) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
err: &Error{
|
||||
Type: "urn:ietf:params:acme:error:badCSR",
|
||||
Detail: "The CSR is unacceptable",
|
||||
Status: 400,
|
||||
Err: fmt.Errorf("CSR Subject Common Name does not match identifiers exactly: "+
|
||||
"CSR Subject Common Name = %s, Order Permanent Identifier = %s", csr.Subject.CommonName, "a-permanent-identifier"),
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/error-provisioner-auth": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
|
@ -415,6 +493,11 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
},
|
||||
err: NewErrorISE("error retrieving authorization options from ACME provisioner: force"),
|
||||
}
|
||||
},
|
||||
|
@ -454,6 +537,11 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
}
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
},
|
||||
err: NewErrorISE("error creating template options from ACME provisioner: error unmarshaling template data: invalid character 'o' in literal false (expecting 'a')"),
|
||||
}
|
||||
},
|
||||
|
@ -495,6 +583,11 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
},
|
||||
err: NewErrorISE("error signing certificate for order oID: force"),
|
||||
}
|
||||
},
|
||||
|
@ -541,6 +634,9 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
assert.Equals(t, cert.OrderID, o.ID)
|
||||
|
@ -595,6 +691,9 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
|
@ -617,6 +716,297 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
err: NewErrorISE("error updating order oID: force"),
|
||||
}
|
||||
},
|
||||
"fail/csr-fingerprint": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
ID: "oID",
|
||||
AccountID: "accID",
|
||||
Status: StatusReady,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
AuthorizationIDs: []string{"a", "b"},
|
||||
Identifiers: []Identifier{
|
||||
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
|
||||
},
|
||||
}
|
||||
|
||||
signer := mustSigner("EC", "P-256", 0)
|
||||
|
||||
csr := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "a-permanent-identifier",
|
||||
},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
leaf := &x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "a-permanent-identifier"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}}
|
||||
root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}}
|
||||
|
||||
return test{
|
||||
o: o,
|
||||
csr: csr,
|
||||
prov: &MockProvisioner{
|
||||
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
|
||||
assert.Equals(t, token, "")
|
||||
return nil, nil
|
||||
},
|
||||
MgetOptions: func() *provisioner.Options {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
ca: &mockSignAuth{
|
||||
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
assert.Equals(t, _csr, csr)
|
||||
return []*x509.Certificate{leaf, inter, root}, nil
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Fingerprint: "other-fingerprint",
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
assert.Equals(t, cert.OrderID, o.ID)
|
||||
assert.Equals(t, cert.Leaf, leaf)
|
||||
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
|
||||
return nil
|
||||
},
|
||||
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
|
||||
assert.Equals(t, updo.CertificateID, "certID")
|
||||
assert.Equals(t, updo.Status, StatusValid)
|
||||
assert.Equals(t, updo.ID, o.ID)
|
||||
assert.Equals(t, updo.AccountID, o.AccountID)
|
||||
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
|
||||
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
|
||||
assert.Equals(t, updo.Identifiers, o.Identifiers)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
err: NewError(ErrorUnauthorizedType, "order oID csr does not match the attested key"),
|
||||
}
|
||||
},
|
||||
"ok/permanent-identifier": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
ID: "oID",
|
||||
AccountID: "accID",
|
||||
Status: StatusReady,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
AuthorizationIDs: []string{"a", "b"},
|
||||
Identifiers: []Identifier{
|
||||
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
|
||||
},
|
||||
}
|
||||
|
||||
signer := mustSigner("EC", "P-256", 0)
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csr := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "a-permanent-identifier",
|
||||
},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
leaf := &x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "a-permanent-identifier"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}}
|
||||
root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}}
|
||||
|
||||
return test{
|
||||
o: o,
|
||||
csr: csr,
|
||||
prov: &MockProvisioner{
|
||||
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
|
||||
assert.Equals(t, token, "")
|
||||
return nil, nil
|
||||
},
|
||||
MgetOptions: func() *provisioner.Options {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
ca: &mockSignAuth{
|
||||
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
assert.Equals(t, _csr, csr)
|
||||
return []*x509.Certificate{leaf, inter, root}, nil
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
switch id {
|
||||
case "a":
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
case "b":
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Fingerprint: fingerprint,
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected authorization %s", id))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
assert.Equals(t, cert.OrderID, o.ID)
|
||||
assert.Equals(t, cert.Leaf, leaf)
|
||||
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
|
||||
return nil
|
||||
},
|
||||
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
|
||||
assert.Equals(t, updo.CertificateID, "certID")
|
||||
assert.Equals(t, updo.Status, StatusValid)
|
||||
assert.Equals(t, updo.ID, o.ID)
|
||||
assert.Equals(t, updo.AccountID, o.AccountID)
|
||||
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
|
||||
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
|
||||
assert.Equals(t, updo.Identifiers, o.Identifiers)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/permanent-identifier-only": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
ID: "oID",
|
||||
AccountID: "accID",
|
||||
Status: StatusReady,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
AuthorizationIDs: []string{"a", "b"},
|
||||
Identifiers: []Identifier{
|
||||
{Type: "dns", Value: "foo.internal"},
|
||||
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
|
||||
},
|
||||
}
|
||||
|
||||
signer := mustSigner("EC", "P-256", 0)
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
csr := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: "a-permanent-identifier",
|
||||
},
|
||||
DNSNames: []string{"foo.internal"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
leaf := &x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "a-permanent-identifier"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
|
||||
Value: []byte("a-permanent-identifier"),
|
||||
},
|
||||
},
|
||||
}
|
||||
inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}}
|
||||
root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}}
|
||||
|
||||
return test{
|
||||
o: o,
|
||||
csr: csr,
|
||||
prov: &MockProvisioner{
|
||||
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
|
||||
assert.Equals(t, token, "")
|
||||
return nil, nil
|
||||
},
|
||||
MgetOptions: func() *provisioner.Options {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// TODO(hs): we should work on making the mocks more realistic. Ideally, we should get rid of
|
||||
// the mock entirely, relying on an instances of provisioner, authority and DB (possibly hardest), so
|
||||
// that behavior of the tests is what an actual CA would do. We could gradually phase them out by
|
||||
// using the mocking functions as a wrapper for actual test helpers generated per test case or per
|
||||
// function that's tested.
|
||||
ca: &mockSignAuth{
|
||||
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
assert.Equals(t, _csr, csr)
|
||||
return []*x509.Certificate{leaf, inter, root}, nil
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{
|
||||
ID: id,
|
||||
Fingerprint: fingerprint,
|
||||
Status: StatusValid,
|
||||
}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
assert.Equals(t, cert.OrderID, o.ID)
|
||||
assert.Equals(t, cert.Leaf, leaf)
|
||||
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
|
||||
return nil
|
||||
},
|
||||
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
|
||||
assert.Equals(t, updo.CertificateID, "certID")
|
||||
assert.Equals(t, updo.Status, StatusValid)
|
||||
assert.Equals(t, updo.ID, o.ID)
|
||||
assert.Equals(t, updo.AccountID, o.AccountID)
|
||||
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
|
||||
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
|
||||
assert.Equals(t, updo.Identifiers, o.Identifiers)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/new-cert-dns": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
o := &Order{
|
||||
|
@ -660,6 +1050,9 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
|
@ -721,6 +1114,9 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
|
@ -785,6 +1181,9 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
|
||||
cert.ID = "certID"
|
||||
assert.Equals(t, cert.AccountID, o.AccountID)
|
||||
|
@ -1492,3 +1891,55 @@ func TestOrder_sans(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrder_getAuthorizationFingerprint(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type fields struct {
|
||||
AuthorizationIDs []string
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
db DB
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
},
|
||||
}}, "", false},
|
||||
{"ok fingerprint", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
if id == "az1" {
|
||||
return &Authorization{ID: id, Status: StatusValid}, nil
|
||||
}
|
||||
return &Authorization{ID: id, Fingerprint: "fingerprint", Status: StatusValid}, nil
|
||||
},
|
||||
}}, "fingerprint", false},
|
||||
{"fail", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
}}, "", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := &Order{
|
||||
AuthorizationIDs: tt.fields.AuthorizationIDs,
|
||||
}
|
||||
got, err := o.getAuthorizationFingerprint(tt.args.ctx, tt.args.db)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Order.getAuthorizationFingerprint() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("Order.getAuthorizationFingerprint() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,13 @@ func CRL(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
_, formatAsPEM := r.URL.Query()["pem"]
|
||||
if formatAsPEM {
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{
|
||||
w.Header().Add("Content-Type", "application/x-pem-file")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.pem\"")
|
||||
|
||||
_ = pem.Encode(w, &pem.Block{
|
||||
Type: "X509 CRL",
|
||||
Bytes: crlBytes,
|
||||
})
|
||||
w.Header().Add("Content-Type", "application/x-pem-file")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.pem\"")
|
||||
w.Write(pemBytes)
|
||||
} else {
|
||||
w.Header().Add("Content-Type", "application/pkix-crl")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"")
|
||||
|
|
|
@ -57,9 +57,9 @@ func validateWebhook(webhook *linkedca.Webhook) error {
|
|||
|
||||
// kind
|
||||
switch webhook.Kind {
|
||||
case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING:
|
||||
case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING, linkedca.Webhook_SCEPCHALLENGE:
|
||||
default:
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook kind is invalid")
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook kind %q is invalid", webhook.Kind)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -180,6 +180,26 @@ func TestWebhookAdminResponder_CreateProvisionerWebhook(t *testing.T) {
|
|||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/unsupported-webhook-kind": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, `(line 5:13): invalid value for enum type: "UNSUPPORTED"`)
|
||||
adminErr.Message = `(line 5:13): invalid value for enum type: "UNSUPPORTED"`
|
||||
body := []byte(`
|
||||
{
|
||||
"name": "metadata",
|
||||
"url": "https://example.com",
|
||||
"kind": "UNSUPPORTED",
|
||||
}`)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
body: body,
|
||||
err: adminErr,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"fail/auth.UpdateProvisioner-error": func(t *testing.T) test {
|
||||
adm := &linkedca.Admin{
|
||||
Subject: "step",
|
||||
|
|
|
@ -545,50 +545,6 @@ func (a *Authority) init() error {
|
|||
tmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts...)
|
||||
}
|
||||
|
||||
// Check if a KMS with decryption capability is required and available
|
||||
if a.requiresDecrypter() {
|
||||
if _, ok := a.keyManager.(kmsapi.Decrypter); !ok {
|
||||
return errors.New("keymanager doesn't provide crypto.Decrypter")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: decide if this is a good approach for providing the SCEP functionality
|
||||
// It currently mirrors the logic for the x509CAService
|
||||
if a.requiresSCEPService() && a.scepService == nil {
|
||||
var options scep.Options
|
||||
|
||||
// Read intermediate and create X509 signer and decrypter for default CAS.
|
||||
options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...)
|
||||
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.IntermediateKey,
|
||||
Password: a.password,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
|
||||
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
|
||||
DecryptionKey: a.config.IntermediateKey,
|
||||
Password: a.password,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
a.scepService, err = scep.NewService(ctx, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: mimick the x509CAService GetCertificateAuthority here too?
|
||||
}
|
||||
|
||||
if a.config.AuthorityConfig.EnableAdmin {
|
||||
// Initialize step-ca Admin Database if it's not already initialized using
|
||||
// WithAdminDB.
|
||||
|
@ -684,6 +640,50 @@ func (a *Authority) init() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Check if a KMS with decryption capability is required and available
|
||||
if a.requiresDecrypter() {
|
||||
if _, ok := a.keyManager.(kmsapi.Decrypter); !ok {
|
||||
return errors.New("keymanager doesn't provide crypto.Decrypter")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: decide if this is a good approach for providing the SCEP functionality
|
||||
// It currently mirrors the logic for the x509CAService
|
||||
if a.requiresSCEPService() && a.scepService == nil {
|
||||
var options scep.Options
|
||||
|
||||
// Read intermediate and create X509 signer and decrypter for default CAS.
|
||||
options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...)
|
||||
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.IntermediateKey,
|
||||
Password: a.password,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
|
||||
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
|
||||
DecryptionKey: a.config.IntermediateKey,
|
||||
Password: a.password,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
a.scepService, err = scep.NewService(ctx, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: mimick the x509CAService GetCertificateAuthority here too?
|
||||
}
|
||||
|
||||
// Load X509 constraints engine.
|
||||
//
|
||||
// This is currently only available in CA mode.
|
||||
|
|
|
@ -248,7 +248,7 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error {
|
|||
if isNamePolicyError && policyErr.Reason == policy.NotAllowed {
|
||||
return &PolicyError{
|
||||
Typ: AdminLockOut,
|
||||
Err: fmt.Errorf("the provided policy would lock out %s from the CA. Please update your policy to include %s as an allowed name", sans, sans),
|
||||
Err: fmt.Errorf("the provided policy would lock out %s from the CA. Please create an x509 policy to include %s as an allowed DNS name", sans, sans),
|
||||
}
|
||||
}
|
||||
return &PolicyError{
|
||||
|
|
|
@ -80,7 +80,7 @@ func TestAuthority_checkPolicy(t *testing.T) {
|
|||
},
|
||||
err: &PolicyError{
|
||||
Typ: AdminLockOut,
|
||||
Err: errors.New("the provided policy would lock out [step] from the CA. Please update your policy to include [step] as an allowed name"),
|
||||
Err: errors.New("the provided policy would lock out [step] from the CA. Please create an x509 policy to include [step] as an allowed DNS name"),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -127,7 +127,7 @@ func TestAuthority_checkPolicy(t *testing.T) {
|
|||
},
|
||||
err: &PolicyError{
|
||||
Typ: AdminLockOut,
|
||||
Err: errors.New("the provided policy would lock out [otherAdmin] from the CA. Please update your policy to include [otherAdmin] as an allowed name"),
|
||||
Err: errors.New("the provided policy would lock out [otherAdmin] from the CA. Please create an x509 policy to include [otherAdmin] as an allowed DNS name"),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
|
@ -48,7 +48,7 @@ func (c ACMEChallenge) Validate() error {
|
|||
type ACMEAttestationFormat string
|
||||
|
||||
const (
|
||||
// APPLE is the format used to enable device-attest-01 on apple devices.
|
||||
// APPLE is the format used to enable device-attest-01 on Apple devices.
|
||||
APPLE ACMEAttestationFormat = "apple"
|
||||
|
||||
// STEP is the format used to enable device-attest-01 on devices that
|
||||
|
@ -57,7 +57,7 @@ const (
|
|||
// TODO(mariano): should we rename this to something else.
|
||||
STEP ACMEAttestationFormat = "step"
|
||||
|
||||
// TPM is the format used to enable device-attest-01 on TPMs.
|
||||
// TPM is the format used to enable device-attest-01 with TPMs.
|
||||
TPM ACMEAttestationFormat = "tpm"
|
||||
)
|
||||
|
||||
|
@ -184,7 +184,7 @@ func (p *ACME) Init(config Config) (err error) {
|
|||
}
|
||||
|
||||
// Parse attestation roots.
|
||||
// The pool will be nil if the there are not roots.
|
||||
// The pool will be nil if there are no roots.
|
||||
if rest := p.AttestationRoots; len(rest) > 0 {
|
||||
var block *pem.Block
|
||||
var hasCert bool
|
||||
|
|
|
@ -26,7 +26,12 @@ import (
|
|||
const azureOIDCBaseURL = "https://login.microsoftonline.com"
|
||||
|
||||
//nolint:gosec // azureIdentityTokenURL is the URL to get the identity token for an instance.
|
||||
const azureIdentityTokenURL = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F"
|
||||
const azureIdentityTokenURL = "http://169.254.169.254/metadata/identity/oauth2/token"
|
||||
|
||||
const azureIdentityTokenAPIVersion = "2018-02-01"
|
||||
|
||||
// azureInstanceComputeURL is the URL to get the instance compute metadata.
|
||||
const azureInstanceComputeURL = "http://169.254.169.254/metadata/instance/compute/azEnvironment"
|
||||
|
||||
// azureDefaultAudience is the default audience used.
|
||||
const azureDefaultAudience = "https://management.azure.com/"
|
||||
|
@ -35,15 +40,27 @@ const azureDefaultAudience = "https://management.azure.com/"
|
|||
// Using case insensitive as resourceGroups appears as resourcegroups.
|
||||
var azureXMSMirIDRegExp = regexp.MustCompile(`(?i)^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.(Compute/virtualMachines|ManagedIdentity/userAssignedIdentities)/([^/]+)$`)
|
||||
|
||||
// azureEnvironments is the list of all Azure environments.
|
||||
var azureEnvironments = map[string]string{
|
||||
"AzurePublicCloud": "https://management.azure.com/",
|
||||
"AzureCloud": "https://management.azure.com/",
|
||||
"AzureUSGovernmentCloud": "https://management.usgovcloudapi.net/",
|
||||
"AzureUSGovernment": "https://management.usgovcloudapi.net/",
|
||||
"AzureChinaCloud": "https://management.chinacloudapi.cn/",
|
||||
"AzureGermanCloud": "https://management.microsoftazure.de/",
|
||||
}
|
||||
|
||||
type azureConfig struct {
|
||||
oidcDiscoveryURL string
|
||||
identityTokenURL string
|
||||
oidcDiscoveryURL string
|
||||
identityTokenURL string
|
||||
instanceComputeURL string
|
||||
}
|
||||
|
||||
func newAzureConfig(tenantID string) *azureConfig {
|
||||
return &azureConfig{
|
||||
oidcDiscoveryURL: azureOIDCBaseURL + "/" + tenantID + "/.well-known/openid-configuration",
|
||||
identityTokenURL: azureIdentityTokenURL,
|
||||
oidcDiscoveryURL: azureOIDCBaseURL + "/" + tenantID + "/.well-known/openid-configuration",
|
||||
identityTokenURL: azureIdentityTokenURL,
|
||||
instanceComputeURL: azureInstanceComputeURL,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,6 +120,7 @@ type Azure struct {
|
|||
oidcConfig openIDConfiguration
|
||||
keyStore *keyStore
|
||||
ctl *Controller
|
||||
environment string
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier.
|
||||
|
@ -167,11 +185,30 @@ func (p *Azure) GetIdentityToken(subject, caURL string) (string, error) {
|
|||
// Initialize the config if this method is used from the cli.
|
||||
p.assertConfig()
|
||||
|
||||
// default to AzurePublicCloud to keep existing behavior
|
||||
identityTokenResource := azureEnvironments["AzurePublicCloud"]
|
||||
|
||||
var err error
|
||||
p.environment, err = p.getAzureEnvironment()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error getting azure environment")
|
||||
}
|
||||
|
||||
if resource, ok := azureEnvironments[p.environment]; ok {
|
||||
identityTokenResource = resource
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", p.config.identityTokenURL, http.NoBody)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error creating request")
|
||||
}
|
||||
req.Header.Set("Metadata", "true")
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Add("resource", identityTokenResource)
|
||||
query.Add("api-version", azureIdentityTokenAPIVersion)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error getting identity token, are you in a Azure VM?")
|
||||
|
@ -444,3 +481,37 @@ func (p *Azure) assertConfig() {
|
|||
p.config = newAzureConfig(p.TenantID)
|
||||
}
|
||||
}
|
||||
|
||||
// getAzureEnvironment returns the Azure environment for the current instance
|
||||
func (p *Azure) getAzureEnvironment() (string, error) {
|
||||
if p.environment != "" {
|
||||
return p.environment, nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", p.config.instanceComputeURL, http.NoBody)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error creating request")
|
||||
}
|
||||
req.Header.Add("Metadata", "True")
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Add("format", "text")
|
||||
query.Add("api-version", "2021-02-01")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error getting azure instance environment, are you in a Azure VM?")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error reading azure environment response")
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return "", errors.Errorf("error getting azure environment: status=%d, response=%s", resp.StatusCode, b)
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
|
|
@ -100,7 +100,14 @@ func TestAzure_GetIdentityToken(t *testing.T) {
|
|||
time.Now(), &p1.keyStore.keySet.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
srvIdentity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
wantResource := r.URL.Query().Get("want_resource")
|
||||
resource := r.URL.Query().Get("resource")
|
||||
if wantResource == "" || resource != wantResource {
|
||||
http.Error(w, fmt.Sprintf("Azure query param resource = %s, wantResource %s", resource, wantResource), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.URL.Path {
|
||||
case "/bad-request":
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
|
@ -111,29 +118,58 @@ func TestAzure_GetIdentityToken(t *testing.T) {
|
|||
fmt.Fprintf(w, `{"access_token":"%s"}`, t1)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
defer srvIdentity.Close()
|
||||
|
||||
srvInstance := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/bad-request":
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
case "/AzureChinaCloud":
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte("AzureChinaCloud"))
|
||||
case "/AzureGermanCloud":
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte("AzureGermanCloud"))
|
||||
case "/AzureUSGovernmentCloud":
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte("AzureUSGovernmentCloud"))
|
||||
default:
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte("AzurePublicCloud"))
|
||||
}
|
||||
}))
|
||||
defer srvInstance.Close()
|
||||
|
||||
type args struct {
|
||||
subject string
|
||||
caURL string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
azure *Azure
|
||||
args args
|
||||
identityTokenURL string
|
||||
want string
|
||||
wantErr bool
|
||||
name string
|
||||
azure *Azure
|
||||
args args
|
||||
identityTokenURL string
|
||||
instanceComputeURL string
|
||||
wantEnvironment string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", p1, args{"subject", "caURL"}, srv.URL, t1, false},
|
||||
{"fail request", p1, args{"subject", "caURL"}, srv.URL + "/bad-request", "", true},
|
||||
{"fail unmarshal", p1, args{"subject", "caURL"}, srv.URL + "/bad-json", "", true},
|
||||
{"fail url", p1, args{"subject", "caURL"}, "://ca.smallstep.com", "", true},
|
||||
{"fail connect", p1, args{"subject", "caURL"}, "foobarzar", "", true},
|
||||
{"ok", p1, args{"subject", "caURL"}, srvIdentity.URL, srvInstance.URL, "AzurePublicCloud", t1, false},
|
||||
{"ok azure china", p1, args{"subject", "caURL"}, srvIdentity.URL, srvInstance.URL, "AzurePublicCloud", t1, false},
|
||||
{"ok azure germany", p1, args{"subject", "caURL"}, srvIdentity.URL, srvInstance.URL, "AzureGermanCloud", t1, false},
|
||||
{"ok azure us gov", p1, args{"subject", "caURL"}, srvIdentity.URL, srvInstance.URL, "AzureUSGovernmentCloud", t1, false},
|
||||
{"fail instance request", p1, args{"subject", "caURL"}, srvIdentity.URL + "/bad-request", srvInstance.URL + "/bad-request", "AzurePublicCloud", "", true},
|
||||
{"fail request", p1, args{"subject", "caURL"}, srvIdentity.URL + "/bad-request", srvInstance.URL, "AzurePublicCloud", "", true},
|
||||
{"fail unmarshal", p1, args{"subject", "caURL"}, srvIdentity.URL + "/bad-json", srvInstance.URL, "AzurePublicCloud", "", true},
|
||||
{"fail url", p1, args{"subject", "caURL"}, "://ca.smallstep.com", srvInstance.URL, "AzurePublicCloud", "", true},
|
||||
{"fail connect", p1, args{"subject", "caURL"}, "foobarzar", srvInstance.URL, "AzurePublicCloud", "", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.azure.config.identityTokenURL = tt.identityTokenURL
|
||||
// reset environment between tests to avoid caching issues
|
||||
p1.environment = ""
|
||||
tt.azure.config.identityTokenURL = tt.identityTokenURL + "?want_resource=" + azureEnvironments[tt.wantEnvironment]
|
||||
tt.azure.config.instanceComputeURL = tt.instanceComputeURL + "/" + tt.wantEnvironment
|
||||
got, err := tt.azure.GetIdentityToken(tt.args.subject, tt.args.caURL)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Azure.GetIdentityToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
|
|
@ -230,7 +230,7 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error {
|
|||
}
|
||||
}
|
||||
if !found {
|
||||
return errs.Unauthorized("validatePayload: failed to validate oidc token payload: email is not allowed")
|
||||
return errs.Unauthorized("validatePayload: failed to validate oidc token payload: email %q is not allowed", p.Email)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,16 +385,13 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
|
|||
}
|
||||
|
||||
var data sshutil.TemplateData
|
||||
var principals []string
|
||||
|
||||
if claims.Email == "" {
|
||||
// If email is empty, use the Subject claim instead to create minimal data for the template to use
|
||||
// If email is empty, use the Subject claim instead to create minimal
|
||||
// data for the template to use.
|
||||
data = sshutil.CreateTemplateData(sshutil.UserCert, claims.Subject, nil)
|
||||
if v, err := unsafeParseSigned(token); err == nil {
|
||||
data.SetToken(v)
|
||||
}
|
||||
|
||||
principals = nil
|
||||
} else {
|
||||
// Get the identity using either the default identityFunc or one injected
|
||||
// externally. Note that the PreferredUsername might be empty.
|
||||
|
@ -417,8 +414,6 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
|
|||
for k, v := range iden.Permissions.CriticalOptions {
|
||||
data.AddCriticalOption(k, v)
|
||||
}
|
||||
|
||||
principals = iden.Usernames
|
||||
}
|
||||
|
||||
// Use the default template unless no-templates are configured and email is
|
||||
|
@ -446,8 +441,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
|
|||
})
|
||||
} else {
|
||||
signOptions = append(signOptions, sshCertOptionsValidator(SignSSHOptions{
|
||||
CertType: SSHUserCert,
|
||||
Principals: principals,
|
||||
CertType: SSHUserCert,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.step.sm/crypto/jose"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
|
@ -221,39 +222,37 @@ func TestOIDC_authorizeToken(t *testing.T) {
|
|||
args args
|
||||
code int
|
||||
wantIssuer string
|
||||
wantErr bool
|
||||
expErr error
|
||||
}{
|
||||
{"ok1", p1, args{t1}, http.StatusOK, issuer, false},
|
||||
{"ok tenantid", p2, args{t2}, http.StatusOK, tenantIssuer, false},
|
||||
{"ok admin", p3, args{t3}, http.StatusOK, issuer, false},
|
||||
{"ok domain", p3, args{t4}, http.StatusOK, issuer, false},
|
||||
{"ok no email", p3, args{t5}, http.StatusOK, issuer, false},
|
||||
{"fail-domain", p3, args{failDomain}, http.StatusUnauthorized, "", true},
|
||||
{"fail-key", p1, args{failKey}, http.StatusUnauthorized, "", true},
|
||||
{"fail-token", p1, args{failTok}, http.StatusUnauthorized, "", true},
|
||||
{"fail-claims", p1, args{failClaims}, http.StatusUnauthorized, "", true},
|
||||
{"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, "", true},
|
||||
{"fail-audience", p1, args{failAud}, http.StatusUnauthorized, "", true},
|
||||
{"fail-signature", p1, args{failSig}, http.StatusUnauthorized, "", true},
|
||||
{"fail-expired", p1, args{failExp}, http.StatusUnauthorized, "", true},
|
||||
{"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, "", true},
|
||||
{"ok1", p1, args{t1}, http.StatusOK, issuer, nil},
|
||||
{"ok tenantid", p2, args{t2}, http.StatusOK, tenantIssuer, nil},
|
||||
{"ok admin", p3, args{t3}, http.StatusOK, issuer, nil},
|
||||
{"ok domain", p3, args{t4}, http.StatusOK, issuer, nil},
|
||||
{"ok no email", p3, args{t5}, http.StatusOK, issuer, nil},
|
||||
{"fail-domain", p3, args{failDomain}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: email "name@example.com" is not allowed`)},
|
||||
{"fail-key", p1, args{failKey}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken; cannot validate oidc token`)},
|
||||
{"fail-token", p1, args{failTok}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken; error parsing oidc token: invalid character '~' looking for beginning of value`)},
|
||||
{"fail-claims", p1, args{failClaims}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken; error parsing oidc token claims: invalid character '~' looking for beginning of value`)},
|
||||
{"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: square/go-jose/jwt: validation failed, invalid issuer claim (iss)`)},
|
||||
{"fail-audience", p1, args{failAud}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: square/go-jose/jwt: validation failed, invalid audience claim (aud)`)},
|
||||
{"fail-signature", p1, args{failSig}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken; cannot validate oidc token`)},
|
||||
{"fail-expired", p1, args{failExp}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: square/go-jose/jwt: validation failed, token is expired (exp)`)},
|
||||
{"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: square/go-jose/jwt: validation failed, token not valid yet (nbf)`)},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.prov.authorizeToken(tt.args.token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
fmt.Println(tt)
|
||||
t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if tt.expErr != nil {
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, tt.expErr.Error())
|
||||
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.Nil(t, got)
|
||||
require.ErrorAs(t, err, &sc, "error does not implement StatusCodedError interface")
|
||||
require.Equal(t, tt.code, sc.StatusCode())
|
||||
require.Nil(t, got)
|
||||
} else {
|
||||
assert.NotNil(t, got)
|
||||
assert.Equals(t, got.Issuer, tt.wantIssuer)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, tt.wantIssuer, got.Issuer)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -339,8 +338,6 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
|
|||
case *validityValidator:
|
||||
assert.Equals(t, v.min, tt.prov.ctl.Claimer.MinTLSCertDuration())
|
||||
assert.Equals(t, v.max, tt.prov.ctl.Claimer.MaxTLSCertDuration())
|
||||
case emailOnlyIdentity:
|
||||
assert.Equals(t, string(v), "name@smallstep.com")
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
|
@ -582,6 +579,9 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
|
|||
{"ok-principals", p1, args{t1, SignSSHOptions{Principals: []string{"name"}}, pub},
|
||||
&SignSSHOptions{CertType: "user", Principals: []string{"name", "name@smallstep.com"},
|
||||
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
|
||||
{"ok-principals-ignore-passed", p1, args{t1, SignSSHOptions{Principals: []string{"root"}}, pub},
|
||||
&SignSSHOptions{CertType: "user", Principals: []string{"name", "name@smallstep.com"},
|
||||
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
|
||||
{"ok-principals-getIdentity", p4, args{okGetIdentityToken, SignSSHOptions{Principals: []string{"mariano"}}, pub},
|
||||
&SignSSHOptions{CertType: "user", Principals: []string{"max", "mariano"},
|
||||
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
|
||||
|
@ -600,7 +600,6 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
|
|||
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
|
||||
{"fail-rsa1024", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},
|
||||
{"fail-user-host", p1, args{t1, SignSSHOptions{CertType: "host"}, pub}, nil, http.StatusOK, false, true},
|
||||
{"fail-user-principals", p1, args{t1, SignSSHOptions{Principals: []string{"root"}}, pub}, nil, http.StatusOK, false, true},
|
||||
{"fail-getIdentity", p5, args{failGetIdentityToken, SignSSHOptions{}, pub}, nil, http.StatusInternalServerError, true, false},
|
||||
{"fail-sshCA-disabled", p6, args{"foo", SignSSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
|
||||
// Missing parametrs
|
||||
|
|
|
@ -2,10 +2,16 @@ package provisioner
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/webhook"
|
||||
)
|
||||
|
||||
// SCEP is the SCEP provisioner type, an entity that can authorize the
|
||||
|
@ -34,6 +40,7 @@ type SCEP struct {
|
|||
Claims *Claims `json:"claims,omitempty"`
|
||||
ctl *Controller
|
||||
encryptionAlgorithm int
|
||||
challengeValidationController *challengeValidationController
|
||||
}
|
||||
|
||||
// GetID returns the provisioner unique identifier.
|
||||
|
@ -81,6 +88,67 @@ func (s *SCEP) DefaultTLSCertDuration() time.Duration {
|
|||
return s.ctl.Claimer.DefaultTLSCertDuration()
|
||||
}
|
||||
|
||||
type challengeValidationController struct {
|
||||
client *http.Client
|
||||
webhooks []*Webhook
|
||||
}
|
||||
|
||||
// newChallengeValidationController creates a new challengeValidationController
|
||||
// that performs challenge validation through webhooks.
|
||||
func newChallengeValidationController(client *http.Client, webhooks []*Webhook) *challengeValidationController {
|
||||
scepHooks := []*Webhook{}
|
||||
for _, wh := range webhooks {
|
||||
if wh.Kind != linkedca.Webhook_SCEPCHALLENGE.String() {
|
||||
continue
|
||||
}
|
||||
if !isCertTypeOK(wh) {
|
||||
continue
|
||||
}
|
||||
scepHooks = append(scepHooks, wh)
|
||||
}
|
||||
return &challengeValidationController{
|
||||
client: client,
|
||||
webhooks: scepHooks,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrSCEPChallengeInvalid = errors.New("webhook server did not allow request")
|
||||
)
|
||||
|
||||
// Validate executes zero or more configured webhooks to
|
||||
// validate the SCEP challenge. If at least one of them indicates
|
||||
// the challenge value is accepted, validation succeeds. In
|
||||
// that case, the other webhooks will be skipped. If none of
|
||||
// the webhooks indicates the value of the challenge was accepted,
|
||||
// an error is returned.
|
||||
func (c *challengeValidationController) Validate(ctx context.Context, challenge, transactionID string) error {
|
||||
for _, wh := range c.webhooks {
|
||||
req := &webhook.RequestBody{
|
||||
SCEPChallenge: challenge,
|
||||
SCEPTransactionID: transactionID,
|
||||
}
|
||||
resp, err := wh.DoWithContext(ctx, c.client, req, nil) // TODO(hs): support templated URL? Requires some refactoring
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed executing webhook request: %w", err)
|
||||
}
|
||||
if resp.Allow {
|
||||
return nil // return early when response is positive
|
||||
}
|
||||
}
|
||||
|
||||
return ErrSCEPChallengeInvalid
|
||||
}
|
||||
|
||||
// isCertTypeOK returns whether or not the webhook can be used
|
||||
// with the SCEP challenge validation webhook controller.
|
||||
func isCertTypeOK(wh *Webhook) bool {
|
||||
if wh.CertType == linkedca.Webhook_ALL.String() || wh.CertType == "" {
|
||||
return true
|
||||
}
|
||||
return linkedca.Webhook_X509.String() == wh.CertType
|
||||
}
|
||||
|
||||
// Init initializes and validates the fields of a SCEP type.
|
||||
func (s *SCEP) Init(config Config) (err error) {
|
||||
switch {
|
||||
|
@ -104,6 +172,11 @@ func (s *SCEP) Init(config Config) (err error) {
|
|||
return errors.New("only encryption algorithm identifiers from 0 to 4 are valid")
|
||||
}
|
||||
|
||||
s.challengeValidationController = newChallengeValidationController(
|
||||
config.WebhookClient,
|
||||
s.GetOptions().GetWebhooks(),
|
||||
)
|
||||
|
||||
// TODO: add other, SCEP specific, options?
|
||||
|
||||
s.ctl, err = NewController(s, s.Claims, config, s.Options)
|
||||
|
@ -151,3 +224,43 @@ func (s *SCEP) ShouldIncludeRootInChain() bool {
|
|||
func (s *SCEP) GetContentEncryptionAlgorithm() int {
|
||||
return s.encryptionAlgorithm
|
||||
}
|
||||
|
||||
// ValidateChallenge validates the provided challenge. It starts by
|
||||
// selecting the validation method to use, then performs validation
|
||||
// according to that method.
|
||||
func (s *SCEP) ValidateChallenge(ctx context.Context, challenge, transactionID string) error {
|
||||
if s.challengeValidationController == nil {
|
||||
return fmt.Errorf("provisioner %q wasn't initialized", s.Name)
|
||||
}
|
||||
switch s.selectValidationMethod() {
|
||||
case validationMethodWebhook:
|
||||
return s.challengeValidationController.Validate(ctx, challenge, transactionID)
|
||||
default:
|
||||
if subtle.ConstantTimeCompare([]byte(s.secretChallengePassword), []byte(challenge)) == 0 {
|
||||
return errors.New("invalid challenge password provided")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type validationMethod string
|
||||
|
||||
const (
|
||||
validationMethodNone validationMethod = "none"
|
||||
validationMethodStatic validationMethod = "static"
|
||||
validationMethodWebhook validationMethod = "webhook"
|
||||
)
|
||||
|
||||
// selectValidationMethod returns the method to validate SCEP
|
||||
// challenges. If a webhook is configured with kind `SCEPCHALLENGE`,
|
||||
// the webhook method will be used. If a challenge password is set,
|
||||
// the static method is used. It will default to the `none` method.
|
||||
func (s *SCEP) selectValidationMethod() validationMethod {
|
||||
if len(s.challengeValidationController.webhooks) > 0 {
|
||||
return validationMethodWebhook
|
||||
}
|
||||
if s.secretChallengePassword != "" {
|
||||
return validationMethodStatic
|
||||
}
|
||||
return validationMethodNone
|
||||
}
|
||||
|
|
342
authority/provisioner/scep_test.go
Normal file
342
authority/provisioner/scep_test.go
Normal file
|
@ -0,0 +1,342 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
func Test_challengeValidationController_Validate(t *testing.T) {
|
||||
type request struct {
|
||||
Challenge string `json:"scepChallenge"`
|
||||
TransactionID string `json:"scepTransactionID"`
|
||||
}
|
||||
type response struct {
|
||||
Allow bool `json:"allow"`
|
||||
}
|
||||
nokServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
req := &request{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "not-allowed", req.Challenge)
|
||||
assert.Equal(t, "transaction-1", req.TransactionID)
|
||||
b, err := json.Marshal(response{Allow: false})
|
||||
require.NoError(t, err)
|
||||
w.WriteHeader(200)
|
||||
w.Write(b)
|
||||
}))
|
||||
okServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
req := &request{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "challenge", req.Challenge)
|
||||
assert.Equal(t, "transaction-1", req.TransactionID)
|
||||
b, err := json.Marshal(response{Allow: true})
|
||||
require.NoError(t, err)
|
||||
w.WriteHeader(200)
|
||||
w.Write(b)
|
||||
}))
|
||||
type fields struct {
|
||||
client *http.Client
|
||||
webhooks []*Webhook
|
||||
}
|
||||
type args struct {
|
||||
challenge string
|
||||
transactionID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
server *httptest.Server
|
||||
expErr error
|
||||
}{
|
||||
{
|
||||
name: "fail/no-webhook",
|
||||
fields: fields{http.DefaultClient, nil},
|
||||
args: args{"no-webhook", "transaction-1"},
|
||||
expErr: errors.New("webhook server did not allow request"),
|
||||
},
|
||||
{
|
||||
name: "fail/wrong-cert-type",
|
||||
fields: fields{http.DefaultClient, []*Webhook{
|
||||
{
|
||||
Kind: linkedca.Webhook_SCEPCHALLENGE.String(),
|
||||
CertType: linkedca.Webhook_SSH.String(),
|
||||
},
|
||||
}},
|
||||
args: args{"wrong-cert-type", "transaction-1"},
|
||||
expErr: errors.New("webhook server did not allow request"),
|
||||
},
|
||||
{
|
||||
name: "fail/wrong-secret-value",
|
||||
fields: fields{http.DefaultClient, []*Webhook{
|
||||
{
|
||||
ID: "webhook-id-1",
|
||||
Name: "webhook-name-1",
|
||||
Secret: "{{}}",
|
||||
Kind: linkedca.Webhook_SCEPCHALLENGE.String(),
|
||||
CertType: linkedca.Webhook_X509.String(),
|
||||
URL: okServer.URL,
|
||||
},
|
||||
}},
|
||||
args: args{
|
||||
challenge: "wrong-secret-value",
|
||||
transactionID: "transaction-1",
|
||||
},
|
||||
expErr: errors.New("failed executing webhook request: illegal base64 data at input byte 0"),
|
||||
},
|
||||
{
|
||||
name: "fail/not-allowed",
|
||||
fields: fields{http.DefaultClient, []*Webhook{
|
||||
{
|
||||
ID: "webhook-id-1",
|
||||
Name: "webhook-name-1",
|
||||
Secret: "MTIzNAo=",
|
||||
Kind: linkedca.Webhook_SCEPCHALLENGE.String(),
|
||||
CertType: linkedca.Webhook_X509.String(),
|
||||
URL: nokServer.URL,
|
||||
},
|
||||
}},
|
||||
args: args{
|
||||
challenge: "not-allowed",
|
||||
transactionID: "transaction-1",
|
||||
},
|
||||
server: nokServer,
|
||||
expErr: errors.New("webhook server did not allow request"),
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{http.DefaultClient, []*Webhook{
|
||||
{
|
||||
ID: "webhook-id-1",
|
||||
Name: "webhook-name-1",
|
||||
Secret: "MTIzNAo=",
|
||||
Kind: linkedca.Webhook_SCEPCHALLENGE.String(),
|
||||
CertType: linkedca.Webhook_X509.String(),
|
||||
URL: okServer.URL,
|
||||
},
|
||||
}},
|
||||
args: args{
|
||||
challenge: "challenge",
|
||||
transactionID: "transaction-1",
|
||||
},
|
||||
server: okServer,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := newChallengeValidationController(tt.fields.client, tt.fields.webhooks)
|
||||
|
||||
if tt.server != nil {
|
||||
defer tt.server.Close()
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err := c.Validate(ctx, tt.args.challenge, tt.args.transactionID)
|
||||
|
||||
if tt.expErr != nil {
|
||||
assert.EqualError(t, err, tt.expErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_isCertTypeOK(t *testing.T) {
|
||||
assert.True(t, isCertTypeOK(&Webhook{CertType: linkedca.Webhook_X509.String()}))
|
||||
assert.True(t, isCertTypeOK(&Webhook{CertType: linkedca.Webhook_ALL.String()}))
|
||||
assert.True(t, isCertTypeOK(&Webhook{CertType: ""}))
|
||||
assert.False(t, isCertTypeOK(&Webhook{CertType: linkedca.Webhook_SSH.String()}))
|
||||
}
|
||||
|
||||
func Test_selectValidationMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p *SCEP
|
||||
want validationMethod
|
||||
}{
|
||||
{"webhooks", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
Options: &Options{
|
||||
Webhooks: []*Webhook{
|
||||
{
|
||||
Kind: linkedca.Webhook_SCEPCHALLENGE.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, "webhook"},
|
||||
{"challenge", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
ChallengePassword: "pass",
|
||||
}, "static"},
|
||||
{"challenge-with-different-webhook", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
Options: &Options{
|
||||
Webhooks: []*Webhook{
|
||||
{
|
||||
Kind: linkedca.Webhook_AUTHORIZING.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
ChallengePassword: "pass",
|
||||
}, "static"},
|
||||
{"none", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
}, "none"},
|
||||
{"none-with-different-webhook", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
Options: &Options{
|
||||
Webhooks: []*Webhook{
|
||||
{
|
||||
Kind: linkedca.Webhook_AUTHORIZING.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, "none"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.p.Init(Config{Claims: globalProvisionerClaims})
|
||||
require.NoError(t, err)
|
||||
got := tt.p.selectValidationMethod()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSCEP_ValidateChallenge(t *testing.T) {
|
||||
type request struct {
|
||||
Challenge string `json:"scepChallenge"`
|
||||
TransactionID string `json:"scepTransactionID"`
|
||||
}
|
||||
type response struct {
|
||||
Allow bool `json:"allow"`
|
||||
}
|
||||
okServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
req := &request{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "webhook-challenge", req.Challenge)
|
||||
assert.Equal(t, "webhook-transaction-1", req.TransactionID)
|
||||
b, err := json.Marshal(response{Allow: true})
|
||||
require.NoError(t, err)
|
||||
w.WriteHeader(200)
|
||||
w.Write(b)
|
||||
}))
|
||||
type args struct {
|
||||
challenge string
|
||||
transactionID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
p *SCEP
|
||||
server *httptest.Server
|
||||
args args
|
||||
expErr error
|
||||
}{
|
||||
{"ok/webhooks", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
Options: &Options{
|
||||
Webhooks: []*Webhook{
|
||||
{
|
||||
ID: "webhook-id-1",
|
||||
Name: "webhook-name-1",
|
||||
Secret: "MTIzNAo=",
|
||||
Kind: linkedca.Webhook_SCEPCHALLENGE.String(),
|
||||
CertType: linkedca.Webhook_X509.String(),
|
||||
URL: okServer.URL,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, okServer, args{"webhook-challenge", "webhook-transaction-1"},
|
||||
nil,
|
||||
},
|
||||
{"fail/webhooks-secret-configuration", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
Options: &Options{
|
||||
Webhooks: []*Webhook{
|
||||
{
|
||||
ID: "webhook-id-1",
|
||||
Name: "webhook-name-1",
|
||||
Secret: "{{}}",
|
||||
Kind: linkedca.Webhook_SCEPCHALLENGE.String(),
|
||||
CertType: linkedca.Webhook_X509.String(),
|
||||
URL: okServer.URL,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil, args{"webhook-challenge", "webhook-transaction-1"},
|
||||
errors.New("failed executing webhook request: illegal base64 data at input byte 0"),
|
||||
},
|
||||
{"ok/static-challenge", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
Options: &Options{},
|
||||
ChallengePassword: "secret-static-challenge",
|
||||
}, nil, args{"secret-static-challenge", "static-transaction-1"},
|
||||
nil,
|
||||
},
|
||||
{"fail/wrong-static-challenge", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
Options: &Options{},
|
||||
ChallengePassword: "secret-static-challenge",
|
||||
}, nil, args{"the-wrong-challenge-secret", "static-transaction-1"},
|
||||
errors.New("invalid challenge password provided"),
|
||||
},
|
||||
{"ok/no-challenge", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
Options: &Options{},
|
||||
ChallengePassword: "",
|
||||
}, nil, args{"", "static-transaction-1"},
|
||||
nil,
|
||||
},
|
||||
{"fail/no-challenge-but-provided", &SCEP{
|
||||
Name: "SCEP",
|
||||
Type: "SCEP",
|
||||
Options: &Options{},
|
||||
ChallengePassword: "",
|
||||
}, nil, args{"a-challenge-value", "static-transaction-1"},
|
||||
errors.New("invalid challenge password provided"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
if tt.server != nil {
|
||||
defer tt.server.Close()
|
||||
}
|
||||
|
||||
err := tt.p.Init(Config{Claims: globalProvisionerClaims, WebhookClient: http.DefaultClient})
|
||||
require.NoError(t, err)
|
||||
ctx := context.Background()
|
||||
|
||||
err = tt.p.ValidateChallenge(ctx, tt.args.challenge, tt.args.transactionID)
|
||||
if tt.expErr != nil {
|
||||
assert.EqualError(t, err, tt.expErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -83,31 +83,6 @@ type AttestationData struct {
|
|||
PermanentIdentifier string
|
||||
}
|
||||
|
||||
// emailOnlyIdentity is a CertificateRequestValidator that checks that the only
|
||||
// SAN provided is the given email address.
|
||||
type emailOnlyIdentity string
|
||||
|
||||
func (e emailOnlyIdentity) Valid(req *x509.CertificateRequest) error {
|
||||
switch {
|
||||
case len(req.DNSNames) > 0:
|
||||
return errs.Forbidden("certificate request cannot contain DNS names")
|
||||
case len(req.IPAddresses) > 0:
|
||||
return errs.Forbidden("certificate request cannot contain IP addresses")
|
||||
case len(req.URIs) > 0:
|
||||
return errs.Forbidden("certificate request cannot contain URIs")
|
||||
case len(req.EmailAddresses) == 0:
|
||||
return errs.Forbidden("certificate request does not contain any email address")
|
||||
case len(req.EmailAddresses) > 1:
|
||||
return errs.Forbidden("certificate request contains too many email addresses")
|
||||
case req.EmailAddresses[0] == "":
|
||||
return errs.Forbidden("certificate request cannot contain an empty email address")
|
||||
case req.EmailAddresses[0] != string(e):
|
||||
return errs.Forbidden("certificate request does not contain the valid email address - got %s, want %s", req.EmailAddresses[0], e)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// defaultPublicKeyValidator validates the public key of a certificate request.
|
||||
type defaultPublicKeyValidator struct{}
|
||||
|
||||
|
|
|
@ -16,38 +16,6 @@ import (
|
|||
"go.step.sm/crypto/pemutil"
|
||||
)
|
||||
|
||||
func Test_emailOnlyIdentity_Valid(t *testing.T) {
|
||||
uri, err := url.Parse("https://example.com/1.0/getUser")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
req *x509.CertificateRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
e emailOnlyIdentity
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", "name@smallstep.com", args{&x509.CertificateRequest{EmailAddresses: []string{"name@smallstep.com"}}}, false},
|
||||
{"DNSNames", "name@smallstep.com", args{&x509.CertificateRequest{DNSNames: []string{"foo.bar.zar"}}}, true},
|
||||
{"IPAddresses", "name@smallstep.com", args{&x509.CertificateRequest{IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}}}, true},
|
||||
{"URIs", "name@smallstep.com", args{&x509.CertificateRequest{URIs: []*url.URL{uri}}}, true},
|
||||
{"no-emails", "name@smallstep.com", args{&x509.CertificateRequest{EmailAddresses: []string{}}}, true},
|
||||
{"empty-email", "", args{&x509.CertificateRequest{EmailAddresses: []string{""}}}, true},
|
||||
{"multiple-emails", "name@smallstep.com", args{&x509.CertificateRequest{EmailAddresses: []string{"name@smallstep.com", "foo@smallstep.com"}}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.e.Valid(tt.args.req); (err != nil) != tt.wantErr {
|
||||
t.Errorf("emailOnlyIdentity.Valid() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_defaultPublicKeyValidator_Valid(t *testing.T) {
|
||||
_shortRSA, err := pemutil.Read("./testdata/certs/short-rsa.csr")
|
||||
assert.FatalError(t, err)
|
||||
|
|
|
@ -125,35 +125,6 @@ func (o SignSSHOptions) match(got SignSSHOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// sshCertPrincipalsModifier is an SSHCertModifier that sets the
|
||||
// principals to the SSH certificate.
|
||||
type sshCertPrincipalsModifier []string
|
||||
|
||||
// Modify the ValidPrincipals value of the cert.
|
||||
func (o sshCertPrincipalsModifier) Modify(cert *ssh.Certificate, _ SignSSHOptions) error {
|
||||
cert.ValidPrincipals = []string(o)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sshCertKeyIDModifier is an SSHCertModifier that sets the given
|
||||
// Key ID in the SSH certificate.
|
||||
type sshCertKeyIDModifier string
|
||||
|
||||
func (m sshCertKeyIDModifier) Modify(cert *ssh.Certificate, _ SignSSHOptions) error {
|
||||
cert.KeyId = string(m)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sshCertTypeModifier is an SSHCertModifier that sets the
|
||||
// certificate type.
|
||||
type sshCertTypeModifier string
|
||||
|
||||
// Modify sets the CertType for the ssh certificate.
|
||||
func (m sshCertTypeModifier) Modify(cert *ssh.Certificate, _ SignSSHOptions) error {
|
||||
cert.CertType = sshCertTypeUInt32(string(m))
|
||||
return nil
|
||||
}
|
||||
|
||||
// sshCertValidAfterModifier is an SSHCertModifier that sets the
|
||||
// ValidAfter in the SSH certificate.
|
||||
type sshCertValidAfterModifier uint64
|
||||
|
@ -172,51 +143,6 @@ func (m sshCertValidBeforeModifier) Modify(cert *ssh.Certificate, _ SignSSHOptio
|
|||
return nil
|
||||
}
|
||||
|
||||
// sshCertDefaultsModifier implements a SSHCertModifier that
|
||||
// modifies the certificate with the given options if they are not set.
|
||||
type sshCertDefaultsModifier SignSSHOptions
|
||||
|
||||
// Modify implements the SSHCertModifier interface.
|
||||
func (m sshCertDefaultsModifier) Modify(cert *ssh.Certificate, _ SignSSHOptions) error {
|
||||
if cert.CertType == 0 {
|
||||
cert.CertType = sshCertTypeUInt32(m.CertType)
|
||||
}
|
||||
if len(cert.ValidPrincipals) == 0 {
|
||||
cert.ValidPrincipals = m.Principals
|
||||
}
|
||||
if cert.ValidAfter == 0 && !m.ValidAfter.IsZero() {
|
||||
cert.ValidAfter = uint64(m.ValidAfter.Unix())
|
||||
}
|
||||
if cert.ValidBefore == 0 && !m.ValidBefore.IsZero() {
|
||||
cert.ValidBefore = uint64(m.ValidBefore.Unix())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sshDefaultExtensionModifier implements an SSHCertModifier that sets
|
||||
// the default extensions in an SSH certificate.
|
||||
type sshDefaultExtensionModifier struct{}
|
||||
|
||||
func (m *sshDefaultExtensionModifier) Modify(cert *ssh.Certificate, _ SignSSHOptions) error {
|
||||
switch cert.CertType {
|
||||
// Default to no extensions for HostCert.
|
||||
case ssh.HostCert:
|
||||
return nil
|
||||
case ssh.UserCert:
|
||||
if cert.Extensions == nil {
|
||||
cert.Extensions = make(map[string]string)
|
||||
}
|
||||
cert.Extensions["permit-X11-forwarding"] = ""
|
||||
cert.Extensions["permit-agent-forwarding"] = ""
|
||||
cert.Extensions["permit-port-forwarding"] = ""
|
||||
cert.Extensions["permit-pty"] = ""
|
||||
cert.Extensions["permit-user-rc"] = ""
|
||||
return nil
|
||||
default:
|
||||
return errs.BadRequest("ssh certificate has an unknown type '%d'", cert.CertType)
|
||||
}
|
||||
}
|
||||
|
||||
// sshDefaultDuration is an SSHCertModifier that sets the certificate
|
||||
// ValidAfter and ValidBefore if they have not been set. It will fail if a
|
||||
// CertType has not been set or is not valid.
|
||||
|
|
|
@ -202,97 +202,6 @@ func TestSSHOptions_Match(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_sshCertPrincipalsModifier_Modify(t *testing.T) {
|
||||
type test struct {
|
||||
modifier sshCertPrincipalsModifier
|
||||
cert *ssh.Certificate
|
||||
expected []string
|
||||
}
|
||||
tests := map[string]func() test{
|
||||
"ok": func() test {
|
||||
a := []string{"foo", "bar"}
|
||||
return test{
|
||||
modifier: sshCertPrincipalsModifier(a),
|
||||
cert: new(ssh.Certificate),
|
||||
expected: a,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run()
|
||||
if assert.Nil(t, tc.modifier.Modify(tc.cert, SignSSHOptions{})) {
|
||||
assert.Equals(t, tc.cert.ValidPrincipals, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sshCertKeyIDModifier_Modify(t *testing.T) {
|
||||
type test struct {
|
||||
modifier sshCertKeyIDModifier
|
||||
cert *ssh.Certificate
|
||||
expected string
|
||||
}
|
||||
tests := map[string]func() test{
|
||||
"ok": func() test {
|
||||
a := "foo"
|
||||
return test{
|
||||
modifier: sshCertKeyIDModifier(a),
|
||||
cert: new(ssh.Certificate),
|
||||
expected: a,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run()
|
||||
if assert.Nil(t, tc.modifier.Modify(tc.cert, SignSSHOptions{})) {
|
||||
assert.Equals(t, tc.cert.KeyId, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sshCertTypeModifier_Modify(t *testing.T) {
|
||||
type test struct {
|
||||
modifier sshCertTypeModifier
|
||||
cert *ssh.Certificate
|
||||
expected uint32
|
||||
}
|
||||
tests := map[string]func() test{
|
||||
"ok/user": func() test {
|
||||
return test{
|
||||
modifier: sshCertTypeModifier("user"),
|
||||
cert: new(ssh.Certificate),
|
||||
expected: ssh.UserCert,
|
||||
}
|
||||
},
|
||||
"ok/host": func() test {
|
||||
return test{
|
||||
modifier: sshCertTypeModifier("host"),
|
||||
cert: new(ssh.Certificate),
|
||||
expected: ssh.HostCert,
|
||||
}
|
||||
},
|
||||
"ok/default": func() test {
|
||||
return test{
|
||||
modifier: sshCertTypeModifier("foo"),
|
||||
cert: new(ssh.Certificate),
|
||||
expected: 0,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run()
|
||||
if assert.Nil(t, tc.modifier.Modify(tc.cert, SignSSHOptions{})) {
|
||||
assert.Equals(t, tc.cert.CertType, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sshCertValidAfterModifier_Modify(t *testing.T) {
|
||||
type test struct {
|
||||
modifier sshCertValidAfterModifier
|
||||
|
@ -318,176 +227,6 @@ func Test_sshCertValidAfterModifier_Modify(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_sshCertDefaultsModifier_Modify(t *testing.T) {
|
||||
type test struct {
|
||||
modifier sshCertDefaultsModifier
|
||||
cert *ssh.Certificate
|
||||
valid func(*ssh.Certificate)
|
||||
}
|
||||
tests := map[string]func() test{
|
||||
"ok/changes": func() test {
|
||||
n := time.Now()
|
||||
va := NewTimeDuration(n.Add(1 * time.Minute))
|
||||
vb := NewTimeDuration(n.Add(5 * time.Minute))
|
||||
so := SignSSHOptions{
|
||||
Principals: []string{"foo", "bar"},
|
||||
CertType: "host",
|
||||
ValidAfter: va,
|
||||
ValidBefore: vb,
|
||||
}
|
||||
return test{
|
||||
modifier: sshCertDefaultsModifier(so),
|
||||
cert: new(ssh.Certificate),
|
||||
valid: func(cert *ssh.Certificate) {
|
||||
assert.Equals(t, cert.ValidPrincipals, so.Principals)
|
||||
assert.Equals(t, cert.CertType, uint32(ssh.HostCert))
|
||||
assert.Equals(t, cert.ValidAfter, uint64(so.ValidAfter.RelativeTime(time.Now()).Unix()))
|
||||
assert.Equals(t, cert.ValidBefore, uint64(so.ValidBefore.RelativeTime(time.Now()).Unix()))
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/no-changes": func() test {
|
||||
n := time.Now()
|
||||
so := SignSSHOptions{
|
||||
Principals: []string{"foo", "bar"},
|
||||
CertType: "host",
|
||||
ValidAfter: NewTimeDuration(n.Add(15 * time.Minute)),
|
||||
ValidBefore: NewTimeDuration(n.Add(25 * time.Minute)),
|
||||
}
|
||||
return test{
|
||||
modifier: sshCertDefaultsModifier(so),
|
||||
cert: &ssh.Certificate{
|
||||
CertType: uint32(ssh.UserCert),
|
||||
ValidPrincipals: []string{"zap", "zoop"},
|
||||
ValidAfter: 15,
|
||||
ValidBefore: 25,
|
||||
},
|
||||
valid: func(cert *ssh.Certificate) {
|
||||
assert.Equals(t, cert.ValidPrincipals, []string{"zap", "zoop"})
|
||||
assert.Equals(t, cert.CertType, uint32(ssh.UserCert))
|
||||
assert.Equals(t, cert.ValidAfter, uint64(15))
|
||||
assert.Equals(t, cert.ValidBefore, uint64(25))
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run()
|
||||
if assert.Nil(t, tc.modifier.Modify(tc.cert, SignSSHOptions{})) {
|
||||
tc.valid(tc.cert)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sshDefaultExtensionModifier_Modify(t *testing.T) {
|
||||
type test struct {
|
||||
modifier sshDefaultExtensionModifier
|
||||
cert *ssh.Certificate
|
||||
valid func(*ssh.Certificate)
|
||||
err error
|
||||
}
|
||||
tests := map[string]func() test{
|
||||
"fail/unexpected-cert-type": func() test {
|
||||
cert := &ssh.Certificate{CertType: 3}
|
||||
return test{
|
||||
modifier: sshDefaultExtensionModifier{},
|
||||
cert: cert,
|
||||
err: errors.New("ssh certificate has an unknown type '3'"),
|
||||
}
|
||||
},
|
||||
"ok/host": func() test {
|
||||
cert := &ssh.Certificate{CertType: ssh.HostCert}
|
||||
return test{
|
||||
modifier: sshDefaultExtensionModifier{},
|
||||
cert: cert,
|
||||
valid: func(cert *ssh.Certificate) {
|
||||
assert.Len(t, 0, cert.Extensions)
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/user/extensions-exists": func() test {
|
||||
cert := &ssh.Certificate{CertType: ssh.UserCert, Permissions: ssh.Permissions{Extensions: map[string]string{
|
||||
"foo": "bar",
|
||||
}}}
|
||||
return test{
|
||||
modifier: sshDefaultExtensionModifier{},
|
||||
cert: cert,
|
||||
valid: func(cert *ssh.Certificate) {
|
||||
val, ok := cert.Extensions["foo"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "bar")
|
||||
|
||||
val, ok = cert.Extensions["permit-X11-forwarding"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "")
|
||||
|
||||
val, ok = cert.Extensions["permit-agent-forwarding"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "")
|
||||
|
||||
val, ok = cert.Extensions["permit-port-forwarding"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "")
|
||||
|
||||
val, ok = cert.Extensions["permit-pty"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "")
|
||||
|
||||
val, ok = cert.Extensions["permit-user-rc"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "")
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/user/no-extensions": func() test {
|
||||
return test{
|
||||
modifier: sshDefaultExtensionModifier{},
|
||||
cert: &ssh.Certificate{CertType: ssh.UserCert},
|
||||
valid: func(cert *ssh.Certificate) {
|
||||
_, ok := cert.Extensions["foo"]
|
||||
assert.False(t, ok)
|
||||
|
||||
val, ok := cert.Extensions["permit-X11-forwarding"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "")
|
||||
|
||||
val, ok = cert.Extensions["permit-agent-forwarding"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "")
|
||||
|
||||
val, ok = cert.Extensions["permit-port-forwarding"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "")
|
||||
|
||||
val, ok = cert.Extensions["permit-pty"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "")
|
||||
|
||||
val, ok = cert.Extensions["permit-user-rc"]
|
||||
assert.True(t, ok)
|
||||
assert.Equals(t, val, "")
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run()
|
||||
if err := tc.modifier.Modify(tc.cert, SignSSHOptions{}); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
tc.valid(tc.cert)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_sshCertDefaultValidator_Valid(t *testing.T) {
|
||||
pub, _, err := keyutil.GenerateDefaultKeyPair()
|
||||
assert.FatalError(t, err)
|
||||
|
|
|
@ -665,6 +665,9 @@ func generateAzureWithServer() (*Azure, *httptest.Server, error) {
|
|||
AccessToken: tok,
|
||||
})
|
||||
}
|
||||
case "/metadata/instance/compute/azEnvironment":
|
||||
w.Header().Add("Content-Type", "text/plain")
|
||||
w.Write([]byte("AzurePublicCloud"))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
@ -672,6 +675,7 @@ func generateAzureWithServer() (*Azure, *httptest.Server, error) {
|
|||
srv.Start()
|
||||
az.config.oidcDiscoveryURL = srv.URL + "/" + az.TenantID + "/.well-known/openid-configuration"
|
||||
az.config.identityTokenURL = srv.URL + "/metadata/identity/oauth2/token"
|
||||
az.config.instanceComputeURL = srv.URL + "/metadata/instance/compute/azEnvironment"
|
||||
return az, srv, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -107,6 +107,13 @@ type Webhook struct {
|
|||
}
|
||||
|
||||
func (w *Webhook) Do(client *http.Client, reqBody *webhook.RequestBody, data any) (*webhook.ResponseBody, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
return w.DoWithContext(ctx, client, reqBody, data)
|
||||
}
|
||||
|
||||
func (w *Webhook) DoWithContext(ctx context.Context, client *http.Client, reqBody *webhook.RequestBody, data any) (*webhook.ResponseBody, error) {
|
||||
tmpl, err := template.New("url").Funcs(templates.StepFuncMap()).Parse(w.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -129,8 +136,6 @@ func (w *Webhook) Do(client *http.Client, reqBody *webhook.RequestBody, data any
|
|||
reqBody.Token = tmpl[sshutil.TokenKey]
|
||||
}
|
||||
*/
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
reqBody.Timestamp = time.Now()
|
||||
|
||||
|
|
|
@ -790,8 +790,6 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
|
|||
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix())
|
||||
case sshCertValidBeforeModifier:
|
||||
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix())
|
||||
case sshCertDefaultsModifier:
|
||||
assert.Equals(t, SignSSHOptions(v), SignSSHOptions{CertType: SSHUserCert})
|
||||
case *sshLimitDuration:
|
||||
assert.Equals(t, v.Claimer, tc.p.ctl.Claimer)
|
||||
assert.Equals(t, v.NotAfter, x5cCerts[0].NotAfter)
|
||||
|
|
|
@ -786,7 +786,7 @@ func (a *Authority) GenerateCertificateRevocationList() error {
|
|||
// Note that this is currently using the port 443 by default.
|
||||
if b, err := marshalDistributionPoint(fullName, false); err == nil {
|
||||
revocationList.ExtraExtensions = []pkix.Extension{
|
||||
{Id: oidExtensionIssuingDistributionPoint, Value: b},
|
||||
{Id: oidExtensionIssuingDistributionPoint, Critical: true, Value: b},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,9 @@ type AdminClient struct {
|
|||
x5cSubject string
|
||||
}
|
||||
|
||||
var ErrAdminAPINotImplemented = errors.New("admin API not implemented")
|
||||
var ErrAdminAPINotAuthorized = errors.New("admin API not authorized")
|
||||
|
||||
// AdminClientError is the client side representation of an
|
||||
// AdminError returned by the CA.
|
||||
type AdminClientError struct {
|
||||
|
@ -137,6 +140,28 @@ func (c *AdminClient) retryOnError(r *http.Response) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// IsEnabled checks if the admin API is enabled.
|
||||
func (c *AdminClient) IsEnabled() error {
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, "admins")})
|
||||
resp, err := c.client.Get(u.String())
|
||||
if err != nil {
|
||||
return clientError(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < http.StatusBadRequest {
|
||||
return nil
|
||||
}
|
||||
switch resp.StatusCode {
|
||||
case http.StatusNotFound, http.StatusNotImplemented:
|
||||
return ErrAdminAPINotImplemented
|
||||
case http.StatusUnauthorized:
|
||||
return ErrAdminAPINotAuthorized
|
||||
default:
|
||||
return errors.Errorf("unexpected status code when performing is-enabled check for Admin API: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAdmin performs the GET /admin/admin/{id} request to the CA.
|
||||
func (c *AdminClient) GetAdmin(id string) (*linkedca.Admin, error) {
|
||||
var retried bool
|
||||
|
|
|
@ -61,7 +61,7 @@ func Bootstrap(token string) (*Client, error) {
|
|||
// }
|
||||
// resp, err := client.Get("https://internal.smallstep.com")
|
||||
func BootstrapClient(ctx context.Context, token string, options ...TLSOption) (*http.Client, error) {
|
||||
b, err := createBootstrap(token)
|
||||
b, err := createBootstrap(token) //nolint:contextcheck // deeply nested context; temporary
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ func BootstrapServer(ctx context.Context, token string, base *http.Server, optio
|
|||
return nil, errors.New("server TLSConfig is already set")
|
||||
}
|
||||
|
||||
b, err := createBootstrap(token)
|
||||
b, err := createBootstrap(token) //nolint:contextcheck // deeply nested context; temporary
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ func BootstrapServer(ctx context.Context, token string, base *http.Server, optio
|
|||
// ... // register services
|
||||
// srv.Serve(lis)
|
||||
func BootstrapListener(ctx context.Context, token string, inner net.Listener, options ...TLSOption) (net.Listener, error) {
|
||||
b, err := createBootstrap(token)
|
||||
b, err := createBootstrap(token) //nolint:contextcheck // deeply nested context; temporary
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
76
ca/ca.go
76
ca/ca.go
|
@ -13,6 +13,7 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
|
@ -126,13 +127,15 @@ type CA struct {
|
|||
insecureSrv *server.Server
|
||||
opts *options
|
||||
renewer *TLSRenewer
|
||||
compactStop chan struct{}
|
||||
}
|
||||
|
||||
// New creates and initializes the CA with the given configuration and options.
|
||||
func New(cfg *config.Config, opts ...Option) (*CA, error) {
|
||||
ca := &CA{
|
||||
config: cfg,
|
||||
opts: new(options),
|
||||
config: cfg,
|
||||
opts: new(options),
|
||||
compactStop: make(chan struct{}),
|
||||
}
|
||||
ca.opts.apply(opts)
|
||||
return ca.Init(cfg)
|
||||
|
@ -193,7 +196,11 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
|
|||
api.Route(r)
|
||||
})
|
||||
|
||||
//Add ACME api endpoints in /acme and /1.0/acme
|
||||
// Mount the CRL to the insecure mux
|
||||
insecureMux.Get("/crl", api.CRL)
|
||||
insecureMux.Get("/1.0/crl", api.CRL)
|
||||
|
||||
// Add ACME api endpoints in /acme and /1.0/acme
|
||||
dns := cfg.DNSNames[0]
|
||||
u, err := url.Parse("https://" + cfg.Address)
|
||||
if err != nil {
|
||||
|
@ -273,6 +280,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
|
|||
|
||||
// helpful routine for logging all routes
|
||||
//dumpRoutes(mux)
|
||||
//dumpRoutes(insecureMux)
|
||||
|
||||
// Add monitoring if configured
|
||||
if len(cfg.Monitoring) > 0 {
|
||||
|
@ -304,7 +312,7 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
|
|||
|
||||
// only start the insecure server if the insecure address is configured
|
||||
// and, currently, also only when it should serve SCEP endpoints.
|
||||
if ca.shouldServeSCEPEndpoints() && cfg.InsecureAddress != "" {
|
||||
if ca.shouldServeInsecureServer() {
|
||||
// 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
|
||||
|
@ -318,6 +326,23 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
|
|||
return ca, nil
|
||||
}
|
||||
|
||||
// shouldServeInsecureServer returns whether or not the insecure
|
||||
// server should also be started. This is (currently) only the case
|
||||
// if the insecure address has been configured AND when a SCEP
|
||||
// provisioner is configured or when a CRL is configured.
|
||||
func (ca *CA) shouldServeInsecureServer() bool {
|
||||
switch {
|
||||
case ca.config.InsecureAddress == "":
|
||||
return false
|
||||
case ca.shouldServeSCEPEndpoints():
|
||||
return true
|
||||
case ca.config.CRL.IsEnabled():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// buildContext builds the server base context.
|
||||
func buildContext(a *authority.Authority, scepAuthority *scep.Authority, acmeDB acme.DB, acmeLinker acme.Linker) context.Context {
|
||||
ctx := authority.NewContext(context.Background(), a)
|
||||
|
@ -370,6 +395,12 @@ func (ca *CA) Run() error {
|
|||
}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ca.runCompactJob()
|
||||
}()
|
||||
|
||||
if ca.insecureSrv != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
|
@ -394,6 +425,7 @@ func (ca *CA) Run() error {
|
|||
|
||||
// Stop stops the CA calling to the server Shutdown method.
|
||||
func (ca *CA) Stop() error {
|
||||
close(ca.compactStop)
|
||||
ca.renewer.Stop()
|
||||
if err := ca.auth.Shutdown(); err != nil {
|
||||
log.Printf("error stopping ca.Authority: %+v\n", err)
|
||||
|
@ -576,3 +608,39 @@ func (ca *CA) getConfigFileOutput() string {
|
|||
}
|
||||
return "loaded from token"
|
||||
}
|
||||
|
||||
// runCompactJob will run the value log garbage collector if the nosql database
|
||||
// supports it.
|
||||
func (ca *CA) runCompactJob() {
|
||||
caDB, ok := ca.auth.GetDatabase().(*db.DB)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
compactor, ok := caDB.DB.(nosql.Compactor)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Compact database at start.
|
||||
runCompact(compactor)
|
||||
|
||||
// Compact database every minute.
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ca.compactStop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
runCompact(compactor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runCompact executes the compact job until it returns an error.
|
||||
func runCompact(c nosql.Compactor) {
|
||||
for err := error(nil); err == nil; {
|
||||
err = c.Compact(0.7)
|
||||
}
|
||||
}
|
||||
|
|
348
ca/client.go
348
ca/client.go
|
@ -2,6 +2,7 @@ package ca
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
|
@ -75,7 +76,11 @@ func (c *uaClient) SetTransport(tr http.RoundTripper) {
|
|||
}
|
||||
|
||||
func (c *uaClient) Get(u string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", u, http.NoBody)
|
||||
return c.GetWithContext(context.Background(), u)
|
||||
}
|
||||
|
||||
func (c *uaClient) GetWithContext(ctx context.Context, u string) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", u, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "create GET %s request failed", u)
|
||||
}
|
||||
|
@ -84,7 +89,11 @@ func (c *uaClient) Get(u string) (*http.Response, error) {
|
|||
}
|
||||
|
||||
func (c *uaClient) Post(u, contentType string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequest("POST", u, body)
|
||||
return c.PostWithContext(context.Background(), u, contentType, body)
|
||||
}
|
||||
|
||||
func (c *uaClient) PostWithContext(ctx context.Context, u, contentType string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", u, body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "create POST %s request failed", u)
|
||||
}
|
||||
|
@ -581,18 +590,24 @@ func (c *Client) SetTransport(tr http.RoundTripper) {
|
|||
c.client.SetTransport(tr)
|
||||
}
|
||||
|
||||
// Version performs the version request to the CA and returns the
|
||||
// Version performs the version request to the CA with an empty context and returns the
|
||||
// api.VersionResponse struct.
|
||||
func (c *Client) Version() (*api.VersionResponse, error) {
|
||||
return c.VersionWithContext(context.Background())
|
||||
}
|
||||
|
||||
// VersionWithContext performs the version request to the CA with the provided context
|
||||
// and returns the api.VersionResponse struct.
|
||||
func (c *Client) VersionWithContext(ctx context.Context) (*api.VersionResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/version"})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
resp, err := c.client.GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -605,18 +620,24 @@ retry:
|
|||
return &version, nil
|
||||
}
|
||||
|
||||
// Health performs the health request to the CA and returns the
|
||||
// api.HealthResponse struct.
|
||||
// Health performs the health request to the CA with an empty context
|
||||
// and returns the api.HealthResponse struct.
|
||||
func (c *Client) Health() (*api.HealthResponse, error) {
|
||||
return c.HealthWithContext(context.Background())
|
||||
}
|
||||
|
||||
// HealthWithContext performs the health request to the CA with the provided context
|
||||
// and returns the api.HealthResponse struct.
|
||||
func (c *Client) HealthWithContext(ctx context.Context) (*api.HealthResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/health"})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
resp, err := c.client.GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -629,21 +650,29 @@ retry:
|
|||
return &health, nil
|
||||
}
|
||||
|
||||
// Root performs the root request to the CA with the given SHA256 and returns
|
||||
// the api.RootResponse struct. It uses an insecure client, but it checks the
|
||||
// resulting root certificate with the given SHA256, returning an error if they
|
||||
// do not match.
|
||||
// Root performs the root request to the CA with an empty context and the provided
|
||||
// SHA256 and returns the api.RootResponse struct. It uses an insecure client, but
|
||||
// it checks the resulting root certificate with the given SHA256, returning an error
|
||||
// if they do not match.
|
||||
func (c *Client) Root(sha256Sum string) (*api.RootResponse, error) {
|
||||
return c.RootWithContext(context.Background(), sha256Sum)
|
||||
}
|
||||
|
||||
// RootWithContext performs the root request to the CA with an empty context and the provided
|
||||
// SHA256 and returns the api.RootResponse struct. It uses an insecure client, but
|
||||
// it checks the resulting root certificate with the given SHA256, returning an error
|
||||
// if they do not match.
|
||||
func (c *Client) RootWithContext(ctx context.Context, sha256Sum string) (*api.RootResponse, error) {
|
||||
var retried bool
|
||||
sha256Sum = strings.ToLower(strings.ReplaceAll(sha256Sum, "-", ""))
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/root/" + sha256Sum})
|
||||
retry:
|
||||
resp, err := newInsecureClient().Get(u.String())
|
||||
resp, err := newInsecureClient().GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -661,9 +690,15 @@ retry:
|
|||
return &root, nil
|
||||
}
|
||||
|
||||
// Sign performs the sign request to the CA and returns the api.SignResponse
|
||||
// struct.
|
||||
// Sign performs the sign request to the CA with an empty context and returns
|
||||
// the api.SignResponse struct.
|
||||
func (c *Client) Sign(req *api.SignRequest) (*api.SignResponse, error) {
|
||||
return c.SignWithContext(context.Background(), req)
|
||||
}
|
||||
|
||||
// SignWithContext performs the sign request to the CA with the provided context
|
||||
// and returns the api.SignResponse struct.
|
||||
func (c *Client) SignWithContext(ctx context.Context, req *api.SignRequest) (*api.SignResponse, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
|
@ -671,12 +706,12 @@ func (c *Client) Sign(req *api.SignRequest) (*api.SignResponse, error) {
|
|||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/sign"})
|
||||
retry:
|
||||
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -692,19 +727,30 @@ retry:
|
|||
return &sign, nil
|
||||
}
|
||||
|
||||
// Renew performs the renew request to the CA and returns the api.SignResponse
|
||||
// struct.
|
||||
// Renew performs the renew request to the CA with an empty context and
|
||||
// returns the api.SignResponse struct.
|
||||
func (c *Client) Renew(tr http.RoundTripper) (*api.SignResponse, error) {
|
||||
return c.RenewWithContext(context.Background(), tr)
|
||||
}
|
||||
|
||||
// RenewWithContext performs the renew request to the CA with the provided context
|
||||
// and returns the api.SignResponse struct.
|
||||
func (c *Client) RenewWithContext(ctx context.Context, tr http.RoundTripper) (*api.SignResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/renew"})
|
||||
client := &http.Client{Transport: tr}
|
||||
retry:
|
||||
resp, err := client.Post(u.String(), "application/json", http.NoBody)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), http.NoBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -718,12 +764,19 @@ retry:
|
|||
}
|
||||
|
||||
// RenewWithToken performs the renew request to the CA with the given
|
||||
// authorization token and returns the api.SignResponse struct. This method is
|
||||
// generally used to renew an expired certificate.
|
||||
// authorization token and and empty context and returns the api.SignResponse struct.
|
||||
// This method is generally used to renew an expired certificate.
|
||||
func (c *Client) RenewWithToken(token string) (*api.SignResponse, error) {
|
||||
return c.RenewWithTokenAndContext(context.Background(), token)
|
||||
}
|
||||
|
||||
// RenewWithTokenAndContext performs the renew request to the CA with the given
|
||||
// authorization token and context and returns the api.SignResponse struct.
|
||||
// This method is generally used to renew an expired certificate.
|
||||
func (c *Client) RenewWithTokenAndContext(ctx context.Context, token string) (*api.SignResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/renew"})
|
||||
req, err := http.NewRequest("POST", u.String(), http.NoBody)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), http.NoBody)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "create POST %s request failed", u)
|
||||
}
|
||||
|
@ -734,7 +787,7 @@ retry:
|
|||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -747,24 +800,34 @@ retry:
|
|||
return &sign, nil
|
||||
}
|
||||
|
||||
// Rekey performs the rekey request to the CA and returns the api.SignResponse
|
||||
// struct.
|
||||
// Rekey performs the rekey request to the CA with an empty context and
|
||||
// returns the api.SignResponse struct.
|
||||
func (c *Client) Rekey(req *api.RekeyRequest, tr http.RoundTripper) (*api.SignResponse, error) {
|
||||
return c.RekeyWithContext(context.Background(), req, tr)
|
||||
}
|
||||
|
||||
// RekeyWithContext performs the rekey request to the CA with the provided context
|
||||
// and returns the api.SignResponse struct.
|
||||
func (c *Client) RekeyWithContext(ctx context.Context, req *api.RekeyRequest, tr http.RoundTripper) (*api.SignResponse, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error marshaling request")
|
||||
}
|
||||
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/rekey"})
|
||||
client := &http.Client{Transport: tr}
|
||||
retry:
|
||||
resp, err := client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", u.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -777,9 +840,15 @@ retry:
|
|||
return &sign, nil
|
||||
}
|
||||
|
||||
// Revoke performs the revoke request to the CA and returns the api.RevokeResponse
|
||||
// struct.
|
||||
// Revoke performs the revoke request to the CA with an empty context and returns
|
||||
// the api.RevokeResponse struct.
|
||||
func (c *Client) Revoke(req *api.RevokeRequest, tr http.RoundTripper) (*api.RevokeResponse, error) {
|
||||
return c.RevokeWithContext(context.Background(), req, tr)
|
||||
}
|
||||
|
||||
// RevokeWithContext performs the revoke request to the CA with the provided context and
|
||||
// returns the api.RevokeResponse struct.
|
||||
func (c *Client) RevokeWithContext(ctx context.Context, req *api.RevokeRequest, tr http.RoundTripper) (*api.RevokeResponse, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
|
@ -794,12 +863,12 @@ retry:
|
|||
}
|
||||
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/revoke"})
|
||||
resp, err := client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
resp, err := client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -812,12 +881,21 @@ retry:
|
|||
return &revoke, nil
|
||||
}
|
||||
|
||||
// Provisioners performs the provisioners request to the CA and returns the
|
||||
// api.ProvisionersResponse struct with a map of provisioners.
|
||||
// Provisioners performs the provisioners request to the CA with an empty context
|
||||
// and returns the api.ProvisionersResponse struct with a map of provisioners.
|
||||
//
|
||||
// ProvisionerOption WithProvisionerCursor and WithProvisionLimit can be used to
|
||||
// paginate the provisioners.
|
||||
func (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersResponse, error) {
|
||||
return c.ProvisionersWithContext(context.Background(), opts...)
|
||||
}
|
||||
|
||||
// ProvisionersWithContext performs the provisioners request to the CA with the provided context
|
||||
// and returns the api.ProvisionersResponse struct with a map of provisioners.
|
||||
//
|
||||
// ProvisionerOption WithProvisionerCursor and WithProvisionLimit can be used to
|
||||
// paginate the provisioners.
|
||||
func (c *Client) ProvisionersWithContext(ctx context.Context, opts ...ProvisionerOption) (*api.ProvisionersResponse, error) {
|
||||
var retried bool
|
||||
o := new(ProvisionerOptions)
|
||||
if err := o.Apply(opts); err != nil {
|
||||
|
@ -828,12 +906,12 @@ func (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersRespo
|
|||
RawQuery: o.rawQuery(),
|
||||
})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
resp, err := c.client.GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -846,19 +924,26 @@ retry:
|
|||
return &provisioners, nil
|
||||
}
|
||||
|
||||
// ProvisionerKey performs the request to the CA to get the encrypted key for
|
||||
// the given provisioner kid and returns the api.ProvisionerKeyResponse struct
|
||||
// with the encrypted key.
|
||||
// ProvisionerKey performs the request to the CA with an empty context to get
|
||||
// the encrypted key for the given provisioner kid and returns the api.ProvisionerKeyResponse
|
||||
// struct with the encrypted key.
|
||||
func (c *Client) ProvisionerKey(kid string) (*api.ProvisionerKeyResponse, error) {
|
||||
return c.ProvisionerKeyWithContext(context.Background(), kid)
|
||||
}
|
||||
|
||||
// ProvisionerKeyWithContext performs the request to the CA with the provided context to get
|
||||
// the encrypted key for the given provisioner kid and returns the api.ProvisionerKeyResponse
|
||||
// struct with the encrypted key.
|
||||
func (c *Client) ProvisionerKeyWithContext(ctx context.Context, kid string) (*api.ProvisionerKeyResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/provisioners/" + kid + "/encrypted-key"})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
resp, err := c.client.GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -871,18 +956,24 @@ retry:
|
|||
return &key, nil
|
||||
}
|
||||
|
||||
// Roots performs the get roots request to the CA and returns the
|
||||
// api.RootsResponse struct.
|
||||
// Roots performs the get roots request to the CA with an empty context
|
||||
// and returns the api.RootsResponse struct.
|
||||
func (c *Client) Roots() (*api.RootsResponse, error) {
|
||||
return c.RootsWithContext(context.Background())
|
||||
}
|
||||
|
||||
// RootsWithContext performs the get roots request to the CA with the provided context
|
||||
// and returns the api.RootsResponse struct.
|
||||
func (c *Client) RootsWithContext(ctx context.Context) (*api.RootsResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/roots"})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
resp, err := c.client.GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -895,18 +986,24 @@ retry:
|
|||
return &roots, nil
|
||||
}
|
||||
|
||||
// Federation performs the get federation request to the CA and returns the
|
||||
// api.FederationResponse struct.
|
||||
// Federation performs the get federation request to the CA with an empty context
|
||||
// and returns the api.FederationResponse struct.
|
||||
func (c *Client) Federation() (*api.FederationResponse, error) {
|
||||
return c.FederationWithContext(context.Background())
|
||||
}
|
||||
|
||||
// FederationWithContext performs the get federation request to the CA with the provided context
|
||||
// and returns the api.FederationResponse struct.
|
||||
func (c *Client) FederationWithContext(ctx context.Context) (*api.FederationResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/federation"})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
resp, err := c.client.GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -919,9 +1016,15 @@ retry:
|
|||
return &federation, nil
|
||||
}
|
||||
|
||||
// SSHSign performs the POST /ssh/sign request to the CA and returns the
|
||||
// api.SSHSignResponse struct.
|
||||
// SSHSign performs the POST /ssh/sign request to the CA with an empty context
|
||||
// and returns the api.SSHSignResponse struct.
|
||||
func (c *Client) SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error) {
|
||||
return c.SSHSignWithContext(context.Background(), req)
|
||||
}
|
||||
|
||||
// SSHSignWithContext performs the POST /ssh/sign request to the CA with the provided context
|
||||
// and returns the api.SSHSignResponse struct.
|
||||
func (c *Client) SSHSignWithContext(ctx context.Context, req *api.SSHSignRequest) (*api.SSHSignResponse, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
|
@ -929,12 +1032,12 @@ func (c *Client) SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error)
|
|||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/sign"})
|
||||
retry:
|
||||
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -947,9 +1050,15 @@ retry:
|
|||
return &sign, nil
|
||||
}
|
||||
|
||||
// SSHRenew performs the POST /ssh/renew request to the CA and returns the
|
||||
// api.SSHRenewResponse struct.
|
||||
// SSHRenew performs the POST /ssh/renew request to the CA with an empty context
|
||||
// and returns the api.SSHRenewResponse struct.
|
||||
func (c *Client) SSHRenew(req *api.SSHRenewRequest) (*api.SSHRenewResponse, error) {
|
||||
return c.SSHRenewWithContext(context.Background(), req)
|
||||
}
|
||||
|
||||
// SSHRenewWithContext performs the POST /ssh/renew request to the CA with the provided context
|
||||
// and returns the api.SSHRenewResponse struct.
|
||||
func (c *Client) SSHRenewWithContext(ctx context.Context, req *api.SSHRenewRequest) (*api.SSHRenewResponse, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
|
@ -957,12 +1066,12 @@ func (c *Client) SSHRenew(req *api.SSHRenewRequest) (*api.SSHRenewResponse, erro
|
|||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/renew"})
|
||||
retry:
|
||||
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -975,9 +1084,15 @@ retry:
|
|||
return &renew, nil
|
||||
}
|
||||
|
||||
// SSHRekey performs the POST /ssh/rekey request to the CA and returns the
|
||||
// api.SSHRekeyResponse struct.
|
||||
// SSHRekey performs the POST /ssh/rekey request to the CA with an empty context
|
||||
// and returns the api.SSHRekeyResponse struct.
|
||||
func (c *Client) SSHRekey(req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, error) {
|
||||
return c.SSHRekeyWithContext(context.Background(), req)
|
||||
}
|
||||
|
||||
// SSHRekeyWithContext performs the POST /ssh/rekey request to the CA with the provided context
|
||||
// and returns the api.SSHRekeyResponse struct.
|
||||
func (c *Client) SSHRekeyWithContext(ctx context.Context, req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
|
@ -985,12 +1100,12 @@ func (c *Client) SSHRekey(req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, erro
|
|||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/rekey"})
|
||||
retry:
|
||||
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -1003,9 +1118,15 @@ retry:
|
|||
return &rekey, nil
|
||||
}
|
||||
|
||||
// SSHRevoke performs the POST /ssh/revoke request to the CA and returns the
|
||||
// api.SSHRevokeResponse struct.
|
||||
// SSHRevoke performs the POST /ssh/revoke request to the CA with an empty context
|
||||
// and returns the api.SSHRevokeResponse struct.
|
||||
func (c *Client) SSHRevoke(req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, error) {
|
||||
return c.SSHRevokeWithContext(context.Background(), req)
|
||||
}
|
||||
|
||||
// SSHRevokeWithContext performs the POST /ssh/revoke request to the CA with the provided context
|
||||
// and returns the api.SSHRevokeResponse struct.
|
||||
func (c *Client) SSHRevokeWithContext(ctx context.Context, req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
|
@ -1013,12 +1134,12 @@ func (c *Client) SSHRevoke(req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, e
|
|||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/revoke"})
|
||||
retry:
|
||||
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -1031,18 +1152,24 @@ retry:
|
|||
return &revoke, nil
|
||||
}
|
||||
|
||||
// SSHRoots performs the GET /ssh/roots request to the CA and returns the
|
||||
// api.SSHRootsResponse struct.
|
||||
// SSHRoots performs the GET /ssh/roots request to the CA with an empty context
|
||||
// and returns the api.SSHRootsResponse struct.
|
||||
func (c *Client) SSHRoots() (*api.SSHRootsResponse, error) {
|
||||
return c.SSHRootsWithContext(context.Background())
|
||||
}
|
||||
|
||||
// SSHRootsWithContext performs the GET /ssh/roots request to the CA with the provided context
|
||||
// and returns the api.SSHRootsResponse struct.
|
||||
func (c *Client) SSHRootsWithContext(ctx context.Context) (*api.SSHRootsResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/roots"})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
resp, err := c.client.GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -1055,18 +1182,24 @@ retry:
|
|||
return &keys, nil
|
||||
}
|
||||
|
||||
// SSHFederation performs the get /ssh/federation request to the CA and returns
|
||||
// the api.SSHRootsResponse struct.
|
||||
// SSHFederation performs the get /ssh/federation request to the CA with an empty context
|
||||
// and returns the api.SSHRootsResponse struct.
|
||||
func (c *Client) SSHFederation() (*api.SSHRootsResponse, error) {
|
||||
return c.SSHFederationWithContext(context.Background())
|
||||
}
|
||||
|
||||
// SSHFederationWithContext performs the get /ssh/federation request to the CA with the provided context
|
||||
// and returns the api.SSHRootsResponse struct.
|
||||
func (c *Client) SSHFederationWithContext(ctx context.Context) (*api.SSHRootsResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/federation"})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
resp, err := c.client.GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -1079,9 +1212,15 @@ retry:
|
|||
return &keys, nil
|
||||
}
|
||||
|
||||
// SSHConfig performs the POST /ssh/config request to the CA to get the ssh
|
||||
// configuration templates.
|
||||
// SSHConfig performs the POST /ssh/config request to the CA with an empty context
|
||||
// to get the ssh configuration templates.
|
||||
func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) {
|
||||
return c.SSHConfigWithContext(context.Background(), req)
|
||||
}
|
||||
|
||||
// SSHConfigWithContext performs the POST /ssh/config request to the CA with the provided context
|
||||
// to get the ssh configuration templates.
|
||||
func (c *Client) SSHConfigWithContext(ctx context.Context, req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
|
@ -1089,12 +1228,12 @@ func (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, e
|
|||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/config"})
|
||||
retry:
|
||||
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -1107,9 +1246,15 @@ retry:
|
|||
return &cfg, nil
|
||||
}
|
||||
|
||||
// SSHCheckHost performs the POST /ssh/check-host request to the CA with the
|
||||
// given principal.
|
||||
// SSHCheckHost performs the POST /ssh/check-host request to the CA with an empty context,
|
||||
// the principal and a token and returns the api.SSHCheckPrincipalResponse.
|
||||
func (c *Client) SSHCheckHost(principal, token string) (*api.SSHCheckPrincipalResponse, error) {
|
||||
return c.SSHCheckHostWithContext(context.Background(), principal, token)
|
||||
}
|
||||
|
||||
// SSHCheckHostWithContext performs the POST /ssh/check-host request to the CA with the provided context,
|
||||
// principal and token and returns the api.SSHCheckPrincipalResponse.
|
||||
func (c *Client) SSHCheckHostWithContext(ctx context.Context, principal, token string) (*api.SSHCheckPrincipalResponse, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(&api.SSHCheckPrincipalRequest{
|
||||
Type: provisioner.SSHHostCert,
|
||||
|
@ -1122,12 +1267,12 @@ func (c *Client) SSHCheckHost(principal, token string) (*api.SSHCheckPrincipalRe
|
|||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/check-host"})
|
||||
retry:
|
||||
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -1141,17 +1286,22 @@ retry:
|
|||
return &check, nil
|
||||
}
|
||||
|
||||
// SSHGetHosts performs the GET /ssh/get-hosts request to the CA.
|
||||
// SSHGetHosts performs the GET /ssh/get-hosts request to the CA with an empty context.
|
||||
func (c *Client) SSHGetHosts() (*api.SSHGetHostsResponse, error) {
|
||||
return c.SSHGetHostsWithContext(context.Background())
|
||||
}
|
||||
|
||||
// SSHGetHostsWithContext performs the GET /ssh/get-hosts request to the CA with the provided context.
|
||||
func (c *Client) SSHGetHostsWithContext(ctx context.Context) (*api.SSHGetHostsResponse, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/hosts"})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
resp, err := c.client.GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -1164,8 +1314,13 @@ retry:
|
|||
return &hosts, nil
|
||||
}
|
||||
|
||||
// SSHBastion performs the POST /ssh/bastion request to the CA.
|
||||
// SSHBastion performs the POST /ssh/bastion request to the CA with an empty context.
|
||||
func (c *Client) SSHBastion(req *api.SSHBastionRequest) (*api.SSHBastionResponse, error) {
|
||||
return c.SSHBastionWithContext(context.Background(), req)
|
||||
}
|
||||
|
||||
// SSHBastionWithContext performs the POST /ssh/bastion request to the CA with the provided context.
|
||||
func (c *Client) SSHBastionWithContext(ctx context.Context, req *api.SSHBastionRequest) (*api.SSHBastionResponse, error) {
|
||||
var retried bool
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
|
@ -1173,12 +1328,12 @@ func (c *Client) SSHBastion(req *api.SSHBastionRequest) (*api.SSHBastionResponse
|
|||
}
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/bastion"})
|
||||
retry:
|
||||
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
|
||||
resp, err := c.client.PostWithContext(ctx, u.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, clientError(err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
if !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
|
@ -1192,11 +1347,16 @@ retry:
|
|||
}
|
||||
|
||||
// RootFingerprint is a helper method that returns the current root fingerprint.
|
||||
// It does an health connection and gets the fingerprint from the TLS verified
|
||||
// chains.
|
||||
// It does an health connection and gets the fingerprint from the TLS verified chains.
|
||||
func (c *Client) RootFingerprint() (string, error) {
|
||||
return c.RootFingerprintWithContext(context.Background())
|
||||
}
|
||||
|
||||
// RootFingerprintWithContext is a helper method that returns the current root fingerprint.
|
||||
// It does an health connection and gets the fingerprint from the TLS verified chains.
|
||||
func (c *Client) RootFingerprintWithContext(ctx context.Context) (string, error) {
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/health"})
|
||||
resp, err := c.client.Get(u.String())
|
||||
resp, err := c.client.GetWithContext(ctx, u.String())
|
||||
if err != nil {
|
||||
return "", clientError(err)
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ func (c *Client) getClientTLSConfig(ctx context.Context, sign *api.SignResponse,
|
|||
//nolint:staticcheck // Use mutable tls.Config on renew
|
||||
tr.DialTLS = c.buildDialTLS(tlsCtx)
|
||||
// tr.DialTLSContext = c.buildDialTLSContext(tlsCtx)
|
||||
renewer.RenewCertificate = getRenewFunc(tlsCtx, c, tr, pk)
|
||||
renewer.RenewCertificate = getRenewFunc(tlsCtx, c, tr, pk) //nolint:contextcheck // deeply nested context
|
||||
|
||||
// Update client transport
|
||||
c.SetTransport(tr)
|
||||
|
@ -183,7 +183,7 @@ func (c *Client) GetServerTLSConfig(ctx context.Context, sign *api.SignResponse,
|
|||
//nolint:staticcheck // Use mutable tls.Config on renew
|
||||
tr.DialTLS = c.buildDialTLS(tlsCtx)
|
||||
// tr.DialTLSContext = c.buildDialTLSContext(tlsCtx)
|
||||
renewer.RenewCertificate = getRenewFunc(tlsCtx, c, tr, pk)
|
||||
renewer.RenewCertificate = getRenewFunc(tlsCtx, c, tr, pk) //nolint:contextcheck // deeply nested context
|
||||
|
||||
// Update client transport
|
||||
c.SetTransport(tr)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package stepcas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -37,7 +38,7 @@ type stepIssuer interface {
|
|||
}
|
||||
|
||||
// newStepIssuer returns the configured step issuer.
|
||||
func newStepIssuer(caURL *url.URL, client *ca.Client, iss *apiv1.CertificateIssuer) (stepIssuer, error) {
|
||||
func newStepIssuer(ctx context.Context, caURL *url.URL, client *ca.Client, iss *apiv1.CertificateIssuer) (stepIssuer, error) {
|
||||
if err := validateCertificateIssuer(iss); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -46,7 +47,7 @@ func newStepIssuer(caURL *url.URL, client *ca.Client, iss *apiv1.CertificateIssu
|
|||
case "x5c":
|
||||
return newX5CIssuer(caURL, iss)
|
||||
case "jwk":
|
||||
return newJWKIssuer(caURL, client, iss)
|
||||
return newJWKIssuer(ctx, caURL, client, iss)
|
||||
default:
|
||||
return nil, errors.Errorf("stepCAS `certificateIssuer.type` %s is not supported", iss.Type)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package stepcas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
@ -118,7 +119,7 @@ func Test_newStepIssuer(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := newStepIssuer(tt.args.caURL, tt.args.client, tt.args.iss)
|
||||
got, err := newStepIssuer(context.TODO(), tt.args.caURL, tt.args.client, tt.args.iss)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("newStepIssuer() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package stepcas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
|
@ -21,13 +22,13 @@ type jwkIssuer struct {
|
|||
signer jose.Signer
|
||||
}
|
||||
|
||||
func newJWKIssuer(caURL *url.URL, client *ca.Client, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) {
|
||||
func newJWKIssuer(ctx context.Context, caURL *url.URL, client *ca.Client, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) {
|
||||
var err error
|
||||
var signer jose.Signer
|
||||
// Read the key from the CA if not provided.
|
||||
// Or read it from a PEM file.
|
||||
if cfg.Key == "" {
|
||||
p, err := findProvisioner(client, provisioner.TypeJWK, cfg.Provisioner)
|
||||
p, err := findProvisioner(ctx, client, provisioner.TypeJWK, cfg.Provisioner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -144,10 +145,10 @@ func newJWKSignerFromEncryptedKey(kid, key, password string) (jose.Signer, error
|
|||
return newJoseSigner(signer, so)
|
||||
}
|
||||
|
||||
func findProvisioner(client *ca.Client, typ provisioner.Type, name string) (provisioner.Interface, error) {
|
||||
func findProvisioner(ctx context.Context, client *ca.Client, typ provisioner.Type, name string) (provisioner.Interface, error) {
|
||||
cursor := ""
|
||||
for {
|
||||
ps, err := client.Provisioners(ca.WithProvisionerCursor(cursor))
|
||||
ps, err := client.ProvisionersWithContext(ctx, ca.WithProvisionerCursor(cursor))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) {
|
|||
}
|
||||
|
||||
// Create client.
|
||||
client, err := ca.NewClient(opts.CertificateAuthority, ca.WithRootSHA256(opts.CertificateAuthorityFingerprint))
|
||||
client, err := ca.NewClient(opts.CertificateAuthority, ca.WithRootSHA256(opts.CertificateAuthorityFingerprint)) //nolint:contextcheck // deeply nested context
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) {
|
|||
// 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 {
|
||||
if iss, err = newStepIssuer(ctx, caURL, client, opts.CertificateIssuer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -245,7 +245,7 @@ func testJWKIssuer(t *testing.T, caURL *url.URL, password string) *jwkIssuer {
|
|||
key = testEncryptedKeyPath
|
||||
password = testPassword
|
||||
}
|
||||
jwk, err := newJWKIssuer(caURL, client, &apiv1.CertificateIssuer{
|
||||
jwk, err := newJWKIssuer(context.TODO(), caURL, client, &apiv1.CertificateIssuer{
|
||||
Type: "jwk",
|
||||
Provisioner: "ra@doe.org",
|
||||
Key: key,
|
||||
|
|
|
@ -1,248 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/sha1" //nolint:gosec // used to create the Subject Key Identifier by RFC 5280
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.step.sm/cli-utils/fileutil"
|
||||
"go.step.sm/cli-utils/ui"
|
||||
"go.step.sm/crypto/kms/apiv1"
|
||||
"go.step.sm/crypto/kms/awskms"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var credentialsFile, region string
|
||||
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(&enableSSH, "ssh", false, "Create SSH keys.")
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
// Initialize windows terminal
|
||||
ui.Init()
|
||||
|
||||
ui.Println("⚠️ This command is deprecated and will be removed in future releases.")
|
||||
ui.Println("⚠️ Please use https://github.com/smallstep/step-kms-plugin instead.")
|
||||
|
||||
c, err := awskms.New(context.Background(), apiv1.Options{
|
||||
Type: apiv1.AmazonKMS,
|
||||
Region: region,
|
||||
CredentialsFile: credentialsFile,
|
||||
})
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
if err := createX509(c); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "Usage: step-awskms-init")
|
||||
fmt.Fprintln(os.Stderr, `
|
||||
The step-awskms-init command initializes a public key infrastructure (PKI)
|
||||
to be used by step-ca.
|
||||
|
||||
This tool is experimental and in the future it will be integrated in step cli.
|
||||
|
||||
OPTIONS`)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
COPYRIGHT
|
||||
|
||||
(c) 2018-%d Smallstep Labs, Inc.
|
||||
`, time.Now().Year())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func createX509(c *awskms.KMS) error {
|
||||
ui.Println("Creating X.509 PKI ...")
|
||||
|
||||
// Root Certificate
|
||||
resp, err := c.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: "root",
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signer, err := c.CreateSigner(&resp.CreateSignerRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
root := &x509.Certificate{
|
||||
IsCA: true,
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour * 24 * 365 * 10),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
MaxPathLen: 1,
|
||||
MaxPathLenZero: false,
|
||||
Issuer: pkix.Name{CommonName: "Smallstep Root"},
|
||||
Subject: pkix.Name{CommonName: "Smallstep Root"},
|
||||
SerialNumber: mustSerialNumber(),
|
||||
SubjectKeyId: mustSubjectKeyID(resp.PublicKey),
|
||||
AuthorityKeyId: mustSubjectKeyID(resp.PublicKey),
|
||||
}
|
||||
|
||||
b, err := x509.CreateCertificate(rand.Reader, root, root, resp.PublicKey, signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: b,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSelected("Root Key", resp.Name)
|
||||
ui.PrintSelected("Root Certificate", "root_ca.crt")
|
||||
|
||||
root, err = pemutil.ReadCertificate("root_ca.crt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Intermediate Certificate
|
||||
resp, err = c.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: "intermediate",
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intermediate := &x509.Certificate{
|
||||
IsCA: true,
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour * 24 * 365 * 10),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
MaxPathLen: 0,
|
||||
MaxPathLenZero: true,
|
||||
Issuer: root.Subject,
|
||||
Subject: pkix.Name{CommonName: "Smallstep Intermediate"},
|
||||
SerialNumber: mustSerialNumber(),
|
||||
SubjectKeyId: mustSubjectKeyID(resp.PublicKey),
|
||||
}
|
||||
|
||||
b, err = x509.CreateCertificate(rand.Reader, intermediate, root, resp.PublicKey, signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: b,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSelected("Intermediate Key", resp.Name)
|
||||
ui.PrintSelected("Intermediate Certificate", "intermediate_ca.crt")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSSH(c *awskms.KMS) error {
|
||||
ui.Println("Creating SSH Keys ...")
|
||||
|
||||
// User Key
|
||||
resp, err := c.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: "ssh-user-key",
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := ssh.NewPublicKey(resp.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile("ssh_user_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSelected("SSH User Public Key", "ssh_user_ca_key.pub")
|
||||
ui.PrintSelected("SSH User Private Key", resp.Name)
|
||||
|
||||
// Host Key
|
||||
resp, err = c.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: "ssh-host-key",
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err = ssh.NewPublicKey(resp.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile("ssh_host_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSelected("SSH Host Public Key", "ssh_host_ca_key.pub")
|
||||
ui.PrintSelected("SSH Host Private Key", resp.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustSerialNumber() *big.Int {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
sn, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sn
|
||||
}
|
||||
|
||||
func mustSubjectKeyID(key crypto.PublicKey) []byte {
|
||||
b, err := x509.MarshalPKIXPublicKey(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
//nolint:gosec // used to create the Subject Key Identifier by RFC 5280
|
||||
hash := sha1.Sum(b)
|
||||
return hash[:]
|
||||
}
|
|
@ -1,286 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/sha1" //nolint:gosec // used to create the Subject Key Identifier by RFC 5280
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.step.sm/cli-utils/fileutil"
|
||||
"go.step.sm/cli-utils/ui"
|
||||
"go.step.sm/crypto/kms/apiv1"
|
||||
"go.step.sm/crypto/kms/cloudkms"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var credentialsFile string
|
||||
var project, location, ring string
|
||||
var protectionLevelName string
|
||||
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(&enableSSH, "ssh", false, "Create SSH keys.")
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
switch {
|
||||
case project == "":
|
||||
usage()
|
||||
case location == "":
|
||||
fmt.Fprintln(os.Stderr, "flag `--location` is required")
|
||||
os.Exit(1)
|
||||
case ring == "":
|
||||
fmt.Fprintln(os.Stderr, "flag `--ring` is required")
|
||||
os.Exit(1)
|
||||
case protectionLevelName == "":
|
||||
fmt.Fprintln(os.Stderr, "flag `--protection-level` is required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var protectionLevel apiv1.ProtectionLevel
|
||||
switch strings.ToUpper(protectionLevelName) {
|
||||
case "SOFTWARE":
|
||||
protectionLevel = apiv1.Software
|
||||
case "HSM":
|
||||
protectionLevel = apiv1.HSM
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "invalid value `%s` for flag `--protection-level`; options are `SOFTWARE` or `HSM`\n", protectionLevelName)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize windows terminal
|
||||
ui.Init()
|
||||
|
||||
ui.Println("⚠️ This command is deprecated and will be removed in future releases.")
|
||||
ui.Println("⚠️ Please use https://github.com/smallstep/step-kms-plugin instead.")
|
||||
|
||||
c, err := cloudkms.New(context.Background(), apiv1.Options{
|
||||
Type: apiv1.CloudKMS,
|
||||
CredentialsFile: credentialsFile,
|
||||
})
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
if err := createPKI(c, project, location, ring, protectionLevel); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "Usage: step-cloudkms-init --project <name>")
|
||||
fmt.Fprintln(os.Stderr, `
|
||||
The step-cloudkms-init command initializes a public key infrastructure (PKI)
|
||||
to be used by step-ca.
|
||||
|
||||
This tool is experimental and in the future it will be integrated in step cli.
|
||||
|
||||
OPTIONS`)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
COPYRIGHT
|
||||
|
||||
(c) 2018-%d Smallstep Labs, Inc.
|
||||
`, time.Now().Year())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func createPKI(c *cloudkms.CloudKMS, project, location, keyRing string, protectionLevel apiv1.ProtectionLevel) error {
|
||||
ui.Println("Creating PKI ...")
|
||||
|
||||
parent := "projects/" + project + "/locations/" + location + "/keyRings/" + keyRing + "/cryptoKeys"
|
||||
|
||||
// Root Certificate
|
||||
resp, err := c.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: parent + "/root",
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
ProtectionLevel: protectionLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signer, err := c.CreateSigner(&resp.CreateSignerRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
root := &x509.Certificate{
|
||||
IsCA: true,
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour * 24 * 365 * 10),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
MaxPathLen: 1,
|
||||
MaxPathLenZero: false,
|
||||
Issuer: pkix.Name{CommonName: "Smallstep Root"},
|
||||
Subject: pkix.Name{CommonName: "Smallstep Root"},
|
||||
SerialNumber: mustSerialNumber(),
|
||||
SubjectKeyId: mustSubjectKeyID(resp.PublicKey),
|
||||
AuthorityKeyId: mustSubjectKeyID(resp.PublicKey),
|
||||
}
|
||||
|
||||
b, err := x509.CreateCertificate(rand.Reader, root, root, resp.PublicKey, signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: b,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSelected("Root Key", resp.Name)
|
||||
ui.PrintSelected("Root Certificate", "root_ca.crt")
|
||||
|
||||
root, err = pemutil.ReadCertificate("root_ca.crt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Intermediate Certificate
|
||||
resp, err = c.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: parent + "/intermediate",
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
ProtectionLevel: protectionLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intermediate := &x509.Certificate{
|
||||
IsCA: true,
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour * 24 * 365 * 10),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
MaxPathLen: 0,
|
||||
MaxPathLenZero: true,
|
||||
Issuer: root.Subject,
|
||||
Subject: pkix.Name{CommonName: "Smallstep Intermediate"},
|
||||
SerialNumber: mustSerialNumber(),
|
||||
SubjectKeyId: mustSubjectKeyID(resp.PublicKey),
|
||||
}
|
||||
|
||||
b, err = x509.CreateCertificate(rand.Reader, intermediate, root, resp.PublicKey, signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: b,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSelected("Intermediate Key", resp.Name)
|
||||
ui.PrintSelected("Intermediate Certificate", "intermediate_ca.crt")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSSH(c *cloudkms.CloudKMS, project, location, keyRing string, protectionLevel apiv1.ProtectionLevel) error {
|
||||
ui.Println("Creating SSH Keys ...")
|
||||
|
||||
parent := "projects/" + project + "/locations/" + location + "/keyRings/" + keyRing + "/cryptoKeys"
|
||||
|
||||
// User Key
|
||||
resp, err := c.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: parent + "/ssh-user-key",
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
ProtectionLevel: protectionLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := ssh.NewPublicKey(resp.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile("ssh_user_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSelected("SSH User Public Key", "ssh_user_ca_key.pub")
|
||||
ui.PrintSelected("SSH User Private Key", resp.Name)
|
||||
|
||||
// Host Key
|
||||
resp, err = c.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: parent + "/ssh-host-key",
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
ProtectionLevel: protectionLevel,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err = ssh.NewPublicKey(resp.PublicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile("ssh_host_ca_key.pub", ssh.MarshalAuthorizedKey(key), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSelected("SSH Host Public Key", "ssh_host_ca_key.pub")
|
||||
ui.PrintSelected("SSH Host Private Key", resp.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustSerialNumber() *big.Int {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
sn, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sn
|
||||
}
|
||||
|
||||
func mustSubjectKeyID(key crypto.PublicKey) []byte {
|
||||
b, err := x509.MarshalPKIXPublicKey(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
//nolint:gosec // used to create the Subject Key Identifier by RFC 5280
|
||||
hash := sha1.Sum(b)
|
||||
return hash[:]
|
||||
}
|
|
@ -1,553 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha1" //nolint:gosec // used to create the Subject Key Identifier by RFC 5280
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.step.sm/cli-utils/fileutil"
|
||||
"go.step.sm/cli-utils/ui"
|
||||
"go.step.sm/crypto/kms"
|
||||
"go.step.sm/crypto/kms/apiv1"
|
||||
"go.step.sm/crypto/kms/uri"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
|
||||
// Enable pkcs11.
|
||||
_ "go.step.sm/crypto/kms/pkcs11"
|
||||
)
|
||||
|
||||
// Config is a mapping of the cli flags.
|
||||
type Config struct {
|
||||
KMS string
|
||||
GenerateRoot bool
|
||||
RootObject string
|
||||
RootKeyObject string
|
||||
RootSubject string
|
||||
RootPath string
|
||||
CrtObject string
|
||||
CrtPath string
|
||||
CrtKeyObject string
|
||||
CrtSubject string
|
||||
CrtKeyPath string
|
||||
SSHHostKeyObject string
|
||||
SSHUserKeyObject string
|
||||
RootFile string
|
||||
KeyFile string
|
||||
Pin string
|
||||
PinFile string
|
||||
NoCerts bool
|
||||
EnableSSH bool
|
||||
Force bool
|
||||
Extractable bool
|
||||
}
|
||||
|
||||
// Validate checks the flags in the config.
|
||||
func (c *Config) Validate() error {
|
||||
switch {
|
||||
case c.KMS == "":
|
||||
return errors.New("flag `--kms` is required")
|
||||
case c.CrtPath == "":
|
||||
return errors.New("flag `--crt-cert-path` is required")
|
||||
case c.RootFile != "" && c.KeyFile == "":
|
||||
return errors.New("flag `--root-cert-file` requires flag `--root-key-file`")
|
||||
case c.KeyFile != "" && c.RootFile == "":
|
||||
return errors.New("flag `--root-key-file` requires flag `--root-cert-file`")
|
||||
case c.RootFile == "" && c.RootObject == "":
|
||||
return errors.New("one of flag `--root-cert-file` or `--root-cert-obj` is required")
|
||||
case c.KeyFile == "" && c.RootKeyObject == "":
|
||||
return errors.New("one of flag `--root-key-file` or `--root-key-obj` is required")
|
||||
case c.CrtKeyPath == "" && c.CrtKeyObject == "":
|
||||
return errors.New("one of flag `--crt-key-path` or `--crt-key-obj` is required")
|
||||
case c.RootFile == "" && c.GenerateRoot && c.RootKeyObject == "":
|
||||
return errors.New("flag `--root-gen` requires flag `--root-key-obj`")
|
||||
case c.RootFile == "" && c.GenerateRoot && c.RootPath == "":
|
||||
return errors.New("flag `--root-gen` requires `--root-cert-path`")
|
||||
case c.Pin != "" && c.PinFile != "":
|
||||
return errors.New("Only set one of pin and pin-file")
|
||||
default:
|
||||
if c.RootFile != "" {
|
||||
c.GenerateRoot = false
|
||||
c.RootObject = ""
|
||||
c.RootKeyObject = ""
|
||||
}
|
||||
if c.CrtKeyPath != "" {
|
||||
c.CrtObject = ""
|
||||
c.CrtKeyObject = ""
|
||||
}
|
||||
if !c.EnableSSH {
|
||||
c.SSHHostKeyObject = ""
|
||||
c.SSHUserKeyObject = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
var kmsuri string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
kmsuri = "pkcs11:module-path=/usr/local/lib/pkcs11/yubihsm_pkcs11.dylib;token=YubiHSM"
|
||||
case "linux":
|
||||
kmsuri = "pkcs11:module-path=/usr/lib/x86_64-linux-gnu/pkcs11/yubihsm_pkcs11.so;token=YubiHSM"
|
||||
case "windows":
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
kmsuri = "pkcs11:module-path=" + home + "\\yubihsm2-sdk\\bin\\yubihsm_pkcs11.dll" + ";token=YubiHSM"
|
||||
}
|
||||
}
|
||||
|
||||
var c Config
|
||||
flag.StringVar(&c.KMS, "kms", kmsuri, "PKCS #11 URI with the module-path and token to connect to the module.")
|
||||
flag.StringVar(&c.Pin, "pin", "", "PKCS #11 PIN")
|
||||
flag.StringVar(&c.PinFile, "pin-file", "", "PKCS #11 PIN File")
|
||||
// Option 1: Generate new root
|
||||
flag.BoolVar(&c.GenerateRoot, "root-gen", true, "Enable the generation of a root key.")
|
||||
flag.StringVar(&c.RootSubject, "root-name", "PKCS #11 Smallstep Root", "Subject and Issuer of the root certificate.")
|
||||
flag.StringVar(&c.RootObject, "root-cert-obj", "pkcs11:id=7330;object=root-cert", "PKCS #11 URI with object id and label to store the root certificate.")
|
||||
flag.StringVar(&c.RootKeyObject, "root-key-obj", "pkcs11:id=7330;object=root-key", "PKCS #11 URI with object id and label to store the root key.")
|
||||
// Option 2: Read root from disk and sign intermediate
|
||||
flag.StringVar(&c.RootFile, "root-cert-file", "", "Path to the root certificate to use.")
|
||||
flag.StringVar(&c.KeyFile, "root-key-file", "", "Path to the root key to use.")
|
||||
// Option 3: Generate certificate signing request
|
||||
flag.StringVar(&c.CrtSubject, "crt-name", "PKCS #11 Smallstep Intermediate", "Subject of the intermediate certificate.")
|
||||
flag.StringVar(&c.CrtObject, "crt-cert-obj", "pkcs11:id=7331;object=intermediate-cert", "PKCS #11 URI with object id and label to store the intermediate certificate.")
|
||||
flag.StringVar(&c.CrtKeyObject, "crt-key-obj", "pkcs11:id=7331;object=intermediate-key", "PKCS #11 URI with object id and label to store the intermediate certificate.")
|
||||
// SSH certificates
|
||||
flag.BoolVar(&c.EnableSSH, "ssh", false, "Enable the creation of ssh keys.")
|
||||
flag.StringVar(&c.SSHHostKeyObject, "ssh-host-key", "pkcs11:id=7332;object=ssh-host-key", "PKCS #11 URI with object id and label to store the key used to sign SSH host certificates.")
|
||||
flag.StringVar(&c.SSHUserKeyObject, "ssh-user-key", "pkcs11:id=7333;object=ssh-user-key", "PKCS #11 URI with object id and label to store the key used to sign SSH user certificates.")
|
||||
// Output files
|
||||
flag.StringVar(&c.RootPath, "root-cert-path", "root_ca.crt", "Location to write the root certificate.")
|
||||
flag.StringVar(&c.CrtPath, "crt-cert-path", "intermediate_ca.crt", "Location to write the intermediate certificate.")
|
||||
flag.StringVar(&c.CrtKeyPath, "crt-key-path", "", "Location to write the intermediate private key.")
|
||||
// Others
|
||||
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()
|
||||
|
||||
if err := c.Validate(); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
u, err := uri.ParseWithScheme("pkcs11", c.KMS)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
// Initialize windows terminal
|
||||
ui.Init()
|
||||
|
||||
ui.Println("⚠️ This command is deprecated and will be removed in future releases.")
|
||||
ui.Println("⚠️ Please use https://github.com/smallstep/step-kms-plugin instead.")
|
||||
|
||||
switch {
|
||||
case u.Get("pin-value") != "":
|
||||
case u.Get("pin-source") != "":
|
||||
case c.Pin != "":
|
||||
case c.PinFile != "":
|
||||
content, err := os.ReadFile(c.PinFile)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
c.Pin = string(content)
|
||||
|
||||
default:
|
||||
pin, err := ui.PromptPassword("What is the PKCS#11 PIN?")
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
c.Pin = string(pin)
|
||||
}
|
||||
|
||||
k, err := kms.New(context.Background(), apiv1.Options{
|
||||
Type: apiv1.PKCS11,
|
||||
URI: c.KMS,
|
||||
Pin: c.Pin,
|
||||
})
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = k.Close()
|
||||
}()
|
||||
|
||||
// Check if the slots are empty, fail if they are not
|
||||
certUris := []string{
|
||||
c.RootObject, c.CrtObject,
|
||||
}
|
||||
keyUris := []string{
|
||||
c.RootKeyObject, c.CrtKeyObject,
|
||||
c.SSHHostKeyObject, c.SSHUserKeyObject,
|
||||
}
|
||||
if !c.Force {
|
||||
for _, u := range certUris {
|
||||
if u != "" && !c.NoCerts {
|
||||
checkObject(k, u)
|
||||
checkCertificate(k, u)
|
||||
}
|
||||
}
|
||||
for _, u := range keyUris {
|
||||
if u != "" {
|
||||
checkObject(k, u)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deleter, ok := k.(interface {
|
||||
DeleteKey(uri string) error
|
||||
DeleteCertificate(uri string) error
|
||||
})
|
||||
if ok {
|
||||
for _, u := range certUris {
|
||||
if u != "" && !c.NoCerts {
|
||||
// Some HSMs like Nitrokey will overwrite the key with the
|
||||
// certificate label.
|
||||
if err := deleter.DeleteKey(u); err != nil {
|
||||
fatalClose(err, k)
|
||||
}
|
||||
if err := deleter.DeleteCertificate(u); err != nil {
|
||||
fatalClose(err, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, u := range keyUris {
|
||||
if u != "" {
|
||||
if err := deleter.DeleteKey(u); err != nil {
|
||||
fatalClose(err, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := createPKI(k, c); err != nil {
|
||||
fatalClose(err, k)
|
||||
}
|
||||
|
||||
// Reset windows terminal
|
||||
ui.Reset()
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
if os.Getenv("STEPDEBUG") == "1" {
|
||||
fmt.Fprintf(os.Stderr, "%+v\n", err)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
ui.Reset()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func fatalClose(err error, k kms.KeyManager) {
|
||||
_ = k.Close()
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "Usage: step-pkcs11-init")
|
||||
fmt.Fprintln(os.Stderr, `
|
||||
The step-pkcs11-init command initializes a public key infrastructure (PKI)
|
||||
to be used by step-ca.
|
||||
|
||||
This tool is experimental and in the future it will be integrated in step cli.
|
||||
|
||||
OPTIONS`)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
COPYRIGHT
|
||||
|
||||
(c) 2018-%d Smallstep Labs, Inc.
|
||||
`, time.Now().Year())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func checkCertificate(k kms.KeyManager, rawuri string) {
|
||||
if cm, ok := k.(kms.CertificateManager); ok {
|
||||
if _, err := cm.LoadCertificate(&apiv1.LoadCertificateRequest{
|
||||
Name: rawuri,
|
||||
}); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Your PKCS #11 module already has a certificate on %s.\n", rawuri)
|
||||
fmt.Fprintln(os.Stderr, " If you want to delete it and start fresh, use `--force`.")
|
||||
_ = k.Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkObject(k kms.KeyManager, rawuri string) {
|
||||
if _, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{
|
||||
Name: rawuri,
|
||||
}); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Your PKCS #11 module already has a key on %s.\n", rawuri)
|
||||
fmt.Fprintln(os.Stderr, " If you want to delete it and start fresh, use `--force`.")
|
||||
_ = k.Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func createPKI(k kms.KeyManager, c Config) error {
|
||||
var err error
|
||||
ui.Println("Creating PKI ...")
|
||||
now := time.Now()
|
||||
|
||||
// Root Certificate
|
||||
var signer crypto.Signer
|
||||
var root *x509.Certificate
|
||||
switch {
|
||||
case c.GenerateRoot:
|
||||
resp, err := k.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: c.RootKeyObject,
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
Extractable: c.Extractable,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signer, err = k.CreateSigner(&resp.CreateSignerRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
IsCA: true,
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour * 24 * 365 * 10),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
MaxPathLen: 1,
|
||||
MaxPathLenZero: false,
|
||||
Issuer: pkix.Name{CommonName: c.RootSubject},
|
||||
Subject: pkix.Name{CommonName: c.RootSubject},
|
||||
SerialNumber: mustSerialNumber(),
|
||||
SubjectKeyId: mustSubjectKeyID(resp.PublicKey),
|
||||
AuthorityKeyId: mustSubjectKeyID(resp.PublicKey),
|
||||
}
|
||||
|
||||
b, err := x509.CreateCertificate(rand.Reader, template, template, resp.PublicKey, signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
root, err = x509.ParseCertificate(b)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing root certificate")
|
||||
}
|
||||
|
||||
if cm, ok := k.(kms.CertificateManager); ok && c.RootObject != "" && !c.NoCerts {
|
||||
if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{
|
||||
Name: c.RootObject,
|
||||
Certificate: root,
|
||||
Extractable: c.Extractable,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
c.RootObject = ""
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile(c.RootPath, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: b,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSelected("Root Key", resp.Name)
|
||||
ui.PrintSelected("Root Certificate", c.RootPath)
|
||||
if c.RootObject != "" {
|
||||
ui.PrintSelected("Root Certificate Object", c.RootObject)
|
||||
}
|
||||
case c.RootFile != "" && c.KeyFile != "": // Read Root From File
|
||||
root, err = pemutil.ReadCertificate(c.RootFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := pemutil.Read(c.KeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if signer, ok = key.(crypto.Signer); !ok {
|
||||
return errors.Errorf("key type '%T' does not implement a signer", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Intermediate Certificate
|
||||
var keyName string
|
||||
var publicKey crypto.PublicKey
|
||||
var intSigner crypto.Signer
|
||||
if c.CrtKeyPath != "" {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating intermediate key")
|
||||
}
|
||||
|
||||
pass, err := ui.PromptPasswordGenerate("What do you want your password to be? [leave empty and we'll generate one]",
|
||||
ui.WithRichPrompt())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = pemutil.Serialize(priv, pemutil.WithPassword(pass), pemutil.ToFile(c.CrtKeyPath, 0600))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publicKey = priv.Public()
|
||||
intSigner = priv
|
||||
} else {
|
||||
resp, err := k.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: c.CrtKeyObject,
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
Extractable: c.Extractable,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicKey = resp.PublicKey
|
||||
keyName = resp.Name
|
||||
|
||||
intSigner, err = k.CreateSigner(&resp.CreateSignerRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if root != nil {
|
||||
template := &x509.Certificate{
|
||||
IsCA: true,
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour * 24 * 365 * 10),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
MaxPathLen: 0,
|
||||
MaxPathLenZero: true,
|
||||
Issuer: root.Subject,
|
||||
Subject: pkix.Name{CommonName: c.CrtSubject},
|
||||
SerialNumber: mustSerialNumber(),
|
||||
SubjectKeyId: mustSubjectKeyID(publicKey),
|
||||
}
|
||||
|
||||
b, err := x509.CreateCertificate(rand.Reader, template, root, publicKey, signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intermediate, err := x509.ParseCertificate(b)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing intermediate certificate")
|
||||
}
|
||||
|
||||
if cm, ok := k.(kms.CertificateManager); ok && c.CrtObject != "" && !c.NoCerts {
|
||||
if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{
|
||||
Name: c.CrtObject,
|
||||
Certificate: intermediate,
|
||||
Extractable: c.Extractable,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
c.CrtObject = ""
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile(c.CrtPath, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: b,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// No root available, generate CSR for external root.
|
||||
csrTemplate := x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: c.CrtSubject},
|
||||
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||
}
|
||||
// step: generate the csr request
|
||||
csrCertificate, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, intSigner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileutil.WriteFile(c.CrtPath, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrCertificate,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.CrtKeyPath != "" {
|
||||
ui.PrintSelected("Intermediate Key", c.CrtKeyPath)
|
||||
} else {
|
||||
ui.PrintSelected("Intermediate Key", keyName)
|
||||
}
|
||||
|
||||
if root != nil {
|
||||
ui.PrintSelected("Intermediate Certificate", c.CrtPath)
|
||||
if c.CrtObject != "" {
|
||||
ui.PrintSelected("Intermediate Certificate Object", c.CrtObject)
|
||||
}
|
||||
} else {
|
||||
ui.PrintSelected("Intermediate Certificate Request", c.CrtPath)
|
||||
}
|
||||
|
||||
if c.SSHHostKeyObject != "" {
|
||||
resp, err := k.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: c.SSHHostKeyObject,
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ui.PrintSelected("SSH Host Key", resp.Name)
|
||||
}
|
||||
|
||||
if c.SSHUserKeyObject != "" {
|
||||
resp, err := k.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: c.SSHUserKeyObject,
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ui.PrintSelected("SSH User Key", resp.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustSerialNumber() *big.Int {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
sn, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sn
|
||||
}
|
||||
|
||||
func mustSubjectKeyID(key crypto.PublicKey) []byte {
|
||||
b, err := x509.MarshalPKIXPublicKey(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
//nolint:gosec // used to create the Subject Key Identifier by RFC 5280
|
||||
hash := sha1.Sum(b)
|
||||
return hash[:]
|
||||
}
|
|
@ -1,355 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha1" //nolint:gosec // used to create the Subject Key Identifier by RFC 5280
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.step.sm/cli-utils/fileutil"
|
||||
"go.step.sm/cli-utils/ui"
|
||||
"go.step.sm/crypto/kms"
|
||||
"go.step.sm/crypto/kms/apiv1"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
|
||||
// Enable yubikey.
|
||||
_ "go.step.sm/crypto/kms/yubikey"
|
||||
)
|
||||
|
||||
// Config is a mapping of the cli flags.
|
||||
type Config struct {
|
||||
RootOnly bool
|
||||
RootSlot string
|
||||
CrtSlot string
|
||||
RootFile string
|
||||
KeyFile string
|
||||
Pin string
|
||||
ManagementKey string
|
||||
Force bool
|
||||
}
|
||||
|
||||
// Validate checks the flags in the config.
|
||||
func (c *Config) Validate() error {
|
||||
switch {
|
||||
case c.ManagementKey != "" && len(c.ManagementKey) != 48:
|
||||
return errors.New("flag `--management-key` must be 48 hexadecimal characters (24 bytes)")
|
||||
case c.RootFile != "" && c.KeyFile == "":
|
||||
return errors.New("flag `--root` requires flag `--key`")
|
||||
case c.KeyFile != "" && c.RootFile == "":
|
||||
return errors.New("flag `--key` requires flag `--root`")
|
||||
case c.RootOnly && c.RootFile != "":
|
||||
return errors.New("flag `--root-only` is incompatible with flag `--root`")
|
||||
case c.RootSlot == c.CrtSlot:
|
||||
return errors.New("flag `--root-slot` and flag `--crt-slot` cannot be the same")
|
||||
case c.RootFile == "" && c.RootSlot == "":
|
||||
return errors.New("one of flag `--root` or `--root-slot` is required")
|
||||
default:
|
||||
if c.RootFile != "" {
|
||||
c.RootSlot = ""
|
||||
}
|
||||
if c.RootOnly {
|
||||
c.CrtSlot = ""
|
||||
}
|
||||
if c.ManagementKey != "" {
|
||||
if _, err := hex.DecodeString(c.ManagementKey); err != nil {
|
||||
return errors.Wrap(err, "flag `--management-key` is not valid")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
var c Config
|
||||
flag.StringVar(&c.ManagementKey, "management-key", "", `Management key to use in hexadecimal format. (default "010203040506070801020304050607080102030405060708")`)
|
||||
flag.BoolVar(&c.RootOnly, "root-only", false, "Slot only the root certificate and sign and intermediate.")
|
||||
flag.StringVar(&c.RootSlot, "root-slot", "9a", "Slot to store the root certificate.")
|
||||
flag.StringVar(&c.CrtSlot, "crt-slot", "9c", "Slot to store the intermediate certificate.")
|
||||
flag.StringVar(&c.RootFile, "root", "", "Path to the root certificate to use.")
|
||||
flag.StringVar(&c.KeyFile, "key", "", "Path to the root key to use.")
|
||||
flag.BoolVar(&c.Force, "force", false, "Force the delete of previous keys.")
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
if err := c.Validate(); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
// Initialize windows terminal
|
||||
ui.Init()
|
||||
|
||||
ui.Println("⚠️ This command is deprecated and will be removed in future releases.")
|
||||
ui.Println("⚠️ Please use https://github.com/smallstep/step-kms-plugin instead.")
|
||||
|
||||
pin, err := ui.PromptPassword("What is the YubiKey PIN?")
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
c.Pin = string(pin)
|
||||
|
||||
k, err := kms.New(context.Background(), apiv1.Options{
|
||||
Type: apiv1.YubiKey,
|
||||
Pin: c.Pin,
|
||||
ManagementKey: c.ManagementKey,
|
||||
})
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
// Check if the slots are empty, fail if they are not
|
||||
if !c.Force {
|
||||
switch {
|
||||
case c.RootSlot != "":
|
||||
checkSlot(k, c.RootSlot)
|
||||
case c.CrtSlot != "":
|
||||
checkSlot(k, c.CrtSlot)
|
||||
}
|
||||
}
|
||||
|
||||
if err := createPKI(k, c); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = k.Close()
|
||||
}()
|
||||
|
||||
// Reset windows terminal
|
||||
ui.Reset()
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
if os.Getenv("STEPDEBUG") == "1" {
|
||||
fmt.Fprintf(os.Stderr, "%+v\n", err)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
ui.Reset()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "Usage: step-yubikey-init")
|
||||
fmt.Fprintln(os.Stderr, `
|
||||
The step-yubikey-init command initializes a public key infrastructure (PKI)
|
||||
to be used by step-ca.
|
||||
|
||||
This tool is experimental and in the future it will be integrated in step cli.
|
||||
|
||||
OPTIONS`)
|
||||
fmt.Fprintln(os.Stderr)
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
COPYRIGHT
|
||||
|
||||
(c) 2018-%d Smallstep Labs, Inc.
|
||||
`, time.Now().Year())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func checkSlot(k kms.KeyManager, slot string) {
|
||||
if _, err := k.GetPublicKey(&apiv1.GetPublicKeyRequest{
|
||||
Name: slot,
|
||||
}); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "⚠️ Your YubiKey already has a key in the slot %s.\n", slot)
|
||||
fmt.Fprintln(os.Stderr, " If you want to delete it and start fresh, use `--force`.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func createPKI(k kms.KeyManager, c Config) error {
|
||||
var err error
|
||||
ui.Println("Creating PKI ...")
|
||||
now := time.Now()
|
||||
|
||||
// Root Certificate
|
||||
var signer crypto.Signer
|
||||
var root *x509.Certificate
|
||||
if c.RootFile != "" && c.KeyFile != "" {
|
||||
root, err = pemutil.ReadCertificate(c.RootFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := pemutil.Read(c.KeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if signer, ok = key.(crypto.Signer); !ok {
|
||||
return errors.Errorf("key type '%T' does not implement a signer", key)
|
||||
}
|
||||
} else {
|
||||
resp, err := k.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: c.RootSlot,
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signer, err = k.CreateSigner(&resp.CreateSignerRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
IsCA: true,
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour * 24 * 365 * 10),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
MaxPathLen: 1,
|
||||
MaxPathLenZero: false,
|
||||
Issuer: pkix.Name{CommonName: "YubiKey Smallstep Root"},
|
||||
Subject: pkix.Name{CommonName: "YubiKey Smallstep Root"},
|
||||
SerialNumber: mustSerialNumber(),
|
||||
SubjectKeyId: mustSubjectKeyID(resp.PublicKey),
|
||||
AuthorityKeyId: mustSubjectKeyID(resp.PublicKey),
|
||||
}
|
||||
|
||||
b, err := x509.CreateCertificate(rand.Reader, template, template, resp.PublicKey, signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
root, err = x509.ParseCertificate(b)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing root certificate")
|
||||
}
|
||||
|
||||
if cm, ok := k.(kms.CertificateManager); ok {
|
||||
if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{
|
||||
Name: c.RootSlot,
|
||||
Certificate: root,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile("root_ca.crt", pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: b,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSelected("Root Key", resp.Name)
|
||||
ui.PrintSelected("Root Certificate", "root_ca.crt")
|
||||
}
|
||||
|
||||
// Intermediate Certificate
|
||||
var keyName string
|
||||
var publicKey crypto.PublicKey
|
||||
if c.RootOnly {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating intermediate key")
|
||||
}
|
||||
|
||||
pass, err := ui.PromptPasswordGenerate("What do you want your password to be? [leave empty and we'll generate one]",
|
||||
ui.WithRichPrompt())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = pemutil.Serialize(priv, pemutil.WithPassword(pass), pemutil.ToFile("intermediate_ca_key", 0600))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publicKey = priv.Public()
|
||||
} else {
|
||||
resp, err := k.CreateKey(&apiv1.CreateKeyRequest{
|
||||
Name: c.CrtSlot,
|
||||
SignatureAlgorithm: apiv1.ECDSAWithSHA256,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicKey = resp.PublicKey
|
||||
keyName = resp.Name
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
IsCA: true,
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour * 24 * 365 * 10),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
MaxPathLen: 0,
|
||||
MaxPathLenZero: true,
|
||||
Issuer: root.Subject,
|
||||
Subject: pkix.Name{CommonName: "YubiKey Smallstep Intermediate"},
|
||||
SerialNumber: mustSerialNumber(),
|
||||
SubjectKeyId: mustSubjectKeyID(publicKey),
|
||||
}
|
||||
|
||||
b, err := x509.CreateCertificate(rand.Reader, template, root, publicKey, signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
intermediate, err := x509.ParseCertificate(b)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing intermediate certificate")
|
||||
}
|
||||
|
||||
if cm, ok := k.(kms.CertificateManager); ok {
|
||||
if err := cm.StoreCertificate(&apiv1.StoreCertificateRequest{
|
||||
Name: c.CrtSlot,
|
||||
Certificate: intermediate,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := fileutil.WriteFile("intermediate_ca.crt", pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: b,
|
||||
}), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.RootOnly {
|
||||
ui.PrintSelected("Intermediate Key", "intermediate_ca_key")
|
||||
} else {
|
||||
ui.PrintSelected("Intermediate Key", keyName)
|
||||
}
|
||||
|
||||
ui.PrintSelected("Intermediate Certificate", "intermediate_ca.crt")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustSerialNumber() *big.Int {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
sn, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sn
|
||||
}
|
||||
|
||||
func mustSubjectKeyID(key crypto.PublicKey) []byte {
|
||||
b, err := x509.MarshalPKIXPublicKey(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
//nolint:gosec // used to create the Subject Key Identifier by RFC 5280
|
||||
hash := sha1.Sum(b)
|
||||
return hash[:]
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
|
@ -29,7 +30,7 @@ var AppCommand = cli.Command{
|
|||
Action: appAction,
|
||||
UsageText: `**step-ca** <config> [**--password-file**=<file>]
|
||||
[**--ssh-host-password-file**=<file>] [**--ssh-user-password-file**=<file>]
|
||||
[**--issuer-password-file**=<file>] [**--resolver**=<addr>]`,
|
||||
[**--issuer-password-file**=<file>] [**--pidfile**=<file>] [**--resolver**=<addr>]`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "password-file",
|
||||
|
@ -82,6 +83,10 @@ Requires **--insecure** flag.`,
|
|||
Usage: `the <port> used on tls-alpn-01 challenges. It can be changed for testing purposes.
|
||||
Requires **--insecure** flag.`,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "pidfile",
|
||||
Usage: "the path to the <file> to write the process ID.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "insecure",
|
||||
Usage: "enable insecure flags.",
|
||||
|
@ -89,6 +94,8 @@ Requires **--insecure** flag.`,
|
|||
},
|
||||
}
|
||||
|
||||
var pidfile string
|
||||
|
||||
// AppAction is the action used when the top command runs.
|
||||
func appAction(ctx *cli.Context) error {
|
||||
passFile := ctx.String("password-file")
|
||||
|
@ -141,6 +148,13 @@ func appAction(ctx *cli.Context) error {
|
|||
|
||||
cfg, err := config.LoadConfiguration(configFile)
|
||||
if err != nil && token == "" {
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
fmt.Println("step-ca can't find or open the configuration file for your CA.")
|
||||
fmt.Println("You may need to create a CA first by running `step ca init`.")
|
||||
fmt.Println("Documentation: https://u.step.sm/docs/ca")
|
||||
os.Exit(1)
|
||||
}
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
|
@ -213,6 +227,15 @@ To get a linked authority token:
|
|||
issuerPassword = bytes.TrimRightFunc(issuerPassword, unicode.IsSpace)
|
||||
}
|
||||
|
||||
if filename := ctx.String("pidfile"); filename != "" {
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
//nolint:gosec // 0644 (-rw-r--r--) are common permissions for a pid file
|
||||
if err := os.WriteFile(filename, pid, 0644); err != nil {
|
||||
fatal(errors.Wrap(err, "error writing pidfile"))
|
||||
}
|
||||
pidfile = filename
|
||||
}
|
||||
|
||||
// replace resolver if requested
|
||||
if resolver != "" {
|
||||
net.DefaultResolver.PreferGo = true
|
||||
|
@ -237,6 +260,11 @@ To get a linked authority token:
|
|||
if err = srv.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
if pidfile != "" {
|
||||
os.Remove(pidfile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -269,5 +297,8 @@ func fatal(err error) {
|
|||
} else {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
if pidfile != "" {
|
||||
os.Remove(pidfile)
|
||||
}
|
||||
os.Exit(2)
|
||||
}
|
||||
|
|
5
debian/changelog
vendored
5
debian/changelog
vendored
|
@ -1,5 +0,0 @@
|
|||
step-ca (0.8.4-14-ge72f087-dev) unstable; urgency=medium
|
||||
|
||||
* See https://github.com/smallstep/certificates/releases
|
||||
|
||||
-- Smallstep Labs, Inc. <techadmin@smallstep.com> Wed, 20 Feb 2019 20:44:25 +0000
|
1
debian/compat
vendored
1
debian/compat
vendored
|
@ -1 +0,0 @@
|
|||
10
|
15
debian/control
vendored
15
debian/control
vendored
|
@ -1,15 +0,0 @@
|
|||
Source: step-ca
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Maintainer: Smallstep Labs, Inc. <techadmin@smallstep.com>
|
||||
Build-Depends: debhelper (>= 9), git, bash-completion
|
||||
Standards-Version: 4.2.0
|
||||
Homepage: https://github.com/smallstep/certificates
|
||||
Vcs-Browser: https://github.com/smallstep/certificates.git
|
||||
Vcs-Git: https://github.com/smallstep/certificates.git
|
||||
|
||||
Package: step-ca
|
||||
Architecture: any
|
||||
Depends: ${misc:Depends}
|
||||
Description: Smallstep Certificate Authority
|
||||
step-ca is the Smallstep Certificate Authority.
|
13
debian/rules
vendored
13
debian/rules
vendored
|
@ -1,13 +0,0 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
override_dh_install-arch:
|
||||
dh_install --arch
|
||||
|
||||
build:
|
||||
dh build
|
||||
|
||||
override_dh_auto_build:
|
||||
dh_auto_build -- build
|
||||
|
||||
%:
|
||||
dh $@
|
1
debian/source/format
vendored
1
debian/source/format
vendored
|
@ -1 +0,0 @@
|
|||
3.0 (quilt)
|
|
@ -3,19 +3,17 @@ FROM golang:alpine AS builder
|
|||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
RUN apk add --no-cache curl git make
|
||||
RUN make V=1 download
|
||||
RUN make V=1 bin/step-ca bin/step-awskms-init bin/step-cloudkms-init
|
||||
RUN apk add --no-cache curl git make libcap
|
||||
RUN make V=1 bin/step-ca
|
||||
RUN setcap CAP_NET_BIND_SERVICE=+eip bin/step-ca
|
||||
|
||||
FROM smallstep/step-kms-plugin:cloud AS kms
|
||||
|
||||
FROM smallstep/step-cli:latest
|
||||
|
||||
COPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca
|
||||
COPY --from=builder /src/bin/step-awskms-init /usr/local/bin/step-awskms-init
|
||||
COPY --from=builder /src/bin/step-cloudkms-init /usr/local/bin/step-cloudkms-init
|
||||
COPY --from=kms /usr/local/bin/step-kms-plugin /usr/local/bin/step-kms-plugin
|
||||
|
||||
USER root
|
||||
RUN apk add --no-cache libcap && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca
|
||||
USER step
|
||||
|
||||
ENV CONFIGPATH="/home/step/config/ca.json"
|
36
docker/Dockerfile.hsm
Normal file
36
docker/Dockerfile.hsm
Normal file
|
@ -0,0 +1,36 @@
|
|||
FROM golang AS builder
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
gcc pkgconf libpcsclite-dev libcap2-bin
|
||||
RUN make V=1 GOFLAGS="" bin/step-ca
|
||||
RUN setcap CAP_NET_BIND_SERVICE=+eip bin/step-ca
|
||||
|
||||
FROM smallstep/step-kms-plugin:bullseye AS kms
|
||||
|
||||
FROM smallstep/step-cli:bullseye
|
||||
|
||||
COPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca
|
||||
COPY --from=kms /usr/local/bin/step-kms-plugin /usr/local/bin/step-kms-plugin
|
||||
|
||||
USER root
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --no-install-recommends pcscd libpcsclite1
|
||||
RUN mkdir -p /run/pcscd
|
||||
RUN chown step:step /run/pcscd
|
||||
USER step
|
||||
|
||||
ENV CONFIGPATH="/home/step/config/ca.json"
|
||||
ENV PWDPATH="/home/step/secrets/password"
|
||||
|
||||
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
|
|
@ -1,35 +0,0 @@
|
|||
FROM golang:alpine AS builder
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
RUN apk add --no-cache curl git make
|
||||
RUN apk add --no-cache gcc musl-dev pkgconf pcsc-lite-dev
|
||||
RUN make V=1 download
|
||||
RUN make V=1 GOFLAGS="" build
|
||||
|
||||
|
||||
FROM smallstep/step-cli:latest
|
||||
|
||||
COPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca
|
||||
COPY --from=builder /src/bin/step-awskms-init /usr/local/bin/step-awskms-init
|
||||
COPY --from=builder /src/bin/step-cloudkms-init /usr/local/bin/step-cloudkms-init
|
||||
COPY --from=builder /src/bin/step-pkcs11-init /usr/local/bin/step-pkcs11-init
|
||||
COPY --from=builder /src/bin/step-yubikey-init /usr/local/bin/step-yubikey-init
|
||||
|
||||
USER root
|
||||
RUN apk add --no-cache libcap && setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/step-ca
|
||||
RUN apk add --no-cache pcsc-lite pcsc-lite-libs
|
||||
USER step
|
||||
|
||||
ENV CONFIGPATH="/home/step/config/ca.json"
|
||||
ENV PWDPATH="/home/step/secrets/password"
|
||||
|
||||
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
|
|
@ -19,7 +19,7 @@ function init_if_possible () {
|
|||
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"
|
||||
>&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
|
||||
|
@ -34,31 +34,53 @@ function generate_password () {
|
|||
|
||||
# Initialize a CA if not already initialized
|
||||
function step_ca_init () {
|
||||
DOCKER_STEPCA_INIT_PROVISIONER_NAME="${DOCKER_STEPCA_INIT_PROVISIONER_NAME:-admin}"
|
||||
DOCKER_STEPCA_INIT_ADMIN_SUBJECT="${DOCKER_STEPCA_INIT_ADMIN_SUBJECT:-step}"
|
||||
DOCKER_STEPCA_INIT_ADDRESS="${DOCKER_STEPCA_INIT_ADDRESS:-:9000}"
|
||||
|
||||
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"
|
||||
--dns "${DOCKER_STEPCA_INIT_DNS_NAMES}"
|
||||
--provisioner "${DOCKER_STEPCA_INIT_PROVISIONER_NAME}"
|
||||
--password-file "${STEPPATH}/password"
|
||||
--provisioner-password-file "${STEPPATH}/provisioner_password"
|
||||
--address "${DOCKER_STEPCA_INIT_ADDRESS}"
|
||||
)
|
||||
if [ -n "${DOCKER_STEPCA_INIT_PASSWORD}" ]; then
|
||||
echo "${DOCKER_STEPCA_INIT_PASSWORD}" > "${STEPPATH}/password"
|
||||
echo "${DOCKER_STEPCA_INIT_PASSWORD}" > "${STEPPATH}/provisioner_password"
|
||||
else
|
||||
generate_password > "${STEPPATH}/password"
|
||||
generate_password > "${STEPPATH}/provisioner_password"
|
||||
fi
|
||||
if [ -n "${DOCKER_STEPCA_INIT_SSH}" ]; then
|
||||
if [ "${DOCKER_STEPCA_INIT_SSH}" == "true" ]; then
|
||||
setup_args=("${setup_args[@]}" --ssh)
|
||||
fi
|
||||
if [ "${DOCKER_STEPCA_INIT_ACME}" == "true" ]; then
|
||||
setup_args=("${setup_args[@]}" --acme)
|
||||
fi
|
||||
if [ "${DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT}" == "true" ]; then
|
||||
setup_args=("${setup_args[@]}" --remote-management
|
||||
--admin-subject "${DOCKER_STEPCA_INIT_ADMIN_SUBJECT}"
|
||||
)
|
||||
fi
|
||||
step ca init "${setup_args[@]}"
|
||||
echo ""
|
||||
if [ "${DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT}" == "true" ]; then
|
||||
echo "👉 Your CA administrative username is: ${DOCKER_STEPCA_INIT_ADMIN_SUBJECT}"
|
||||
fi
|
||||
echo "👉 Your CA administrative password is: $(< $STEPPATH/provisioner_password )"
|
||||
echo "🤫 This will only be displayed once."
|
||||
shred -u $STEPPATH/provisioner_password
|
||||
mv $STEPPATH/password $PWDPATH
|
||||
}
|
||||
|
||||
if [ -f /usr/sbin/pcscd ]; then
|
||||
/usr/sbin/pcscd
|
||||
/usr/sbin/pcscd
|
||||
fi
|
||||
|
||||
if [ ! -f "${STEPPATH}/config/ca.json" ]; then
|
||||
init_if_possible
|
||||
init_if_possible
|
||||
fi
|
||||
|
||||
exec "${@}"
|
||||
|
|
161
go.mod
161
go.mod
|
@ -3,150 +3,138 @@ module github.com/smallstep/certificates
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.105.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.3.0
|
||||
cloud.google.com/go/security v1.10.0
|
||||
github.com/Azure/azure-sdk-for-go v67.0.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.132 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.4.1
|
||||
cloud.google.com/go/security v1.14.0
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/fxamacker/cbor/v2 v2.4.0
|
||||
github.com/go-chi/chi v4.1.2+incompatible
|
||||
github.com/go-kit/kit v0.10.0 // indirect
|
||||
github.com/go-piv/piv-go v1.10.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-attestation v0.4.4-0.20220404204839-8820d49b18d9
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/go-tpm v0.3.3
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/googleapis/gax-go/v2 v2.7.0
|
||||
github.com/hashicorp/vault/api v1.8.2
|
||||
github.com/hashicorp/vault/api/auth/approle v0.3.0
|
||||
github.com/hashicorp/vault/api/auth/kubernetes v0.3.0
|
||||
github.com/jhump/protoreflect v1.9.0 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.8.0
|
||||
github.com/hashicorp/vault/api v1.9.1
|
||||
github.com/hashicorp/vault/api/auth/approle v0.4.0
|
||||
github.com/hashicorp/vault/api/auth/kubernetes v0.4.0
|
||||
github.com/micromdm/scep/v2 v2.1.0
|
||||
github.com/newrelic/go-agent/v3 v3.20.1
|
||||
github.com/newrelic/go-agent/v3 v3.21.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rs/xid v1.4.0
|
||||
github.com/rs/xid v1.5.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/slackhq/nebula v1.6.1
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262
|
||||
github.com/smallstep/nosql v0.5.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/urfave/cli v1.22.10
|
||||
github.com/smallstep/nosql v0.6.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/urfave/cli v1.22.13
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
|
||||
go.step.sm/cli-utils v0.7.5
|
||||
go.step.sm/crypto v0.23.1
|
||||
go.step.sm/linkedca v0.19.0
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
|
||||
golang.org/x/net v0.2.0
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
||||
google.golang.org/api v0.103.0
|
||||
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect
|
||||
google.golang.org/grpc v1.51.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
go.step.sm/cli-utils v0.7.6
|
||||
go.step.sm/crypto v0.29.3
|
||||
go.step.sm/linkedca v0.19.1
|
||||
golang.org/x/crypto v0.8.0
|
||||
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0
|
||||
golang.org/x/net v0.9.0
|
||||
google.golang.org/api v0.120.0
|
||||
google.golang.org/grpc v1.54.0
|
||||
google.golang.org/protobuf v1.30.0
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.12.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||
cloud.google.com/go/iam v0.6.0 // indirect
|
||||
cloud.google.com/go/kms v1.6.0 // indirect
|
||||
cloud.google.com/go v0.110.0 // indirect
|
||||
cloud.google.com/go/compute v1.19.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v0.13.0 // indirect
|
||||
cloud.google.com/go/kms v1.10.1 // indirect
|
||||
filippo.io/edwards25519 v1.0.0 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/armon/go-metrics v0.3.9 // indirect
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.240 // indirect
|
||||
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/go-kit/kit v0.10.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
|
||||
github.com/go-piv/piv-go v1.11.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.4 // indirect
|
||||
github.com/google/go-tpm-tools v0.3.11 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/google/s2a-go v0.1.2 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v0.16.2 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.4.5 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.6.6 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.2 // indirect
|
||||
github.com/hashicorp/go-version v1.2.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/vault/sdk v0.6.0 // indirect
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.13.0 // indirect
|
||||
github.com/jackc/pgconn v1.14.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.12.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgtype v1.14.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.18.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/compress v1.15.11 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/oklog/run v1.0.0 // indirect
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/schollz/jsonstore v1.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/thales-e-security/pool v0.0.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/oauth2 v0.7.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/time v0.1.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
@ -157,4 +145,7 @@ require (
|
|||
// replace go.step.sm/linkedca => ../linkedca
|
||||
|
||||
// use github.com/smallstep/pkcs7 fork with patches applied
|
||||
replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20221024180420-e1aab68dda05
|
||||
replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948
|
||||
|
||||
// use github.com/smallstep/go-attestation fork with patches for Windows AK support applied
|
||||
replace github.com/google/go-attestation v0.4.4-0.20220404204839-8820d49b18d9 => github.com/smallstep/go-attestation v0.4.4-0.20230224121042-1bcb20a75add
|
||||
|
|
|
@ -812,6 +812,11 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) {
|
|||
Templates: p.getTemplates(),
|
||||
}
|
||||
|
||||
// Disable the database when WithNoDB() option is passed.
|
||||
if p.options.noDB {
|
||||
cfg.DB = nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -305,6 +305,8 @@ func PKIOperation(ctx context.Context, req request) (Response, error) {
|
|||
|
||||
// NOTE: at this point we have sufficient information for returning nicely signed CertReps
|
||||
csr := msg.CSRReqMessage.CSR
|
||||
transactionID := string(msg.TransactionID)
|
||||
challengePassword := msg.CSRReqMessage.ChallengePassword
|
||||
|
||||
// NOTE: we're blocking the RenewalReq if the challenge does not match, because otherwise we don't have any authentication.
|
||||
// The macOS SCEP client performs renewals using PKCSreq. The CertNanny SCEP client will use PKCSreq with challenge too, it seems,
|
||||
|
@ -312,13 +314,11 @@ func PKIOperation(ctx context.Context, req request) (Response, error) {
|
|||
// a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients.
|
||||
// We'll have to see how it works out.
|
||||
if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq {
|
||||
challengeMatches, err := auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword)
|
||||
if err != nil {
|
||||
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("error when checking password"))
|
||||
}
|
||||
if !challengeMatches {
|
||||
// TODO: can this be returned safely to the client? In the end, if the password was correct, that gains a bit of info too.
|
||||
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("wrong password provided"))
|
||||
if err := auth.ValidateChallenge(ctx, challengePassword, transactionID); err != nil {
|
||||
if errors.Is(err, provisioner.ErrSCEPChallengeInvalid) {
|
||||
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, err)
|
||||
}
|
||||
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("failed validating challenge password"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package scep
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -456,24 +455,6 @@ func (a *Authority) CreateFailureResponse(ctx context.Context, csr *x509.Certifi
|
|||
return crepMsg, nil
|
||||
}
|
||||
|
||||
// MatchChallengePassword verifies a SCEP challenge password
|
||||
func (a *Authority) MatchChallengePassword(ctx context.Context, password string) (bool, error) {
|
||||
p, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(p.GetChallengePassword()), []byte(password)) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TODO: support dynamic challenges, i.e. a list of challenges instead of one?
|
||||
// That's probably a bit harder to configure, though; likely requires some data store
|
||||
// that can be interacted with more easily, via some internal API, for example.
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// GetCACaps returns the CA capabilities
|
||||
func (a *Authority) GetCACaps(ctx context.Context) []string {
|
||||
p, err := provisionerFromContext(ctx)
|
||||
|
@ -494,3 +475,11 @@ func (a *Authority) GetCACaps(ctx context.Context) []string {
|
|||
|
||||
return caps
|
||||
}
|
||||
|
||||
func (a *Authority) ValidateChallenge(ctx context.Context, challenge, transactionID string) error {
|
||||
p, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.ValidateChallenge(ctx, challenge, transactionID)
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ type Provisioner interface {
|
|||
GetName() string
|
||||
DefaultTLSCertDuration() time.Duration
|
||||
GetOptions() *provisioner.Options
|
||||
GetChallengePassword() string
|
||||
GetCapabilities() []string
|
||||
ShouldIncludeRootInChain() bool
|
||||
GetContentEncryptionAlgorithm() int
|
||||
ValidateChallenge(ctx context.Context, challenge, transactionID string) error
|
||||
}
|
||||
|
|
|
@ -188,7 +188,7 @@ CA_VERSION=$(curl -s https://api.github.com/repos/smallstep/certificates/release
|
|||
|
||||
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
|
||||
install -m 0755 -t /usr/bin step-ca_${CA_VERSION:1}/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}
|
||||
|
|
|
@ -108,10 +108,10 @@ var DefaultSSHTemplateData = map[string]string{
|
|||
{{- end }}
|
||||
{{- if or .User.GOOS "none" | eq "windows" }}
|
||||
UserKnownHostsFile "{{.User.StepPath}}\ssh\known_hosts"
|
||||
ProxyCommand C:\Windows\System32\cmd.exe /c step ssh proxycommand{{- if .User.Context }} --context {{ .User.Context }}{{- end }} %r %h %p
|
||||
ProxyCommand C:\Windows\System32\cmd.exe /c step ssh proxycommand{{- if .User.Context }} --context {{ .User.Context }}{{- end }}{{- if .User.Provisioner }} --provisioner {{ .User.Provisioner }}{{- end }} %r %h %p
|
||||
{{- else }}
|
||||
UserKnownHostsFile "{{.User.StepPath}}/ssh/known_hosts"
|
||||
ProxyCommand step ssh proxycommand{{- if .User.Context }} --context {{ .User.Context }}{{- end }} %r %h %p
|
||||
ProxyCommand step ssh proxycommand{{- if .User.Context }} --context {{ .User.Context }}{{- end }}{{- if .User.Provisioner }} --provisioner {{ .User.Provisioner }}{{- end }} %r %h %p
|
||||
{{- end }}
|
||||
`,
|
||||
|
||||
|
|
|
@ -68,4 +68,7 @@ type RequestBody struct {
|
|||
X509Certificate *X509Certificate `json:"x509Certificate,omitempty"`
|
||||
SSHCertificateRequest *SSHCertificateRequest `json:"sshCertificateRequest,omitempty"`
|
||||
SSHCertificate *SSHCertificate `json:"sshCertificate,omitempty"`
|
||||
// Only set for SCEP challenge validation requests
|
||||
SCEPChallenge string `json:"scepChallenge,omitempty"`
|
||||
SCEPTransactionID string `json:"scepTransactionID,omitempty"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue