Compare commits

..

6 commits

Author SHA1 Message Date
Herman Slatman
fe7db2ae02
Fix shadowed variable 2022-09-21 14:41:19 +02:00
Herman Slatman
711af6d0d6
Fix linting issue 2022-09-21 14:35:51 +02:00
Herman Slatman
ce3215c702
Merge branch 'master' into herman/acme-da-roots 2022-09-21 14:27:57 +02:00
Herman Slatman
8b44c0924b
Merge branch 'master' into herman/acme-da-roots 2022-09-21 12:53:20 +02:00
Herman Slatman
bffb8542d5
Merge branch 'master' into herman/acme-da-roots 2022-09-08 23:10:37 +02:00
Herman Slatman
acdfdf3451
Add tpm attestation with configurable roots 2022-09-02 19:01:50 +02:00
267 changed files with 9195 additions and 16712 deletions

View file

@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

4
.github/labeler.yml vendored Normal file
View file

@ -0,0 +1,4 @@
needs triage:
- '**' # index.php | src/main.php
- '.*' # .gitignore
- '.*/**' # .github/workflows/label.yml

View file

@ -1,27 +0,0 @@
name: CI
on:
push:
tags-ignore:
- 'v*'
branches:
- "master"
pull_request:
workflow_call:
secrets:
CODECOV_TOKEN:
required: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
ci:
uses: smallstep/workflows/.github/workflows/goCI.yml@main
with:
only-latest-golang: false
os-dependencies: 'libpcsclite-dev'
run-codeql: true
test-command: 'V=1 make test'
secrets: inherit

View file

@ -1,9 +0,0 @@
on:
schedule:
- cron: '0 0 * * *'
jobs:
code-scan:
uses: smallstep/workflows/.github/workflows/code-scan.yml@main
secrets:
GITLEAKS_LICENSE_KEY: ${{ secrets.GITLEAKS_LICENSE_KEY }}

72
.github/workflows/codeql-analysis.yml vendored Normal file
View file

@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '30 3 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View file

@ -1,22 +0,0 @@
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}}

View file

@ -7,43 +7,63 @@ on:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
jobs: jobs:
ci: test:
uses: smallstep/certificates/.github/workflows/ci.yml@master name: Lint, Test, Build
secrets: inherit runs-on: ubuntu-20.04
strategy:
matrix:
go: [ '1.18', '1.19' ]
outputs:
is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
-
name: Install Deps
id: install-deps
run: sudo apt-get -y install libpcsclite-dev
-
name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: ${{ secrets.GOLANGCI_LINT_VERSION }}
args: --timeout=30m
-
name: Test, Build
id: lint_test_build
run: V=1 make ci
create_release: create_release:
name: Create Release name: Create Release
needs: ci needs: test
runs-on: ubuntu-latest runs-on: ubuntu-20.04
env:
DOCKER_IMAGE: smallstep/step-ca
outputs: outputs:
version: ${{ steps.extract-tag.outputs.VERSION }} debversion: ${{ steps.extract-tag.outputs.DEB_VERSION }}
is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
docker_tags: ${{ env.DOCKER_TAGS }}
docker_tags_hsm: ${{ env.DOCKER_TAGS_HSM }}
steps: steps:
- name: Is Pre-release -
name: Extract Tag Names
id: extract-tag
run: |
DEB_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | sed 's/-/./')
echo "::set-output name=DEB_VERSION::${DEB_VERSION}"
-
name: Is Pre-release
id: is_prerelease id: is_prerelease
run: | run: |
set +e set +e
echo ${{ github.ref }} | grep "\-rc.*" echo ${{ github.ref }} | grep "\-rc.*"
OUT=$? OUT=$?
if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi
echo "IS_PRERELEASE=${IS_PRERELEASE}" >> ${GITHUB_OUTPUT} echo "::set-output name=IS_PRERELEASE::${IS_PRERELEASE}"
- name: Extract Tag Names -
id: extract-tag name: Create Release
run: |
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 id: create_release
uses: actions/create-release@v1 uses: actions/create-release@v1
env: env:
@ -55,37 +75,89 @@ jobs:
prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }} prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
goreleaser: goreleaser:
name: Upload Assets To Github w/ goreleaser
runs-on: ubuntu-20.04
needs: create_release needs: create_release
permissions: steps:
id-token: write -
contents: write name: Checkout
uses: smallstep/workflows/.github/workflows/goreleaser.yml@main uses: actions/checkout@v2
secrets: inherit with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
-
name: APT Install
id: aptInstall
run: sudo apt-get -y install build-essential debhelper fakeroot
-
name: Build Debian package
id: make_debian
run: |
PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin
make debian
# need to restore the git state otherwise goreleaser fails due to dirty state
git restore debian/changelog
git clean -fd
-
name: Install cosign
uses: sigstore/cosign-installer@v1.1.0
with:
cosign-release: 'v1.1.0'
-
name: Write cosign key to disk
id: write_key
run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key"
-
name: Get Release Date
id: release_date
run: |
RELEASE_DATE=$(date +"%y-%m-%d")
echo "::set-output name=RELEASE_DATE::${RELEASE_DATE}"
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@5a54d7e660bda43b405e8463261b3d25631ffe86 # v2.7.0
with:
version: 'v1.7.0'
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
DEB_VERSION: ${{ needs.create_release.outputs.debversion }}
RELEASE_DATE: ${{ steps.release_date.outputs.RELEASE_DATE }}
build_upload_docker: build_upload_docker:
name: Build & Upload Docker Images name: Build & Upload Docker Images
needs: create_release runs-on: ubuntu-20.04
permissions: needs: test
id-token: write steps:
contents: write -
uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main name: Checkout
with: uses: actions/checkout@v2
platforms: linux/amd64,linux/386,linux/arm,linux/arm64 -
tags: ${{ needs.create_release.outputs.docker_tags }} name: Setup Go
docker_image: smallstep/step-ca uses: actions/setup-go@v2
docker_file: docker/Dockerfile with:
secrets: inherit go-version: '1.19'
-
build_upload_docker_hsm: name: Install cosign
name: Build & Upload HSM Enabled Docker Images uses: sigstore/cosign-installer@v1.1.0
needs: create_release with:
permissions: cosign-release: 'v1.1.0'
id-token: write -
contents: write name: Write cosign key to disk
uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main id: write_key
with: run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key"
platforms: linux/amd64,linux/386,linux/arm,linux/arm64 -
tags: ${{ needs.create_release.outputs.docker_tags_hsm }} name: Build
docker_image: smallstep/step-ca id: build
docker_file: docker/Dockerfile.hsm run: |
secrets: inherit PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin
make docker-artifacts
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}

49
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,49 @@
name: Lint, Test, Build
on:
push:
tags-ignore:
- 'v*'
branches:
- "**"
pull_request:
jobs:
lintTestBuild:
name: Lint, Test, Build
runs-on: ubuntu-20.04
strategy:
matrix:
go: [ '1.18', '1.19' ]
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
-
name: Install Deps
id: install-deps
run: sudo apt-get -y install libpcsclite-dev
-
name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: ${{ secrets.GOLANGCI_LINT_VERSION }}
args: --timeout=30m
-
name: Test, Build
id: lint_test_build
run: V=1 make ci
-
name: Codecov
if: matrix.go == '1.19'
uses: codecov/codecov-action@v2
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.out # optional
name: codecov-umbrella # optional
fail_ci_if_error: true # optional (default = false)

View file

@ -4,13 +4,26 @@ on:
issues: issues:
types: types:
- opened - opened
- reopened
pull_request_target: pull_request_target:
types: types:
- opened - opened
- reopened
jobs: jobs:
triage:
uses: smallstep/workflows/.github/workflows/triage.yml@main label:
secrets: inherit name: Label PR
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target'
steps:
- uses: actions/labeler@v3.0.2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
add-to-project:
name: Add to Triage Project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.3.0
with:
project-url: https://github.com/orgs/smallstep/projects/94
github-token: ${{ secrets.TRIAGE_PAT }}

1
.gitignore vendored
View file

@ -24,4 +24,3 @@ output
vendor vendor
.idea .idea
.envrc .envrc
.vscode

View file

@ -1,18 +0,0 @@
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:85
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:107
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:108
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:129
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:131
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:136
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:138
7c9ab9814fb676cb3c125c3dac4893271f1b7ae5:README.md:generic-api-key:282
fb7140444ac8f1fa1245a80e49d17e206f7435f3:docs/provisioners.md:generic-api-key:110
e4de7f07e82118b3f926716666b620db058fa9f7:docs/revocation.md:generic-api-key:73
e4de7f07e82118b3f926716666b620db058fa9f7:docs/revocation.md:generic-api-key:113
e4de7f07e82118b3f926716666b620db058fa9f7:docs/revocation.md:generic-api-key:151
8b2de42e9cf6ce99f53a5049881e1d6077d5d66e:docs/docker.md:generic-api-key:152
3939e855264117e81531df777a642ea953d325a7:autocert/init/ca/intermediate_ca_key:private-key:1
e72f08703753facfa05f2d8c68f9f6a3745824b8:README.md:generic-api-key:244
e70a5dae7de0b6ca40a0393c09c28872d4cfa071:autocert/README.md:generic-api-key:365
e70a5dae7de0b6ca40a0393c09c28872d4cfa071:autocert/README.md:generic-api-key:366
c284a2c0ab1c571a46443104be38c873ef0c7c6d:config.json:generic-api-key:10

74
.golangci.yml Normal file
View file

@ -0,0 +1,74 @@
linters-settings:
govet:
check-shadowing: true
settings:
printf:
funcs:
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
revive:
min-confidence: 0
gocyclo:
min-complexity: 10
maligned:
suggest-new: true
dupl:
threshold: 100
goconst:
min-len: 2
min-occurrences: 2
depguard:
list-type: blacklist
packages:
# logging is allowed only by logutils.Log, logrus
# is allowed to use only in logutils package
- github.com/sirupsen/logrus
misspell:
locale: US
lll:
line-length: 140
goimports:
local-prefixes: github.com/golangci/golangci-lint
gocritic:
enabled-tags:
- performance
- style
- experimental
- diagnostic
disabled-checks:
- commentFormatting
- commentedOutCode
- evalOrder
- hugeParam
- octalLiteral
- rangeValCopy
- tooManyResultsChecker
- unnamedResult
linters:
disable-all: true
enable:
- gocritic
- gofmt
- gosimple
- govet
- ineffassign
- misspell
- revive
- staticcheck
- unused
run:
skip-dirs:
- pkg
issues:
exclude:
- can't lint
- declaration of "err" shadows declaration at line
- should have a package comment, unless it's in another file for this package
- error strings should not be capitalized or end with punctuation or a newline
- Wrapf call needs 1 arg but has 2 args
- cs.NegotiatedProtocolIsMutual is deprecated

View file

@ -26,17 +26,58 @@ builds:
flags: flags:
- -trimpath - -trimpath
main: ./cmd/step-ca/main.go main: ./cmd/step-ca/main.go
binary: step-ca binary: bin/step-ca
ldflags:
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
-
id: step-cloudkms-init
env:
- CGO_ENABLED=0
targets:
- darwin_amd64
- darwin_arm64
- freebsd_amd64
- linux_386
- linux_amd64
- linux_arm64
- linux_arm_5
- linux_arm_6
- linux_arm_7
- windows_amd64
flags:
- -trimpath
main: ./cmd/step-cloudkms-init/main.go
binary: bin/step-cloudkms-init
ldflags:
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
-
id: step-awskms-init
env:
- CGO_ENABLED=0
targets:
- darwin_amd64
- darwin_arm64
- freebsd_amd64
- linux_386
- linux_amd64
- linux_arm64
- linux_arm_5
- linux_arm_6
- linux_arm_7
- windows_amd64
flags:
- -trimpath
main: ./cmd/step-awskms-init/main.go
binary: bin/step-awskms-init
ldflags: ldflags:
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}} - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
archives: archives:
- &ARCHIVE -
# Can be used to change the archive formats for specific GOOSs. # Can be used to change the archive formats for specific GOOSs.
# Most common use case is to archive as zip on Windows. # Most common use case is to archive as zip on Windows.
# Default is empty. # Default is empty.
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
rlcp: true
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip format: zip
@ -44,51 +85,9 @@ archives:
files: files:
- README.md - README.md
- LICENSE - LICENSE
allow_different_binary_count: true
-
<< : *ARCHIVE
id: unversioned
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
nfpms:
# Configure nFPM for .deb and .rpm releases
#
# See https://nfpm.goreleaser.com/configuration/
# and https://goreleaser.com/customization/nfpm/
#
# Useful tools for debugging .debs:
# List file contents: dpkg -c dist/step_...deb
# Package metadata: dpkg --info dist/step_....deb
#
- &NFPM
builds:
- step-ca
package_name: step-ca
file_name_template: "{{ .PackageName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
vendor: Smallstep Labs
homepage: https://github.com/smallstep/certificates
maintainer: Smallstep <techadmin@smallstep.com>
description: >
step-ca is an online certificate authority for secure, automated certificate management.
license: Apache 2.0
section: utils
formats:
- deb
- rpm
priority: optional
bindir: /usr/bin
contents:
- src: debian/copyright
dst: /usr/share/doc/step-ca/copyright
-
<< : *NFPM
id: unversioned
file_name_template: "{{ .PackageName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
source: source:
enabled: true enabled: true
rlcp: true
name_template: '{{ .ProjectName }}_{{ .Version }}' name_template: '{{ .ProjectName }}_{{ .Version }}'
checksum: checksum:
@ -98,9 +97,8 @@ checksum:
signs: signs:
- cmd: cosign - cmd: cosign
signature: "${artifact}.sig" stdin: '{{ .Env.COSIGN_PWD }}'
certificate: "${artifact}.pem" args: ["sign-blob", "-key=/tmp/cosign.key", "-output=${signature}", "${artifact}"]
args: ["sign-blob", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"]
artifacts: all artifacts: all
snapshot: snapshot:
@ -141,17 +139,17 @@ release:
#### Linux #### Linux
- 📦 [step-ca_linux_{{ .Version }}_amd64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_linux_{{ .Version }}_amd64.tar.gz) - 📦 [step-ca_linux_{{ .Version }}_amd64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_linux_{{ .Version }}_amd64.tar.gz)
- 📦 [step-ca_{{ .Version }}_amd64.deb](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ .Version }}_amd64.deb) - 📦 [step-ca_{{ .Env.DEB_VERSION }}_amd64.deb](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ .Env.DEB_VERSION }}_amd64.deb)
#### OSX Darwin #### OSX Darwin
- 📦 [step-ca_darwin_{{ .Version }}_amd64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_amd64.tar.gz) - 📦 [step-ca_darwin_{{ .Version }}_amd64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_amd64.tar.gz)
- 📦 [step-ca_darwin_{{ .Version }}_arm64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_arm64.tar.gz) - 📦 [step-ca_darwin_{{ .Version }}_arm64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_arm64.tar.gz)
#### Windows #### Windows
- 📦 [step-ca_windows_{{ .Version }}_amd64.zip](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_windows_{{ .Version }}_amd64.zip) - 📦 [step-ca_windows_{{ .Version }}_arm64.zip](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_windows_{{ .Version }}_amd64.zip)
For more builds across platforms and architectures, see the `Assets` section below. 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). And for packaged versions (Docker, k8s, Homebrew), see our [installation docs](https://smallstep.com/docs/step-ca/installation).
@ -166,10 +164,8 @@ release:
``` ```
cosign verify-blob \ cosign verify-blob \
--certificate ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig.pem \ -key https://raw.githubusercontent.com/smallstep/certificates/master/cosign.pub \
--signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig \ -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 ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz
``` ```
@ -199,40 +195,38 @@ release:
# - glob: ./glob/**/to/**/file/**/* # - glob: ./glob/**/to/**/file/**/*
# - glob: ./glob/foo/to/bar/file/foobar/override_from_previous # - glob: ./glob/foo/to/bar/file/foobar/override_from_previous
scoops: scoop:
- # Template for the url which is determined by the given Token (github or gitlab)
ids: [ default ] # Default for github is "https://github.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
# Template for the url which is determined by the given Token (github or gitlab) # Default for gitlab is "https://gitlab.com/<repo_owner>/<repo_name>/uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}"
# Default for github is "https://github.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}" # Default for gitea is "https://gitea.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
# Default for gitlab is "https://gitlab.com/<repo_owner>/<repo_name>/uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}" url_template: "http://github.com/smallstep/certificates/releases/download/{{ .Tag }}/{{ .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. # Repository to push the app manifest to.
# Defaults are shown. bucket:
commit_author: owner: smallstep
name: goreleaserbot name: scoop-bucket
email: goreleaser@smallstep.com
# The project name and current git tag are used in the format string. # Git author used to commit to the repository.
commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}" # Defaults are shown.
commit_author:
name: goreleaserbot
email: goreleaser@smallstep.com
# Your app's homepage. # The project name and current git tag are used in the format string.
# Default is empty. commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}"
homepage: "https://smallstep.com/docs/step-ca"
# Skip uploads for prerelease. # Your app's homepage.
skip_upload: auto # Default is empty.
homepage: "https://smallstep.com/docs/step-ca"
# Your app's description. # Skip uploads for prerelease.
# Default is empty. skip_upload: auto
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 # Your app's description.
# Default is empty. # Default is empty.
license: "Apache-2.0" 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"

View file

@ -1,4 +1,4 @@
#!/usr/bin/env sh #!/usr/bin/env bash
read -r firstline < .VERSION read -r firstline < .VERSION
last_half="${firstline##*tag: }" last_half="${firstline##*tag: }"
if [[ ${last_half::1} == "v" ]]; then if [[ ${last_half::1} == "v" ]]; then

View file

@ -1,200 +1,33 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## TEMPLATE -- do not alter or remove ### TEMPLATE -- do not alter or remove
--- ---
## [x.y.z] - aaaa-bb-cc ## [x.y.z] - aaaa-bb-cc
### Added ### Added
### Changed ### Changed
### Deprecated ### Deprecated
### Removed ### Removed
### Fixed ### Fixed
### Security ### Security
--- ---
## [Unreleased] ## [Unreleased]
### Fixed
- Improved authentication for ACME requests using kid and provisioner name
(smallstep/certificates#1386).
## [v0.24.2] - 2023-05-11
### Added ### Added
- Added support for ACME device-attest-01 challenge.
- Log SSH certificates (smallstep/certificates#1374)
- CRL endpoints on the HTTP server (smallstep/certificates#1372)
- Dynamic SCEP challenge validation using webhooks (smallstep/certificates#1366)
- For Docker deployments, added DOCKER_STEPCA_INIT_PASSWORD_FILE. Useful for pointing to a Docker Secret in the container (smallstep/certificates#1384)
### Changed
- Depend on [smallstep/go-attestation](https://github.com/smallstep/go-attestation) instead of [google/go-attestation](https://github.com/google/go-attestation)
- Render CRLs into http.ResponseWriter instead of memory (smallstep/certificates#1373)
- Redaction of SCEP static challenge when listing provisioners (smallstep/certificates#1204)
### Fixed
- VaultCAS certificate lifetime (smallstep/certificates#1376)
## [v0.24.1] - 2023-04-14
### Fixed
- Docker image name for HSM support (smallstep/certificates#1348)
## [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 (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
### Added
- Added support for ACME device-attest-01 challenge on iOS, iPadOS, tvOS and
YubiKey.
- Ability to disable ACME challenges and attestation formats.
- Added flags to change ACME challenge ports for testing purposes.
- Added name constraints evaluation and enforcement when issuing or renewing
X.509 certificates.
- Added provisioner webhooks for augmenting template data and authorizing
certificate requests before signing.
- Added automatic migration of provisioners when enabling remote management.
- Added experimental support for CRLs.
- Add certificate renewal support on RA mode. The `step ca renew` command must
use the flag `--mtls=false` to use the token renewal flow.
- Added support for initializing remote management using `step ca init`.
- Added support for renewing X.509 certificates on RAs.
- Added support for using SCEP with keys in a KMS.
- Added client support to set the dialer's local address with the environment variable
`STEP_CLIENT_ADDR`.
### Changed
- Remove the email requirement for issuing SSH certificates with an OIDC
provisioner.
- Root files can contain more than one certificate.
### Fixed
- Fixed MySQL DSN parsing issues with an upgrade to
[smallstep/nosql@v0.5.0](https://github.com/smallstep/nosql/releases/tag/v0.5.0).
- Fixed renewal of certificates with missing subject attributes.
- Fixed ACME support with [ejabberd](https://github.com/processone/ejabberd).
### Deprecated
- The CLIs `step-awskms-init`, `step-cloudkms-init`, `step-pkcs11-init`,
`step-yubikey-init` are deprecated. Now you can use
[`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) in
combination with `step certificates create` to initialize your PKI.
## [0.22.1] - 2022-08-31 ## [0.22.1] - 2022-08-31
### Fixed ### Fixed
- Fixed signature algorithm on EC (root) + RSA (intermediate) PKIs. - Fixed signature algorithm on EC (root) + RSA (intermediate) PKIs.
## [0.22.0] - 2022-08-26 ## [0.22.0] - 2022-08-26
### Added ### Added
- Added automatic configuration of Linked RAs. - Added automatic configuration of Linked RAs.
- Send provisioner configuration on Linked RAs. - Send provisioner configuration on Linked RAs.
### Changed ### Changed
- Certificates signed by an issuer using an RSA key will be signed using the - Certificates signed by an issuer using an RSA key will be signed using the
same algorithm used to sign the issuer certificate. The signature will no same algorithm used to sign the issuer certificate. The signature will no
longer default to PKCS #1. For example, if the issuer certificate was signed longer default to PKCS #1. For example, if the issuer certificate was signed
@ -206,28 +39,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Sanitize TLS options. - Sanitize TLS options.
## [0.20.0] - 2022-05-26 ## [0.20.0] - 2022-05-26
### Added ### Added
- Added Kubernetes auth method for Vault RAs. - Added Kubernetes auth method for Vault RAs.
- Added support for reporting provisioners to linkedca. - Added support for reporting provisioners to linkedca.
- Added support for certificate policies on authority level. - Added support for certificate policies on authority level.
- Added a Dockerfile with a step-ca build with HSM support. - Added a Dockerfile with a step-ca build with HSM support.
- A few new WithXX methods for instantiating authorities - A few new WithXX methods for instantiating authorities
### Changed ### Changed
- Context usage in HTTP APIs. - Context usage in HTTP APIs.
- Changed authentication for Vault RAs. - Changed authentication for Vault RAs.
- Error message returned to client when authenticating with expired certificate. - Error message returned to client when authenticating with expired certificate.
- Strip padding from ACME CSRs. - Strip padding from ACME CSRs.
### Deprecated ### Deprecated
- HTTP API handler types. - HTTP API handler types.
### Fixed ### Fixed
- Fixed SSH revocation. - Fixed SSH revocation.
- CA client dial context for js/wasm target. - CA client dial context for js/wasm target.
- Incomplete `extraNames` support in templates. - Incomplete `extraNames` support in templates.
@ -235,9 +60,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Large SCEP request handling. - Large SCEP request handling.
## [0.19.0] - 2022-04-19 ## [0.19.0] - 2022-04-19
### Added ### Added
- Added support for certificate renewals after expiry using the claim `allowRenewalAfterExpiry`. - Added support for certificate renewals after expiry using the claim `allowRenewalAfterExpiry`.
- Added support for `extraNames` in X.509 templates. - Added support for `extraNames` in X.509 templates.
- Added `armv5` builds. - Added `armv5` builds.
@ -252,156 +75,104 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
on startup, the configuration for the current context is used. on startup, the configuration for the current context is used.
- Added startup info logging and option to skip it (`--quiet`). - Added startup info logging and option to skip it (`--quiet`).
- Added support for renaming the CA (Common Name). - Added support for renaming the CA (Common Name).
### Changed ### Changed
- Made SCEP CA URL paths dynamic. - Made SCEP CA URL paths dynamic.
- Support two latest versions of Go (1.17, 1.18). - Support two latest versions of Go (1.17, 1.18).
- Upgrade go.step.sm/crypto to v0.16.1. - Upgrade go.step.sm/crypto to v0.16.1.
- Upgrade go.step.sm/linkedca to v0.15.0. - Upgrade go.step.sm/linkedca to v0.15.0.
### Deprecated ### Deprecated
- Go 1.16 support. - Go 1.16 support.
### Removed ### Removed
### Fixed ### Fixed
- Fixed admin credentials on RAs. - Fixed admin credentials on RAs.
- Fixed ACME HTTP-01 challenges for IPv6 identifiers. - Fixed ACME HTTP-01 challenges for IPv6 identifiers.
- Various improvements under the hood. - Various improvements under the hood.
### Security ### Security
## [0.18.2] - 2022-03-01 ## [0.18.2] - 2022-03-01
### Added ### Added
- Added `subscriptionIDs` and `objectIDs` filters to the Azure provisioner. - Added `subscriptionIDs` and `objectIDs` filters to the Azure provisioner.
- [NoSQL](https://github.com/smallstep/nosql/pull/21) package allows filtering - [NoSQL](https://github.com/smallstep/nosql/pull/21) package allows filtering
out database drivers using Go tags. For example, using the Go flag out database drivers using Go tags. For example, using the Go flag
`--tags=nobadger,nobbolt,nomysql` will only compile `step-ca` with the pgx `--tags=nobadger,nobbolt,nomysql` will only compile `step-ca` with the pgx
driver for PostgreSQL. driver for PostgreSQL.
### Changed ### Changed
- IPv6 addresses are normalized as IP addresses instead of hostnames. - IPv6 addresses are normalized as IP addresses instead of hostnames.
- More descriptive JWK decryption error message. - More descriptive JWK decryption error message.
- Make the X5C leaf certificate available to the templates using `{{ .AuthorizationCrt }}`. - Make the X5C leaf certificate available to the templates using `{{ .AuthorizationCrt }}`.
### Fixed ### Fixed
- During provisioner add - validate provisioner configuration before storing to DB. - During provisioner add - validate provisioner configuration before storing to DB.
## [0.18.1] - 2022-02-03 ## [0.18.1] - 2022-02-03
### Added ### Added
- Support for ACME revocation. - Support for ACME revocation.
- Replace hash function with an RSA SSH CA to "rsa-sha2-256". - Replace hash function with an RSA SSH CA to "rsa-sha2-256".
- Support Nebula provisioners. - Support Nebula provisioners.
- Example Ansible configurations. - Example Ansible configurations.
- Support PKCS#11 as a decrypter, as used by SCEP. - Support PKCS#11 as a decrypter, as used by SCEP.
### Changed ### Changed
- Automatically create database directory on `step ca init`. - Automatically create database directory on `step ca init`.
- Slightly improve errors reported when a template has invalid content. - Slightly improve errors reported when a template has invalid content.
- Error reporting in logs and to clients. - Error reporting in logs and to clients.
### Fixed ### Fixed
- SCEP renewal using HTTPS on macOS. - SCEP renewal using HTTPS on macOS.
## [0.18.0] - 2021-11-17 ## [0.18.0] - 2021-11-17
### Added ### Added
- Support for multiple certificate authority contexts. - Support for multiple certificate authority contexts.
- Support for generating extractable keys and certificates on a pkcs#11 module. - Support for generating extractable keys and certificates on a pkcs#11 module.
### Changed ### Changed
- Support two latest versions of Go (1.16, 1.17) - Support two latest versions of Go (1.16, 1.17)
### Deprecated ### Deprecated
- go 1.15 support - go 1.15 support
## [0.17.6] - 2021-10-20 ## [0.17.6] - 2021-10-20
### Notes ### Notes
- 0.17.5 failed in CI/CD - 0.17.5 failed in CI/CD
## [0.17.5] - 2021-10-20 ## [0.17.5] - 2021-10-20
### Added ### Added
- Support for Azure Key Vault as a KMS. - Support for Azure Key Vault as a KMS.
- Adapt `pki` package to support key managers. - Adapt `pki` package to support key managers.
- gocritic linter - gocritic linter
### Fixed ### Fixed
- gocritic warnings - gocritic warnings
## [0.17.4] - 2021-09-28 ## [0.17.4] - 2021-09-28
### Fixed ### Fixed
- Support host-only or user-only SSH CA. - Support host-only or user-only SSH CA.
## [0.17.3] - 2021-09-24 ## [0.17.3] - 2021-09-24
### Added ### Added
- go 1.17 to github action test matrix - go 1.17 to github action test matrix
- Support for CloudKMS RSA-PSS signers without using templates. - Support for CloudKMS RSA-PSS signers without using templates.
- Add flags to support individual passwords for the intermediate and SSH keys. - Add flags to support individual passwords for the intermediate and SSH keys.
- Global support for group admins in the OIDC provisioner. - Global support for group admins in the OIDC provisioner.
### Changed ### Changed
- Using go 1.17 for binaries - Using go 1.17 for binaries
### Fixed ### Fixed
- Upgrade go-jose.v2 to fix a bug in the JWK fingerprint of Ed25519 keys. - Upgrade go-jose.v2 to fix a bug in the JWK fingerprint of Ed25519 keys.
### Security ### Security
- Use cosign to sign and upload signatures for multi-arch Docker container. - Use cosign to sign and upload signatures for multi-arch Docker container.
- Add debian checksum - Add debian checksum
## [0.17.2] - 2021-08-30 ## [0.17.2] - 2021-08-30
### Added ### Added
- Additional way to distinguish Azure IID and Azure OIDC tokens. - Additional way to distinguish Azure IID and Azure OIDC tokens.
### Security ### Security
- Sign over all goreleaser github artifacts using cosign - Sign over all goreleaser github artifacts using cosign
## [0.17.1] - 2021-08-26 ## [0.17.1] - 2021-08-26
## [0.17.0] - 2021-08-25 ## [0.17.0] - 2021-08-25
### Added ### Added
- Add support for Linked CAs using protocol buffers and gRPC - Add support for Linked CAs using protocol buffers and gRPC
- `step-ca init` adds support for - `step-ca init` adds support for
- configuring a StepCAS RA - configuring a StepCAS RA
- configuring a Linked CA - configuring a Linked CA
- congifuring a `step-ca` using Helm - congifuring a `step-ca` using Helm
### Changed ### Changed
- Update badger driver to use v2 by default - Update badger driver to use v2 by default
- Update TLS cipher suites to include 1.3 - Update TLS cipher suites to include 1.3
### Security ### Security
- Fix key version when SHA512WithRSA is used. There was a typo creating RSA keys with SHA256 digests instead of SHA512. - Fix key version when SHA512WithRSA is used. There was a typo creating RSA keys with SHA256 digests instead of SHA512.

150
Makefile
View file

@ -1,11 +1,21 @@
PKG?=github.com/smallstep/certificates/cmd/step-ca PKG?=github.com/smallstep/certificates/cmd/step-ca
BINNAME?=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 # Set V to 1 for verbose output from the Makefile
Q=$(if $V,,@) Q=$(if $V,,@)
PREFIX?= PREFIX?=
SRC=$(shell find . -type f -name '*.go' -not -path "./vendor/*") SRC=$(shell find . -type f -name '*.go' -not -path "./vendor/*")
GOOS_OVERRIDE ?= GOOS_OVERRIDE ?=
OUTPUT_ROOT=output/
RELEASE=./.releases
all: lint test build all: lint test build
@ -18,11 +28,8 @@ ci: testcgo build
######################################### #########################################
bootstra%: bootstra%:
$Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin latest # Using a released version of golangci-lint to take into account custom replacements in their go.mod
$Q go install golang.org/x/vuln/cmd/govulncheck@latest $Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.42.0
$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% .PHONY: bootstra%
@ -30,8 +37,17 @@ bootstra%:
# Determine the type of `push` and `version` # 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 # GITHUB Actions
ifdef GITHUB_REF else ifdef GITHUB_REF
VERSION ?= $(shell echo $(GITHUB_REF) | sed 's/^refs\/tags\///') VERSION ?= $(shell echo $(GITHUB_REF) | sed 's/^refs\/tags\///')
NOT_RC := $(shell echo $(VERSION) | grep -v -e -rc) NOT_RC := $(shell echo $(VERSION) | grep -v -e -rc)
ifeq ($(NOT_RC),) ifeq ($(NOT_RC),)
@ -44,50 +60,59 @@ 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. # 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 contains a slug populated by `git archive`.
VERSION := $(or $(VERSION),$(shell ./.version.sh .VERSION)) VERSION := $(or $(VERSION),$(shell ./.version.sh .VERSION))
ifeq ($(TRAVIS_BRANCH),master)
PUSHTYPE := master
else
PUSHTYPE := branch PUSHTYPE := branch
endif
endif endif
VERSION := $(shell echo $(VERSION) | sed 's/^v//') VERSION := $(shell echo $(VERSION) | sed 's/^v//')
DEB_VERSION := $(shell echo $(VERSION) | sed 's/-/./g')
ifdef V ifdef V
$(info TRAVIS_TAG is $(TRAVIS_TAG))
$(info GITHUB_REF is $(GITHUB_REF)) $(info GITHUB_REF is $(GITHUB_REF))
$(info VERSION is $(VERSION)) $(info VERSION is $(VERSION))
$(info DEB_VERSION is $(DEB_VERSION))
$(info PUSHTYPE is $(PUSHTYPE)) $(info PUSHTYPE is $(PUSHTYPE))
endif endif
include make/docker.mk
######################################### #########################################
# Build # Build
######################################### #########################################
DATE := $(shell date -u '+%Y-%m-%d %H:%M UTC') DATE := $(shell date -u '+%Y-%m-%d %H:%M UTC')
LDFLAGS := -ldflags='-w -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"' LDFLAGS := -ldflags='-w -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
GOFLAGS := CGO_ENABLED=0
# Always explicitly enable or disable cgo,
# so that go doesn't silently fall back on
# non-cgo when gcc is not found.
ifeq (,$(findstring CGO_ENABLED,$(GO_ENVS)))
ifneq ($(origin GOFLAGS),undefined)
# This section is for backward compatibility with
#
# $ make build GOFLAGS=""
#
# which is how we recommended building step-ca with cgo support
# until June 2023.
GO_ENVS := $(GO_ENVS) CGO_ENABLED=1
else
GO_ENVS := $(GO_ENVS) CGO_ENABLED=0
endif
endif
download: download:
$Q go mod download $Q go mod download
build: $(PREFIX)bin/$(BINNAME) build: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(AWSKMS_BINNAME) $(PREFIX)bin/$(YUBIKEY_BINNAME) $(PREFIX)bin/$(PKCS11_BINNAME)
@echo "Build Complete!" @echo "Build Complete!"
$(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go) $(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go)
$Q mkdir -p $(@D) $Q mkdir -p $(@D)
$Q $(GOOS_OVERRIDE) GOFLAGS="$(GOFLAGS)" $(GO_ENVS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG) $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 # Target to force a build of step-ca without running tests
simple: build simple: build
@ -106,26 +131,18 @@ generate:
######################################### #########################################
# Test # Test
######################################### #########################################
test: testdefault testtpmsimulator combinecoverage test:
$Q $(GOFLAGS) go test -short -coverprofile=coverage.out ./...
testdefault:
$Q $(GO_ENVS) gotestsum -- -coverprofile=defaultcoverage.out -short -covermode=atomic ./...
testtpmsimulator:
$Q CGO_ENABLED=1 gotestsum -- -coverprofile=tpmsimulatorcoverage.out -short -covermode=atomic -tags tpmsimulator ./acme
testcgo: testcgo:
$Q gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./... $Q go test -short -coverprofile=coverage.out ./...
combinecoverage: .PHONY: test testcgo
cat defaultcoverage.out tpmsimulatorcoverage.out > coverage.out
.PHONY: test testdefault testtpmsimulator testcgo combinecoverage
integrate: integration integrate: integration
integration: bin/$(BINNAME) integration: bin/$(BINNAME)
$Q $(GO_ENVS) gotestsum -- -tags=integration ./integration/... $Q $(GOFLAGS) go test -tags=integration ./integration/...
.PHONY: integrate integration .PHONY: integrate integration
@ -134,14 +151,15 @@ integration: bin/$(BINNAME)
######################################### #########################################
fmt: fmt:
$Q goimports -l -w $(SRC) $Q gofmt -l -s -w $(SRC)
lint: SHELL:=/bin/bash
lint: lint:
$Q LOG_LEVEL=error golangci-lint run --config <(curl -s https://raw.githubusercontent.com/smallstep/workflows/master/.golangci.yml) --timeout=30m $Q golangci-lint run --timeout=30m
$Q govulncheck ./...
.PHONY: fmt lint lintcgo:
$Q LOG_LEVEL=error golangci-lint run --timeout=30m
.PHONY: fmt lint lintcgo
######################################### #########################################
# Install # Install
@ -149,11 +167,15 @@ lint:
INSTALL_PREFIX?=/usr/ INSTALL_PREFIX?=/usr/
install: $(PREFIX)bin/$(BINNAME) install: $(PREFIX)bin/$(BINNAME) $(PREFIX)bin/$(CLOUDKMS_BINNAME) $(PREFIX)bin/$(AWSKMS_BINNAME)
$Q install -D $(PREFIX)bin/$(BINNAME) $(DESTDIR)$(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: uninstall:
$Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BINNAME) $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 .PHONY: install uninstall
@ -165,6 +187,18 @@ clean:
ifneq ($(BINNAME),"") ifneq ($(BINNAME),"")
$Q rm -f bin/$(BINNAME) $Q rm -f bin/$(BINNAME)
endif 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 .PHONY: clean
@ -177,3 +211,31 @@ run:
.PHONY: 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
#################################################
# Targets for creating step artifacts
#################################################
docker-artifacts: docker-$(PUSHTYPE)
.PHONY: docker-artifacts

View file

@ -119,12 +119,18 @@ See our installation docs [here](https://smallstep.com/docs/step-ca/installation
## Documentation ## Documentation
* [Official documentation](https://smallstep.com/docs/step-ca) is on smallstep.com Documentation can be found in a handful of different places:
* The `step` command reference is available via `step help`,
[on smallstep.com](https://smallstep.com/docs/step-cli/reference/), 1. On the web at https://smallstep.com/docs/step-ca.
or by running `step help --http=:8080` from the command line
2. On the command line with `step help ca xxx` where `xxx` is the subcommand
you are interested in. Ex: `step help ca provisioner list`.
3. In your browser, by running `step help --http=:8080 ca` from the command line
and visiting http://localhost:8080. and visiting http://localhost:8080.
4. The [docs](./docs/README.md) folder is being deprecated, but it still has some documentation and tutorials.
## Feedback? ## Feedback?
* Tell us what you like and don't like about managing your PKI - we're eager to help solve problems in this space. * Tell us what you like and don't like about managing your PKI - we're eager to help solve problems in this space.

View file

@ -20,16 +20,6 @@ type Account struct {
Status Status `json:"status"` Status Status `json:"status"`
OrdersURL string `json:"orders"` OrdersURL string `json:"orders"`
ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"` ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"`
LocationPrefix string `json:"-"`
ProvisionerName string `json:"-"`
}
// GetLocation returns the URL location of the given account.
func (a *Account) GetLocation() string {
if a.LocationPrefix == "" {
return ""
}
return a.LocationPrefix + a.ID
} }
// ToLog enables response logging. // ToLog enables response logging.
@ -43,7 +33,7 @@ func (a *Account) ToLog() (interface{}, error) {
// IsValid returns true if the Account is valid. // IsValid returns true if the Account is valid.
func (a *Account) IsValid() bool { func (a *Account) IsValid() bool {
return a.Status == StatusValid return Status(a.Status) == StatusValid
} }
// KeyToID converts a JWK to a thumbprint. // KeyToID converts a JWK to a thumbprint.
@ -82,7 +72,6 @@ func (p *Policy) GetAllowedNameOptions() *policy.X509NameOptions {
IPRanges: p.X509.Allowed.IPRanges, IPRanges: p.X509.Allowed.IPRanges,
} }
} }
func (p *Policy) GetDeniedNameOptions() *policy.X509NameOptions { func (p *Policy) GetDeniedNameOptions() *policy.X509NameOptions {
if p == nil { if p == nil {
return nil return nil

View file

@ -46,14 +46,14 @@ func TestKeyToID(t *testing.T) {
tc := run(t) tc := run(t)
if id, err := KeyToID(tc.jwk); err != nil { if id, err := KeyToID(tc.jwk); err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var k *Error switch k := err.(type) {
if errors.As(err, &k) { case *Error:
assert.Equals(t, k.Type, tc.err.Type) assert.Equals(t, k.Type, tc.err.Type)
assert.Equals(t, k.Detail, tc.err.Detail) assert.Equals(t, k.Detail, tc.err.Detail)
assert.Equals(t, k.Status, tc.err.Status) assert.Equals(t, k.Status, tc.err.Status)
assert.Equals(t, k.Err.Error(), tc.err.Err.Error()) assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
assert.Equals(t, k.Detail, tc.err.Detail) assert.Equals(t, k.Detail, tc.err.Detail)
} else { default:
assert.FatalError(t, errors.New("unexpected error type")) assert.FatalError(t, errors.New("unexpected error type"))
} }
} }
@ -66,23 +66,6 @@ func TestKeyToID(t *testing.T) {
} }
} }
func TestAccount_GetLocation(t *testing.T) {
locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/"
type test struct {
acc *Account
exp string
}
tests := map[string]test{
"empty": {acc: &Account{LocationPrefix: ""}, exp: ""},
"not-empty": {acc: &Account{ID: "bar", LocationPrefix: locationPrefix}, exp: locationPrefix + "bar"},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
assert.Equals(t, tc.acc.GetLocation(), tc.exp)
})
}
}
func TestAccount_IsValid(t *testing.T) { func TestAccount_IsValid(t *testing.T) {
type test struct { type test struct {
acc *Account acc *Account
@ -148,12 +131,12 @@ func TestExternalAccountKey_BindTo(t *testing.T) {
} }
if wantErr { if wantErr {
assert.NotNil(t, err) assert.NotNil(t, err)
var ae *Error assert.Type(t, &Error{}, err)
if assert.True(t, errors.As(err, &ae)) { ae, _ := err.(*Error)
assert.Equals(t, ae.Type, tt.err.Type) assert.Equals(t, ae.Type, tt.err.Type)
assert.Equals(t, ae.Detail, tt.err.Detail) assert.Equals(t, ae.Detail, tt.err.Detail)
assert.Equals(t, ae.Subproblems, tt.err.Subproblems) assert.Equals(t, ae.Identifier, tt.err.Identifier)
} assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
} else { } else {
assert.Equals(t, eak.AccountID, acct.ID) assert.Equals(t, eak.AccountID, acct.ID)
assert.Equals(t, eak.HmacKey, []byte{}) assert.Equals(t, eak.HmacKey, []byte{})

View file

@ -1,9 +1,7 @@
package api package api
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"github.com/go-chi/chi" "github.com/go-chi/chi"
@ -68,12 +66,6 @@ func (u *UpdateAccountRequest) Validate() error {
} }
} }
// getAccountLocationPath returns the current account URL location.
// Returned location will be of the form: https://<ca-url>/acme/<provisioner>/account/<accID>
func getAccountLocationPath(ctx context.Context, linker acme.Linker, accID string) string {
return linker.GetLink(ctx, acme.AccountLinkType, accID)
}
// NewAccount is the handler resource for creating new ACME accounts. // NewAccount is the handler resource for creating new ACME accounts.
func NewAccount(w http.ResponseWriter, r *http.Request) { func NewAccount(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -105,8 +97,8 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
httpStatus := http.StatusCreated httpStatus := http.StatusCreated
acc, err := accountFromContext(ctx) acc, err := accountFromContext(ctx)
if err != nil { if err != nil {
var acmeErr *acme.Error acmeErr, ok := err.(*acme.Error)
if !errors.As(err, &acmeErr) || acmeErr.Status != http.StatusBadRequest { if !ok || acmeErr.Status != http.StatusBadRequest {
// Something went wrong ... // Something went wrong ...
render.Error(w, err) render.Error(w, err)
return return
@ -132,11 +124,9 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
} }
acc = &acme.Account{ acc = &acme.Account{
Key: jwk, Key: jwk,
Contact: nar.Contact, Contact: nar.Contact,
Status: acme.StatusValid, Status: acme.StatusValid,
LocationPrefix: getAccountLocationPath(ctx, linker, ""),
ProvisionerName: prov.GetName(),
} }
if err := db.CreateAccount(ctx, acc); err != nil { if err := db.CreateAccount(ctx, acc); err != nil {
render.Error(w, acme.WrapErrorISE(err, "error creating account")) render.Error(w, acme.WrapErrorISE(err, "error creating account"))
@ -161,7 +151,7 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
linker.LinkAccount(ctx, acc) linker.LinkAccount(ctx, acc)
w.Header().Set("Location", getAccountLocationPath(ctx, linker, acc.ID)) w.Header().Set("Location", linker.GetLink(r.Context(), acme.AccountLinkType, acc.ID))
render.JSONStatus(w, acc, httpStatus) render.JSONStatus(w, acc, httpStatus)
} }

View file

@ -34,24 +34,31 @@ var (
type fakeProvisioner struct{} type fakeProvisioner struct{}
func (*fakeProvisioner) AuthorizeOrderIdentifier(context.Context, provisioner.ACMEIdentifier) error { func (*fakeProvisioner) AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error {
return nil return nil
} }
func (*fakeProvisioner) AuthorizeSign(context.Context, string) ([]provisioner.SignOption, error) {
func (*fakeProvisioner) AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) {
return nil, nil return nil, nil
} }
func (*fakeProvisioner) IsChallengeEnabled(context.Context, provisioner.ACMEChallenge) bool {
func (*fakeProvisioner) IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool {
return true return true
} }
func (*fakeProvisioner) IsAttestationFormatEnabled(context.Context, provisioner.ACMEAttestationFormat) bool {
func (*fakeProvisioner) IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool {
return true return true
} }
func (*fakeProvisioner) GetAttestationRoots() (*x509.CertPool, bool) { return nil, false }
func (*fakeProvisioner) AuthorizeRevoke(context.Context, string) error { return nil } func (*fakeProvisioner) GetAttestationRoots() (*x509.CertPool, bool) {
func (*fakeProvisioner) GetID() string { return "" } return nil, false
func (*fakeProvisioner) GetName() string { return "" } }
func (*fakeProvisioner) DefaultTLSCertDuration() time.Duration { return 0 }
func (*fakeProvisioner) GetOptions() *provisioner.Options { return nil } func (*fakeProvisioner) AuthorizeRevoke(ctx context.Context, token string) error { return nil }
func (*fakeProvisioner) GetID() string { return "" }
func (*fakeProvisioner) GetName() string { return "" }
func (*fakeProvisioner) DefaultTLSCertDuration() time.Duration { return 0 }
func (*fakeProvisioner) GetOptions() *provisioner.Options { return nil }
func newProv() acme.Provisioner { func newProv() acme.Provisioner {
// Initialize provisioners // Initialize provisioners
@ -190,12 +197,11 @@ func TestNewAccountRequest_Validate(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
if err := tc.nar.Validate(); err != nil { if err := tc.nar.Validate(); err != nil {
if assert.NotNil(t, err) { if assert.NotNil(t, err) {
var ae *acme.Error ae, ok := err.(*acme.Error)
if assert.True(t, errors.As(err, &ae)) { assert.True(t, ok)
assert.HasPrefix(t, ae.Error(), tc.err.Error()) assert.HasPrefix(t, ae.Error(), tc.err.Error())
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode()) assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
}
} }
} else { } else {
assert.Nil(t, tc.err) assert.Nil(t, tc.err)
@ -262,12 +268,11 @@ func TestUpdateAccountRequest_Validate(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
if err := tc.uar.Validate(); err != nil { if err := tc.uar.Validate(); err != nil {
if assert.NotNil(t, err) { if assert.NotNil(t, err) {
var ae *acme.Error ae, ok := err.(*acme.Error)
if assert.True(t, errors.As(err, &ae)) { assert.True(t, ok)
assert.HasPrefix(t, ae.Error(), tc.err.Error()) assert.HasPrefix(t, ae.Error(), tc.err.Error())
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode()) assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
}
} }
} else { } else {
assert.Nil(t, tc.err) assert.Nil(t, tc.err)
@ -362,7 +367,7 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) {
for name, run := range tests { for name, run := range tests {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil, "") ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
req := httptest.NewRequest("GET", u, nil) req := httptest.NewRequest("GET", u, nil)
req = req.WithContext(ctx) req = req.WithContext(ctx)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -381,6 +386,7 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -801,7 +807,7 @@ func TestHandler_NewAccount(t *testing.T) {
for name, run := range tests { for name, run := range tests {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil, "") ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
req := httptest.NewRequest("GET", "/foo/bar", nil) req := httptest.NewRequest("GET", "/foo/bar", nil)
req = req.WithContext(ctx) req = req.WithContext(ctx)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -820,6 +826,7 @@ func TestHandler_NewAccount(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -1004,7 +1011,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) {
for name, run := range tests { for name, run := range tests {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil, "") ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
req := httptest.NewRequest("GET", "/foo/bar", nil) req := httptest.NewRequest("GET", "/foo/bar", nil)
req = req.WithContext(ctx) req = req.WithContext(ctx)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -1023,6 +1030,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {

View file

@ -3,7 +3,6 @@ package api
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
@ -25,7 +24,6 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest)
} }
if !acmeProv.RequireEAB { if !acmeProv.RequireEAB {
//nolint:nilnil // legacy
return nil, nil return nil, nil
} }
@ -53,8 +51,7 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest)
db := acme.MustDatabaseFromContext(ctx) db := acme.MustDatabaseFromContext(ctx)
externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID) externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)
if err != nil { if err != nil {
var ae *acme.Error if _, ok := err.(*acme.Error); ok {
if errors.As(err, &ae) {
return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key") return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key")
} }
return nil, acme.WrapErrorISE(err, "error retrieving external account key") return nil, acme.WrapErrorISE(err, "error retrieving external account key")

View file

@ -860,14 +860,13 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
if wantErr { if wantErr {
assert.NotNil(t, err) assert.NotNil(t, err)
assert.Type(t, &acme.Error{}, err) assert.Type(t, &acme.Error{}, err)
var ae *acme.Error ae, _ := err.(*acme.Error)
if assert.True(t, errors.As(err, &ae)) { assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Status, tc.err.Status)
assert.Equals(t, ae.Status, tc.err.Status) assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error())
assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error()) assert.Equals(t, ae.Detail, tc.err.Detail)
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, ae.Subproblems, tc.err.Subproblems)
}
} else { } else {
if got == nil { if got == nil {
assert.Nil(t, tc.eak) assert.Nil(t, tc.eak)
@ -1144,6 +1143,7 @@ func Test_validateEABJWS(t *testing.T) {
assert.Equals(t, tc.err.Status, err.Status) assert.Equals(t, tc.err.Status, err.Status)
assert.HasPrefix(t, err.Err.Error(), tc.err.Err.Error()) assert.HasPrefix(t, err.Err.Error(), tc.err.Err.Error())
assert.Equals(t, tc.err.Detail, err.Detail) assert.Equals(t, tc.err.Detail, err.Detail)
assert.Equals(t, tc.err.Identifier, err.Identifier)
assert.Equals(t, tc.err.Subproblems, err.Subproblems) assert.Equals(t, tc.err.Subproblems, err.Subproblems)
} else { } else {
assert.Nil(t, err) assert.Nil(t, err)

View file

@ -95,7 +95,7 @@ func (h *handler) Route(r api.Router) {
if ca, ok := h.opts.CA.(*authority.Authority); ok && ca != nil { if ca, ok := h.opts.CA.(*authority.Authority); ok && ca != nil {
ctx = authority.NewContext(ctx, ca) ctx = authority.NewContext(ctx, ca)
} }
ctx = acme.NewContext(ctx, h.opts.DB, client, linker, h.opts.PrerequisitesChecker, "") ctx = acme.NewContext(ctx, h.opts.DB, client, linker, h.opts.PrerequisitesChecker)
next(w, r.WithContext(ctx)) next(w, r.WithContext(ctx))
} }
}) })
@ -205,7 +205,7 @@ type Directory struct {
NewOrder string `json:"newOrder"` NewOrder string `json:"newOrder"`
RevokeCert string `json:"revokeCert"` RevokeCert string `json:"revokeCert"`
KeyChange string `json:"keyChange"` KeyChange string `json:"keyChange"`
Meta *Meta `json:"meta,omitempty"` Meta Meta `json:"meta"`
} }
// ToLog enables response logging for the Directory type. // ToLog enables response logging for the Directory type.
@ -228,52 +228,21 @@ func GetDirectory(w http.ResponseWriter, r *http.Request) {
} }
linker := acme.MustLinkerFromContext(ctx) linker := acme.MustLinkerFromContext(ctx)
render.JSON(w, &Directory{ render.JSON(w, &Directory{
NewNonce: linker.GetLink(ctx, acme.NewNonceLinkType), NewNonce: linker.GetLink(ctx, acme.NewNonceLinkType),
NewAccount: linker.GetLink(ctx, acme.NewAccountLinkType), NewAccount: linker.GetLink(ctx, acme.NewAccountLinkType),
NewOrder: linker.GetLink(ctx, acme.NewOrderLinkType), NewOrder: linker.GetLink(ctx, acme.NewOrderLinkType),
RevokeCert: linker.GetLink(ctx, acme.RevokeCertLinkType), RevokeCert: linker.GetLink(ctx, acme.RevokeCertLinkType),
KeyChange: linker.GetLink(ctx, acme.KeyChangeLinkType), KeyChange: linker.GetLink(ctx, acme.KeyChangeLinkType),
Meta: createMetaObject(acmeProv), Meta: Meta{
ExternalAccountRequired: acmeProv.RequireEAB,
},
}) })
} }
// createMetaObject creates a Meta object if the ACME provisioner
// has one or more properties that are written in the ACME directory output.
// It returns nil if none of the properties are set.
func createMetaObject(p *provisioner.ACME) *Meta {
if shouldAddMetaObject(p) {
return &Meta{
TermsOfService: p.TermsOfService,
Website: p.Website,
CaaIdentities: p.CaaIdentities,
ExternalAccountRequired: p.RequireEAB,
}
}
return nil
}
// shouldAddMetaObject returns whether or not the ACME provisioner
// has properties configured that must be added to the ACME directory object.
func shouldAddMetaObject(p *provisioner.ACME) bool {
switch {
case p.TermsOfService != "":
return true
case p.Website != "":
return true
case len(p.CaaIdentities) > 0:
return true
case p.RequireEAB:
return true
default:
return false
}
}
// NotImplemented returns a 501 and is generally a placeholder for functionality which // NotImplemented returns a 501 and is generally a placeholder for functionality which
// MAY be added at some point in the future but is not in any way a guarantee of such. // MAY be added at some point in the future but is not in any way a guarantee of such.
func NotImplemented(w http.ResponseWriter, _ *http.Request) { func NotImplemented(w http.ResponseWriter, r *http.Request) {
render.Error(w, acme.NewError(acme.ErrorNotImplementedType, "this API is not implemented")) render.Error(w, acme.NewError(acme.ErrorNotImplementedType, "this API is not implemented"))
} }
@ -394,6 +363,6 @@ func GetCertificate(w http.ResponseWriter, r *http.Request) {
} }
api.LogCertificate(w, cert.Leaf) api.LogCertificate(w, cert.Leaf)
w.Header().Set("Content-Type", "application/pem-certificate-chain") w.Header().Set("Content-Type", "application/pem-certificate-chain; charset=utf-8")
w.Write(certBytes) w.Write(certBytes)
} }

View file

@ -18,13 +18,10 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil"
) )
type mockClient struct { type mockClient struct {
@ -132,35 +129,7 @@ func TestHandler_GetDirectory(t *testing.T) {
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName), NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName), RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName), KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
Meta: &Meta{ Meta: Meta{
ExternalAccountRequired: true,
},
}
return test{
ctx: ctx,
dir: expDir,
statusCode: 200,
}
},
"ok/full-meta": func(t *testing.T) test {
prov := newACMEProv(t)
prov.TermsOfService = "https://terms.ca.local/"
prov.Website = "https://ca.local/"
prov.CaaIdentities = []string{"ca.local"}
prov.RequireEAB = true
provName := url.PathEscape(prov.GetName())
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
ctx := acme.NewProvisionerContext(context.Background(), prov)
expDir := Directory{
NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName),
NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName),
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
Meta: &Meta{
TermsOfService: "https://terms.ca.local/",
Website: "https://ca.local/",
CaaIdentities: []string{"ca.local"},
ExternalAccountRequired: true, ExternalAccountRequired: true,
}, },
} }
@ -193,6 +162,7 @@ func TestHandler_GetDirectory(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -346,7 +316,7 @@ func TestHandler_GetAuthorization(t *testing.T) {
for name, run := range tests { for name, run := range tests {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil, "") ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
req := httptest.NewRequest("GET", "/foo/bar", nil) req := httptest.NewRequest("GET", "/foo/bar", nil)
req = req.WithContext(ctx) req = req.WithContext(ctx)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -365,6 +335,7 @@ func TestHandler_GetAuthorization(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -507,11 +478,12 @@ func TestHandler_GetCertificate(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.HasPrefix(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
assert.Equals(t, bytes.TrimSpace(body), bytes.TrimSpace(certBytes)) assert.Equals(t, bytes.TrimSpace(body), bytes.TrimSpace(certBytes))
assert.Equals(t, res.Header["Content-Type"], []string{"application/pem-certificate-chain"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/pem-certificate-chain; charset=utf-8"})
} }
}) })
} }
@ -746,7 +718,7 @@ func TestHandler_GetChallenge(t *testing.T) {
for name, run := range tests { for name, run := range tests {
tc := run(t) tc := run(t)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil, "") ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
req := httptest.NewRequest("GET", u, nil) req := httptest.NewRequest("GET", u, nil)
req = req.WithContext(ctx) req = req.WithContext(ctx)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -765,6 +737,7 @@ func TestHandler_GetChallenge(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -778,89 +751,3 @@ func TestHandler_GetChallenge(t *testing.T) {
}) })
} }
} }
func Test_createMetaObject(t *testing.T) {
tests := []struct {
name string
p *provisioner.ACME
want *Meta
}{
{
name: "no-meta",
p: &provisioner.ACME{
Type: "ACME",
Name: "acme",
},
want: nil,
},
{
name: "terms-of-service",
p: &provisioner.ACME{
Type: "ACME",
Name: "acme",
TermsOfService: "https://terms.ca.local",
},
want: &Meta{
TermsOfService: "https://terms.ca.local",
},
},
{
name: "website",
p: &provisioner.ACME{
Type: "ACME",
Name: "acme",
Website: "https://ca.local",
},
want: &Meta{
Website: "https://ca.local",
},
},
{
name: "caa",
p: &provisioner.ACME{
Type: "ACME",
Name: "acme",
CaaIdentities: []string{"ca.local", "ca.remote"},
},
want: &Meta{
CaaIdentities: []string{"ca.local", "ca.remote"},
},
},
{
name: "require-eab",
p: &provisioner.ACME{
Type: "ACME",
Name: "acme",
RequireEAB: true,
},
want: &Meta{
ExternalAccountRequired: true,
},
},
{
name: "full-meta",
p: &provisioner.ACME{
Type: "ACME",
Name: "acme",
TermsOfService: "https://terms.ca.local",
Website: "https://ca.local",
CaaIdentities: []string{"ca.local", "ca.remote"},
RequireEAB: true,
},
want: &Meta{
TermsOfService: "https://terms.ca.local",
Website: "https://ca.local",
CaaIdentities: []string{"ca.local", "ca.remote"},
ExternalAccountRequired: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createMetaObject(tt.p)
if !cmp.Equal(tt.want, got) {
t.Errorf("createMetaObject() diff =\n%s", cmp.Diff(tt.want, got))
}
})
}
}

View file

@ -7,7 +7,6 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"path"
"strings" "strings"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
@ -17,6 +16,7 @@ import (
"github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/logging" "github.com/smallstep/certificates/logging"
"github.com/smallstep/nosql"
) )
type nextHTTP = func(http.ResponseWriter, *http.Request) type nextHTTP = func(http.ResponseWriter, *http.Request)
@ -293,6 +293,7 @@ func lookupJWK(next nextHTTP) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
db := acme.MustDatabaseFromContext(ctx) db := acme.MustDatabaseFromContext(ctx)
linker := acme.MustLinkerFromContext(ctx)
jws, err := jwsFromContext(ctx) jws, err := jwsFromContext(ctx)
if err != nil { if err != nil {
@ -300,16 +301,19 @@ func lookupJWK(next nextHTTP) nextHTTP {
return return
} }
kidPrefix := linker.GetLink(ctx, acme.AccountLinkType, "")
kid := jws.Signatures[0].Protected.KeyID kid := jws.Signatures[0].Protected.KeyID
if kid == "" { if !strings.HasPrefix(kid, kidPrefix) {
render.Error(w, acme.NewError(acme.ErrorMalformedType, "signature missing 'kid'")) render.Error(w, acme.NewError(acme.ErrorMalformedType,
"kid does not have required prefix; expected %s, but got %s",
kidPrefix, kid))
return return
} }
accID := path.Base(kid) accID := strings.TrimPrefix(kid, kidPrefix)
acc, err := db.GetAccount(ctx, accID) acc, err := db.GetAccount(ctx, accID)
switch { switch {
case acme.IsErrNotFound(err): case nosql.IsErrNotFound(err):
render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType, "account with ID '%s' not found", accID)) render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType, "account with ID '%s' not found", accID))
return return
case err != nil: case err != nil:
@ -320,45 +324,6 @@ func lookupJWK(next nextHTTP) nextHTTP {
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account is not active")) render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account is not active"))
return return
} }
if storedLocation := acc.GetLocation(); storedLocation != "" {
if kid != storedLocation {
// ACME accounts should have a stored location equivalent to the
// kid in the ACME request.
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"kid does not match stored account location; expected %s, but got %s",
storedLocation, kid))
return
}
// Verify that the provisioner with which the account was created
// matches the provisioner in the request URL.
reqProv := acme.MustProvisionerFromContext(ctx)
reqProvName := reqProv.GetName()
accProvName := acc.ProvisionerName
if reqProvName != accProvName {
// Provisioner in the URL must match the provisioner with
// which the account was created.
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s",
accProvName, reqProvName))
return
}
} else {
// This code will only execute for old ACME accounts that do
// not have a cached location. The following validation was
// the original implementation of the `kid` check which has
// since been deprecated. However, the code will remain to
// ensure consistent behavior for old ACME accounts.
linker := acme.MustLinkerFromContext(ctx)
kidPrefix := linker.GetLink(ctx, acme.AccountLinkType, "")
if !strings.HasPrefix(kid, kidPrefix) {
render.Error(w, acme.NewError(acme.ErrorMalformedType,
"kid does not have required prefix; expected %s, but got %s",
kidPrefix, kid))
return
}
}
ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, accContextKey, acc)
ctx = context.WithValue(ctx, jwkContextKey, acc.Key) ctx = context.WithValue(ctx, jwkContextKey, acc.Key)
next(w, r.WithContext(ctx)) next(w, r.WithContext(ctx))

View file

@ -17,13 +17,14 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/acme"
"github.com/smallstep/nosql/database"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil" "go.step.sm/crypto/keyutil"
) )
var testBody = []byte("foo") var testBody = []byte("foo")
func testNext(w http.ResponseWriter, _ *http.Request) { func testNext(w http.ResponseWriter, r *http.Request) {
w.Write(testBody) w.Write(testBody)
} }
@ -92,6 +93,7 @@ func TestHandler_addNonce(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -145,6 +147,7 @@ func TestHandler_addDirLink(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -249,6 +252,7 @@ func TestHandler_verifyContentType(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -316,6 +320,7 @@ func TestHandler_isPostAsGet(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -327,7 +332,7 @@ func TestHandler_isPostAsGet(t *testing.T) {
type errReader int type errReader int
func (errReader) Read([]byte) (int, error) { func (errReader) Read(p []byte) (n int, err error) {
return 0, errors.New("force") return 0, errors.New("force")
} }
func (errReader) Close() error { func (errReader) Close() error {
@ -405,6 +410,7 @@ func TestHandler_parseJWS(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -512,6 +518,9 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) {
} }
}, },
"ok/empty-algorithm-in-jwk": func(t *testing.T) test { "ok/empty-algorithm-in-jwk": func(t *testing.T) test {
_pub := *pub
clone := &_pub
clone.Algorithm = ""
ctx := context.WithValue(context.Background(), jwsContextKey, parsedJWS) ctx := context.WithValue(context.Background(), jwsContextKey, parsedJWS)
ctx = context.WithValue(ctx, jwkContextKey, pub) ctx = context.WithValue(ctx, jwkContextKey, pub)
return test{ return test{
@ -600,6 +609,7 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -677,7 +687,31 @@ func TestHandler_lookupJWK(t *testing.T) {
linker: acme.NewLinker("test.ca.smallstep.com", "acme"), linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
ctx: ctx, ctx: ctx,
statusCode: 400, statusCode: 400,
err: acme.NewError(acme.ErrorMalformedType, "signature missing 'kid'"), err: acme.NewError(acme.ErrorMalformedType, "kid does not have required prefix; expected %s, but got ", prefix),
}
},
"fail/bad-kid-prefix": func(t *testing.T) test {
_so := new(jose.SignerOptions)
_so.WithHeader("kid", "foo")
_signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
Key: jwk.Key,
}, _so)
assert.FatalError(t, err)
_jws, err := _signer.Sign([]byte("baz"))
assert.FatalError(t, err)
_raw, err := _jws.CompactSerialize()
assert.FatalError(t, err)
_parsed, err := jose.ParseJWS(_raw)
assert.FatalError(t, err)
ctx := acme.NewProvisionerContext(context.Background(), prov)
ctx = context.WithValue(ctx, jwsContextKey, _parsed)
return test{
db: &acme.MockDB{},
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
ctx: ctx,
statusCode: 400,
err: acme.NewError(acme.ErrorMalformedType, "kid does not have required prefix; expected %s, but got foo", prefix),
} }
}, },
"fail/account-not-found": func(t *testing.T) test { "fail/account-not-found": func(t *testing.T) test {
@ -688,7 +722,7 @@ func TestHandler_lookupJWK(t *testing.T) {
db: &acme.MockDB{ db: &acme.MockDB{
MockGetAccount: func(ctx context.Context, accID string) (*acme.Account, error) { MockGetAccount: func(ctx context.Context, accID string) (*acme.Account, error) {
assert.Equals(t, accID, accID) assert.Equals(t, accID, accID)
return nil, acme.ErrNotFound return nil, database.ErrNotFound
}, },
}, },
ctx: ctx, ctx: ctx,
@ -729,77 +763,7 @@ func TestHandler_lookupJWK(t *testing.T) {
err: acme.NewError(acme.ErrorUnauthorizedType, "account is not active"), err: acme.NewError(acme.ErrorUnauthorizedType, "account is not active"),
} }
}, },
"fail/account-with-location-prefix/bad-kid": func(t *testing.T) test { "ok": func(t *testing.T) test {
acc := &acme.Account{LocationPrefix: "foobar", Status: "valid"}
ctx := acme.NewProvisionerContext(context.Background(), prov)
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
return test{
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
db: &acme.MockDB{
MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {
assert.Equals(t, id, accID)
return acc, nil
},
},
ctx: ctx,
statusCode: http.StatusUnauthorized,
err: acme.NewError(acme.ErrorUnauthorizedType, "kid does not match stored account location; expected foobar, but %q", prefix+accID),
}
},
"fail/account-with-location-prefix/bad-provisioner": func(t *testing.T) test {
acc := &acme.Account{LocationPrefix: prefix + accID, Status: "valid", Key: jwk, ProvisionerName: "other"}
ctx := acme.NewProvisionerContext(context.Background(), prov)
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
return test{
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
db: &acme.MockDB{
MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {
assert.Equals(t, id, accID)
return acc, nil
},
},
ctx: ctx,
next: func(w http.ResponseWriter, r *http.Request) {
_acc, err := accountFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _acc, acc)
_jwk, err := jwkFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _jwk, jwk)
w.Write(testBody)
},
statusCode: http.StatusUnauthorized,
err: acme.NewError(acme.ErrorUnauthorizedType,
"account provisioner does not match requested provisioner; account provisioner = %s, reqested provisioner = %s",
prov.GetName(), "other"),
}
},
"ok/account-with-location-prefix": func(t *testing.T) test {
acc := &acme.Account{LocationPrefix: prefix + accID, Status: "valid", Key: jwk, ProvisionerName: prov.GetName()}
ctx := acme.NewProvisionerContext(context.Background(), prov)
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
return test{
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
db: &acme.MockDB{
MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {
assert.Equals(t, id, accID)
return acc, nil
},
},
ctx: ctx,
next: func(w http.ResponseWriter, r *http.Request) {
_acc, err := accountFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _acc, acc)
_jwk, err := jwkFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _jwk, jwk)
w.Write(testBody)
},
statusCode: http.StatusOK,
}
},
"ok/account-without-location-prefix": func(t *testing.T) test {
acc := &acme.Account{Status: "valid", Key: jwk} acc := &acme.Account{Status: "valid", Key: jwk}
ctx := acme.NewProvisionerContext(context.Background(), prov) ctx := acme.NewProvisionerContext(context.Background(), prov)
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS) ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
@ -847,6 +811,7 @@ func TestHandler_lookupJWK(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -1046,6 +1011,7 @@ func TestHandler_extractJWK(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -1421,6 +1387,7 @@ func TestHandler_validateJWS(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -1603,6 +1570,7 @@ func TestHandler_extractOrLookupJWK(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -1687,6 +1655,7 @@ func TestHandler_checkPrerequisites(t *testing.T) {
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae)) assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {

View file

@ -392,7 +392,7 @@ func challengeTypes(az *acme.Authorization) []acme.ChallengeType {
case acme.IP: case acme.IP:
chTypes = []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01} chTypes = []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}
case acme.DNS: case acme.DNS:
chTypes = []acme.ChallengeType{acme.DNS01, acme.NNS01} chTypes = []acme.ChallengeType{acme.DNS01}
// HTTP and TLS challenges can only be used for identifiers without wildcards. // HTTP and TLS challenges can only be used for identifiers without wildcards.
if !az.Wildcard { if !az.Wildcard {
chTypes = append(chTypes, []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}...) chTypes = append(chTypes, []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}...)

View file

@ -179,12 +179,11 @@ func TestNewOrderRequest_Validate(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
if err := tc.nor.Validate(); err != nil { if err := tc.nor.Validate(); err != nil {
if assert.NotNil(t, err) { if assert.NotNil(t, err) {
var ae *acme.Error ae, ok := err.(*acme.Error)
if assert.True(t, errors.As(err, &ae)) { assert.True(t, ok)
assert.HasPrefix(t, ae.Error(), tc.err.Error()) assert.HasPrefix(t, ae.Error(), tc.err.Error())
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode()) assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
}
} }
} else { } else {
if assert.Nil(t, tc.err) { if assert.Nil(t, tc.err) {
@ -254,12 +253,11 @@ func TestFinalizeRequestValidate(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
if err := tc.fr.Validate(); err != nil { if err := tc.fr.Validate(); err != nil {
if assert.NotNil(t, err) { if assert.NotNil(t, err) {
var ae *acme.Error ae, ok := err.(*acme.Error)
if assert.True(t, errors.As(err, &ae)) { assert.True(t, ok)
assert.HasPrefix(t, ae.Error(), tc.err.Error()) assert.HasPrefix(t, ae.Error(), tc.err.Error())
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode()) assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
}
} }
} else { } else {
if assert.Nil(t, tc.err) { if assert.Nil(t, tc.err) {
@ -486,6 +484,7 @@ func TestHandler_GetOrder(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -757,22 +756,19 @@ func TestHandler_newAuthorization(t *testing.T) {
} }
for name, run := range tests { for name, run := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
if name == "ok/permanent-identifier-enabled" {
println(1)
}
tc := run(t) tc := run(t)
ctx := newBaseContext(context.Background(), tc.db) ctx := newBaseContext(context.Background(), tc.db)
ctx = acme.NewProvisionerContext(ctx, tc.prov) ctx = acme.NewProvisionerContext(ctx, tc.prov)
if err := newAuthorization(ctx, tc.az); err != nil { if err := newAuthorization(ctx, tc.az); err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var k *acme.Error switch k := err.(type) {
if assert.True(t, errors.As(err, &k)) { case *acme.Error:
assert.Equals(t, k.Type, tc.err.Type) assert.Equals(t, k.Type, tc.err.Type)
assert.Equals(t, k.Detail, tc.err.Detail) assert.Equals(t, k.Detail, tc.err.Detail)
assert.Equals(t, k.Status, tc.err.Status) assert.Equals(t, k.Status, tc.err.Status)
assert.Equals(t, k.Err.Error(), tc.err.Err.Error()) assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
assert.Equals(t, k.Detail, tc.err.Detail) assert.Equals(t, k.Detail, tc.err.Detail)
} else { default:
assert.FatalError(t, errors.New("unexpected error type")) assert.FatalError(t, errors.New("unexpected error type"))
} }
} }
@ -1845,6 +1841,7 @@ func TestHandler_NewOrder(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -2142,6 +2139,7 @@ func TestHandler_FinalizeOrder(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {

View file

@ -151,7 +151,7 @@ func RevokeCert(w http.ResponseWriter, r *http.Request) {
// the identifiers in the certificate are extracted and compared against the (valid) Authorizations // the identifiers in the certificate are extracted and compared against the (valid) Authorizations
// that are stored for the ACME Account. If these sets match, the Account is considered authorized // that are stored for the ACME Account. If these sets match, the Account is considered authorized
// to revoke the certificate. If this check fails, the client will receive an unauthorized error. // to revoke the certificate. If this check fails, the client will receive an unauthorized error.
func isAccountAuthorized(_ context.Context, dbCert *acme.Certificate, certToBeRevoked *x509.Certificate, account *acme.Account) *acme.Error { func isAccountAuthorized(ctx context.Context, dbCert *acme.Certificate, certToBeRevoked *x509.Certificate, account *acme.Account) *acme.Error {
if !account.IsValid() { if !account.IsValid() {
return wrapUnauthorizedError(certToBeRevoked, nil, fmt.Sprintf("account '%s' has status '%s'", account.ID, account.Status), nil) return wrapUnauthorizedError(certToBeRevoked, nil, fmt.Sprintf("account '%s' has status '%s'", account.ID, account.Status), nil)
} }

View file

@ -258,7 +258,7 @@ func jwkEncode(pub crypto.PublicKey) (string, error) {
// jwsFinal constructs the final JWS object. // jwsFinal constructs the final JWS object.
// Implementation taken from github.com/mholt/acmez, which seems to be based on // Implementation taken from github.com/mholt/acmez, which seems to be based on
// https://github.com/golang/crypto/blob/master/acme/jws.go. // https://github.com/golang/crypto/blob/master/acme/jws.go.
func jwsFinal(_ crypto.Hash, sig []byte, phead, payload string) ([]byte, error) { func jwsFinal(sha crypto.Hash, sig []byte, phead, payload string) ([]byte, error) {
enc := struct { enc := struct {
Protected string `json:"protected"` Protected string `json:"protected"`
Payload string `json:"payload"` Payload string `json:"payload"`
@ -281,7 +281,7 @@ type mockCA struct {
MockAreSANsallowed func(ctx context.Context, sans []string) error MockAreSANsallowed func(ctx context.Context, sans []string) error
} }
func (m *mockCA) Sign(*x509.CertificateRequest, provisioner.SignOptions, ...provisioner.SignOption) ([]*x509.Certificate, error) { func (m *mockCA) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
return nil, nil return nil, nil
} }
@ -1090,6 +1090,7 @@ func TestHandler_RevokeCert(t *testing.T) {
assert.Equals(t, ae.Type, tc.err.Type) assert.Equals(t, ae.Type, tc.err.Type)
assert.Equals(t, ae.Detail, tc.err.Detail) 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, ae.Subproblems, tc.err.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else { } else {
@ -1229,6 +1230,7 @@ func TestHandler_isAccountAuthorized(t *testing.T) {
assert.Equals(t, acmeErr.Type, tc.err.Type) assert.Equals(t, acmeErr.Type, tc.err.Type)
assert.Equals(t, acmeErr.Status, tc.err.Status) assert.Equals(t, acmeErr.Status, tc.err.Status)
assert.Equals(t, acmeErr.Detail, tc.err.Detail) assert.Equals(t, acmeErr.Detail, tc.err.Detail)
assert.Equals(t, acmeErr.Identifier, tc.err.Identifier)
assert.Equals(t, acmeErr.Subproblems, tc.err.Subproblems) assert.Equals(t, acmeErr.Subproblems, tc.err.Subproblems)
}) })
@ -1321,6 +1323,7 @@ func Test_wrapUnauthorizedError(t *testing.T) {
assert.Equals(t, acmeErr.Type, tc.want.Type) assert.Equals(t, acmeErr.Type, tc.want.Type)
assert.Equals(t, acmeErr.Status, tc.want.Status) assert.Equals(t, acmeErr.Status, tc.want.Status)
assert.Equals(t, acmeErr.Detail, tc.want.Detail) assert.Equals(t, acmeErr.Detail, tc.want.Detail)
assert.Equals(t, acmeErr.Identifier, tc.want.Identifier)
assert.Equals(t, acmeErr.Subproblems, tc.want.Subproblems) assert.Equals(t, acmeErr.Subproblems, tc.want.Subproblems)
}) })
} }

View file

@ -8,16 +8,15 @@ import (
// Authorization representst an ACME Authorization. // Authorization representst an ACME Authorization.
type Authorization struct { type Authorization struct {
ID string `json:"-"` ID string `json:"-"`
AccountID string `json:"-"` AccountID string `json:"-"`
Token string `json:"-"` Token string `json:"-"`
Fingerprint string `json:"-"` Identifier Identifier `json:"identifier"`
Identifier Identifier `json:"identifier"` Status Status `json:"status"`
Status Status `json:"status"` Challenges []*Challenge `json:"challenges"`
Challenges []*Challenge `json:"challenges"` Wildcard bool `json:"wildcard"`
Wildcard bool `json:"wildcard"` ExpiresAt time.Time `json:"expires"`
ExpiresAt time.Time `json:"expires"` Error *Error `json:"error,omitempty"`
Error *Error `json:"error,omitempty"`
} }
// ToLog enables response logging. // ToLog enables response logging.

View file

@ -130,14 +130,14 @@ func TestAuthorization_UpdateStatus(t *testing.T) {
tc := run(t) tc := run(t)
if err := tc.az.UpdateStatus(context.Background(), tc.db); err != nil { if err := tc.az.UpdateStatus(context.Background(), tc.db); err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var k *Error switch k := err.(type) {
if errors.As(err, &k) { case *Error:
assert.Equals(t, k.Type, tc.err.Type) assert.Equals(t, k.Type, tc.err.Type)
assert.Equals(t, k.Detail, tc.err.Detail) assert.Equals(t, k.Detail, tc.err.Detail)
assert.Equals(t, k.Status, tc.err.Status) assert.Equals(t, k.Status, tc.err.Status)
assert.Equals(t, k.Err.Error(), tc.err.Err.Error()) assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
assert.Equals(t, k.Detail, tc.err.Detail) assert.Equals(t, k.Detail, tc.err.Detail)
} else { default:
assert.FatalError(t, errors.New("unexpected error type")) assert.FatalError(t, errors.New("unexpected error type"))
} }
} }

View file

@ -27,13 +27,10 @@ import (
"github.com/fxamacker/cbor/v2" "github.com/fxamacker/cbor/v2"
"github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2"
"golang.org/x/exp/slices" "github.com/ryboe/q"
"github.com/smallstep/go-attestation/attest"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/pemutil" "go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x509util"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
) )
@ -49,20 +46,6 @@ const (
TLSALPN01 ChallengeType = "tls-alpn-01" TLSALPN01 ChallengeType = "tls-alpn-01"
// DEVICEATTEST01 is the device-attest-01 ACME challenge type // DEVICEATTEST01 is the device-attest-01 ACME challenge type
DEVICEATTEST01 ChallengeType = "device-attest-01" DEVICEATTEST01 ChallengeType = "device-attest-01"
// NNS01 is the nns-01 ACME challenge type
NNS01 ChallengeType = "nns-01"
)
var (
// InsecurePortHTTP01 is the port used to verify http-01 challenges. If not set it
// defaults to 80.
InsecurePortHTTP01 int
// InsecurePortTLSALPN01 is the port used to verify tls-alpn-01 challenges. If not
// set it defaults to 443.
//
// This variable can be used for testing purposes.
InsecurePortTLSALPN01 int
) )
// Challenge represents an ACME response Challenge type. // Challenge represents an ACME response Challenge type.
@ -88,9 +71,10 @@ func (ch *Challenge) ToLog() (interface{}, error) {
return string(b), nil return string(b), nil
} }
// Validate attempts to validate the Challenge. Stores changes to the Challenge // Validate attempts to validate the challenge. Stores changes to the Challenge
// type using the DB interface. If the Challenge is validated, the 'status' and // type using the DB interface.
// 'validated' attributes are updated. // satisfactorily validated, the 'status' and 'validated' attributes are
// updated.
func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, payload []byte) error { 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 already valid or invalid then return without performing validation.
if ch.Status != StatusPending { if ch.Status != StatusPending {
@ -105,8 +89,6 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey,
return tlsalpn01Validate(ctx, ch, db, jwk) return tlsalpn01Validate(ctx, ch, db, jwk)
case DEVICEATTEST01: case DEVICEATTEST01:
return deviceAttest01Validate(ctx, ch, db, jwk, payload) return deviceAttest01Validate(ctx, ch, db, jwk, payload)
case NNS01:
return nns01Validate(ctx, ch, db, jwk)
default: default:
return NewErrorISE("unexpected challenge type '%s'", ch.Type) return NewErrorISE("unexpected challenge type '%s'", ch.Type)
} }
@ -115,12 +97,6 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey,
func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error { func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error {
u := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)} u := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)}
// Append insecure port if set.
// Only used for testing purposes.
if InsecurePortHTTP01 != 0 {
u.Host += ":" + strconv.Itoa(InsecurePortHTTP01)
}
vc := MustClientFromContext(ctx) vc := MustClientFromContext(ctx)
resp, err := vc.Get(u.String()) resp, err := vc.Get(u.String())
if err != nil { if err != nil {
@ -190,17 +166,10 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON
// [RFC5246] or higher when connecting to clients for validation. // [RFC5246] or higher when connecting to clients for validation.
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
ServerName: serverName(ch), ServerName: serverName(ch),
InsecureSkipVerify: true, //nolint:gosec // we expect a self-signed challenge certificate InsecureSkipVerify: true, // nolint:gosec // we expect a self-signed challenge certificate
} }
var hostPort string hostPort := net.JoinHostPort(ch.Value, "443")
// Allow to change TLS port for testing purposes.
if port := InsecurePortTLSALPN01; port == 0 {
hostPort = net.JoinHostPort(ch.Value, "443")
} else {
hostPort = net.JoinHostPort(ch.Value, strconv.Itoa(port))
}
vc := MustClientFromContext(ctx) vc := MustClientFromContext(ctx)
conn, err := vc.TLSDial("tcp", hostPort, config) conn, err := vc.TLSDial("tcp", hostPort, config)
@ -345,26 +314,20 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK
return nil return nil
} }
type payloadType struct { type Payload struct {
AttObj string `json:"attObj"` AttObj string `json:"attObj"`
Error string `json:"error"` Error string `json:"error"`
} }
type attestationObject struct { type AttestationObject struct {
Format string `json:"fmt"` Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"` AttStatement map[string]interface{} `json:"attStmt,omitempty"`
} }
// TODO(bweeks): move attestation verification to a shared package. // 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 { func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error {
// Load authorization to store the key fingerprint. var p Payload
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 { if err := json.Unmarshal(payload, &p); err != nil {
return WrapErrorISE(err, "error unmarshalling JSON") return WrapErrorISE(err, "error unmarshalling JSON")
} }
@ -378,7 +341,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
return WrapErrorISE(err, "error base64 decoding attObj") return WrapErrorISE(err, "error base64 decoding attObj")
} }
att := attestationObject{} att := AttestationObject{}
if err := cbor.Unmarshal(attObj, &att); err != nil { if err := cbor.Unmarshal(attObj, &att); err != nil {
return WrapErrorISE(err, "error unmarshalling CBOR") return WrapErrorISE(err, "error unmarshalling CBOR")
} }
@ -402,6 +365,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
} }
return WrapErrorISE(err, "error validating attestation") return WrapErrorISE(err, "error validating attestation")
} }
// Validate nonce with SHA-256 of the token. // Validate nonce with SHA-256 of the token.
if len(data.Nonce) != 0 { if len(data.Nonce) != 0 {
sum := sha256.Sum256([]byte(ch.Token)) sum := sha256.Sum256([]byte(ch.Token))
@ -417,9 +381,6 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
if data.UDID != ch.Value && data.SerialNumber != ch.Value { if data.UDID != ch.Value && data.SerialNumber != ch.Value {
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match")) 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": case "step":
data, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att) data, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att)
if err != nil { if err != nil {
@ -433,53 +394,36 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
return WrapErrorISE(err, "error validating attestation") return WrapErrorISE(err, "error validating attestation")
} }
// Validate the YubiKey serial number from the attestation // Validate Apple's ClientIdentifier (Identifier.Value) with device
// certificate with the challenged Order value. // identifiers.
// //
// Note: We might want to use an external service for this. // Note: We might want to use an external service for this.
if data.SerialNumber != ch.Value { if data.SerialNumber != ch.Value {
subproblem := NewSubproblemWithIdentifier( return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match"))
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))
} }
case "tpm": // TODO(hs): this may end up being a different case; this is the generic `tpm` format from `WebAuthn`
// Update attestation key fingerprint to compare against the CSR data, err := doTPMAttestationFormat(ctx, ch, db, &att)
az.Fingerprint = data.Fingerprint
case "tpm":
data, err := doTPMAttestationFormat(ctx, prov, ch, jwk, &att)
if err != nil { if err != nil {
// TODO(hs): we should provide more details in the error reported to the client; q.Q("attestation error:", err)
// "Attestation statement cannot be verified" is VERY generic. Also holds true for the other formats. return err
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 expectedDigest, err := keyAuthDigest(jwk, ch.Token)
// extracted from the AK certificate. This is currently the case for AK certs from the CLI, as we if err != nil {
// haven't implemented a way for AK certs requested by the CLI to always contain the requested return fmt.Errorf("error creating key auth digest: %w", err)
// 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 q.Q(data)
az.Fingerprint = data.Fingerprint q.Q(expectedDigest)
// 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(expectedDigest, data.ExtraData) == 0 {
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "key authorization doesn't match"))
}
// TODO(hs): more properties to verify? Apple method has nonce, check for permanent identifier.
default: default:
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "unexpected attestation object format")) return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "unexpected attestation object format"))
} }
@ -489,360 +433,132 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
ch.Error = nil ch.Error = nil
ch.ValidatedAt = clock.Now().Format(time.RFC3339) 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 { if err := db.UpdateChallenge(ctx, ch); err != nil {
return WrapErrorISE(err, "error updating challenge") return WrapErrorISE(err, "error updating challenge")
} }
return nil return nil
} }
func nns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error { // Borrowed from:
domain := strings.TrimPrefix(ch.Value, "*.") // https://github.com/golang/crypto/blob/master/acme/acme.go#L748
func keyAuthDigest(jwk *jose.JSONWebKey, token string) ([]byte, error) {
nnsCtx, ok := GetNNSContext(ctx) th, err := jwk.Thumbprint(crypto.SHA256) // TODO(hs): verify this is the correct thumbprint
if !ok { digest := sha256.Sum256([]byte(fmt.Sprintf("%s.%s", token, th)))
return errors.New("error retrieving NNS context") return digest[:], err
}
nns := NNS{}
err := nns.Dial(nnsCtx.nnsServer)
if err != nil {
return err
}
defer nns.Close()
txtRecords, err := nns.GetTXTRecords("acme-challenge." + domain)
if err != nil {
return storeError(ctx, db, ch, false, WrapError(ErrorNNSType, err,
"error looking up TXT records for domain %s", domain))
}
expectedKeyAuth, err := KeyAuthorization(ch.Token, jwk)
if err != nil {
return err
}
h := sha256.Sum256([]byte(expectedKeyAuth))
expected := base64.RawURLEncoding.EncodeToString(h[:])
var found bool
for _, r := range txtRecords {
if r == expected {
found = true
break
}
}
if !found {
return storeError(ctx, db, ch, false, NewError(ErrorRejectedIdentifierType,
"keyAuthorization does not match; expected %s, but got %s", expectedKeyAuth, txtRecords))
}
// Update and store the challenge.
ch.Status = StatusValid
ch.Error = nil
ch.ValidatedAt = clock.Now().Format(time.RFC3339)
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 { type tpmAttestationData struct {
Certificate *x509.Certificate Certificate *x509.Certificate
VerifiedChains [][]*x509.Certificate VerifiedChains [][]*x509.Certificate
PermanentIdentifiers []string ExtraData []byte // TODO(hs): rename this to KeyAuthorization to reflect its usage?
Fingerprint string
} }
// coseAlgorithmIdentifier models a COSEAlgorithmIdentifier. func doTPMAttestationFormat(ctx context.Context, ch *Challenge, db DB, att *AttestationObject) (*tpmAttestationData, error) {
// Also see https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier.
type coseAlgorithmIdentifier int32
const ( p := MustProvisionerFromContext(ctx)
coseAlgES256 coseAlgorithmIdentifier = -7 prov, ok := p.(*provisioner.ACME)
coseAlgRS256 coseAlgorithmIdentifier = -257
)
func doTPMAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*tpmAttestationData, error) {
ver, ok := att.AttStatement["ver"].(string)
if !ok { if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "ver not present") return nil, NewErrorISE("provisioner in context is not an ACME provisioner")
}
if ver != "2.0" {
return nil, NewError(ErrorBadAttestationStatementType, "version %q is not supported", ver)
} }
x5c, ok := att.AttStatement["x5c"].([]interface{}) x5c, ok := att.AttStatement["x5c"].([]interface{})
if !ok { if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c not present") return nil, storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "x5c not present"))
} }
if len(x5c) == 0 { if len(x5c) == 0 {
return nil, NewError(ErrorBadAttestationStatementType, "x5c is empty") return nil, storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "x5c is empty"))
} }
akCertBytes, ok := x5c[0].([]byte) der, ok := x5c[0].([]byte)
if !ok { if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed") return nil, storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "x5c is malformed"))
} }
akCert, err := x509.ParseCertificate(akCertBytes) leaf, err := x509.ParseCertificate(der)
if err != nil { if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed") return nil, storeError(ctx, db, ch, true, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed"))
} }
intermediates := x509.NewCertPool() intermediates := x509.NewCertPool()
for _, v := range x5c[1:] { if len(x5c[1:]) > 0 {
intCertBytes, vok := v.([]byte) for _, v := range x5c[1:] {
if !vok { der, ok = v.([]byte)
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed") if !ok {
return nil, storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "x5c is malformed"))
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, storeError(ctx, db, ch, true, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed"))
}
intermediates.AddCert(cert)
} }
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 // 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, // 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. // but I doubt that will happen.
if len(akCert.UnhandledCriticalExtensions) > 0 { // TODO(hs): decide on the right logic for handling unhandled critical extensions
unhandledCriticalExtensions := akCert.UnhandledCriticalExtensions[:0] if len(leaf.UnhandledCriticalExtensions) > 0 {
for _, extOID := range akCert.UnhandledCriticalExtensions { unhandledCriticalExtensions := leaf.UnhandledCriticalExtensions[:0]
if !extOID.Equal(oidSubjectAlternativeName) { for _, extOID := range leaf.UnhandledCriticalExtensions {
// critical extensions other than the Subject Alternative Name remain unhandled switch {
// TODO(hs): extend the switch statement with other allowed OIDs; this might have to become configurable too
case extOID.Equal(asn1.ObjectIdentifier{2, 5, 29, 17}):
// TODO(hs): decide when the processed extension is "OK"; permanent-identifier/hardware-module-name
for _, e := range leaf.Extensions {
if e.Id.Equal(extOID) {
// TODO(hs): validate this is in fact a valid PermanentIdentifier/HardwareModuleName
q.Q(e)
}
}
continue
default:
// OIDs that are not in the switch remain unhandled
unhandledCriticalExtensions = append(unhandledCriticalExtensions, extOID) unhandledCriticalExtensions = append(unhandledCriticalExtensions, extOID)
} }
} }
akCert.UnhandledCriticalExtensions = unhandledCriticalExtensions leaf.UnhandledCriticalExtensions = unhandledCriticalExtensions
} }
roots, ok := prov.GetAttestationRoots() roots, ok := prov.GetAttestationRoots()
if !ok { if !ok {
return nil, NewErrorISE("no root CA bundle available to verify the attestation certificate") return nil, NewErrorISE("error getting tpm attestation root CAs")
} }
// verify that the AK certificate was signed by a trusted root, verifiedChains, err := leaf.Verify(x509.VerifyOptions{
// 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, Roots: roots,
Intermediates: intermediates, Intermediates: intermediates,
CurrentTime: time.Now().Truncate(time.Second), CurrentTime: time.Now().Truncate(time.Second),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}) })
if err != nil { if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is not valid") if storeErr := storeError(ctx, db, ch, true, WrapError(ErrorBadAttestationStatementType, err, "x5c is not valid")); storeErr != nil {
} return nil, fmt.Errorf("error saving order error: %w", storeErr)
}
// validate additional AK certificate requirements return nil, fmt.Errorf("error verifying x5c leaf: %w", err)
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. // TODO(hs): implement revocation check; Verify() doesn't perform CRL check nor OCSP lookup.
// TODO(hs): more properties to verify and/or return?
sans, err := x509util.ParseSubjectAlternativeNames(akCert) q.Q(att.AttStatement)
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) certInfo, ok := att.AttStatement["certInfo"].([]byte)
if !ok { if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "invalid certInfo in attestation statement") return nil, errors.New("invalid certInfo in attestation statement")
}
if len(certInfo) == 0 {
return nil, NewError(ErrorBadAttestationStatementType, "certInfo is empty")
} }
alg, ok := att.AttStatement["alg"].(int64) tpmCertInfo, err := tpm2.DecodeAttestationData([]byte(certInfo))
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 { if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed decoding attestation data") return nil, fmt.Errorf("invalid certInfo: %w", err)
} }
keyAuth, err := KeyAuthorization(ch.Token, jwk) q.Q(tpmCertInfo.ExtraData)
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 return &tpmAttestationData{
// within the encoded `certInfo` property of the attestation statement. Certificate: leaf,
if subtle.ConstantTimeCompare(hashedKeyAuth[:], []byte(tpmCertInfo.ExtraData)) == 0 { VerifiedChains: verifiedChains,
return nil, NewError(ErrorBadAttestationStatementType, "key authorization does not match") ExtraData: []byte(tpmCertInfo.ExtraData),
} }, nil
// 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
}
return validateAKCertificateSubjectAlternativeNames(c)
}
// 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 // Apple Enterprise Attestation Root CA from
@ -875,10 +591,9 @@ type appleAttestationData struct {
UDID string UDID string
SEPVersion string SEPVersion string
Certificate *x509.Certificate Certificate *x509.Certificate
Fingerprint string
} }
func doAppleAttestationFormat(_ context.Context, prov Provisioner, _ *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. // Use configured or default attestation roots if none is configured.
roots, ok := prov.GetAttestationRoots() roots, ok := prov.GetAttestationRoots()
if !ok { if !ok {
@ -932,9 +647,6 @@ func doAppleAttestationFormat(_ context.Context, prov Provisioner, _ *Challenge,
data := &appleAttestationData{ data := &appleAttestationData{
Certificate: leaf, 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 { for _, ext := range leaf.Extensions {
switch { switch {
case ext.Id.Equal(oidAppleSerialNumber): case ext.Id.Equal(oidAppleSerialNumber):
@ -980,10 +692,9 @@ var oidYubicoSerialNumber = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 41482, 3, 7}
type stepAttestationData struct { type stepAttestationData struct {
Certificate *x509.Certificate Certificate *x509.Certificate
SerialNumber string SerialNumber string
Fingerprint string
} }
func doStepAttestationFormat(_ 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. // Use configured or default attestation roots if none is configured.
roots, ok := prov.GetAttestationRoots() roots, ok := prov.GetAttestationRoots()
if !ok { if !ok {
@ -1076,9 +787,6 @@ func doStepAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
data := &stepAttestationData{ data := &stepAttestationData{
Certificate: leaf, 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 { for _, ext := range leaf.Extensions {
if !ext.Id.Equal(oidYubicoSerialNumber) { if !ext.Id.Equal(oidYubicoSerialNumber) {
continue continue

File diff suppressed because it is too large Load diff

View file

@ -1,860 +0,0 @@
//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/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/smallstep/go-attestation/attest"
"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, err := simulator.New()
require.NoError(t, err)
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
}
})
}
}

View file

@ -56,7 +56,7 @@ func NewClient() Client {
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
//nolint:gosec // used on tls-alpn-01 challenge // nolint:gosec // used on tls-alpn-01 challenge
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check] InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
}, },
}, },

View file

@ -29,12 +29,10 @@ type CertificateAuthority interface {
} }
// NewContext adds the given acme components to the context. // NewContext adds the given acme components to the context.
func NewContext(ctx context.Context, db DB, client Client, linker Linker, fn PrerequisitesChecker, func NewContext(ctx context.Context, db DB, client Client, linker Linker, fn PrerequisitesChecker) context.Context {
nnsServer string) context.Context {
ctx = NewDatabaseContext(ctx, db) ctx = NewDatabaseContext(ctx, db)
ctx = NewClientContext(ctx, client) ctx = NewClientContext(ctx, client)
ctx = NewLinkerContext(ctx, linker) ctx = NewLinkerContext(ctx, linker)
ctx = NewNNSContext(ctx, nnsServer)
// Prerequisite checker is optional. // Prerequisite checker is optional.
if fn != nil { if fn != nil {
ctx = NewPrerequisitesCheckerContext(ctx, fn) ctx = NewPrerequisitesCheckerContext(ctx, fn)
@ -48,7 +46,7 @@ type PrerequisitesChecker func(ctx context.Context) (bool, error)
// DefaultPrerequisitesChecker is the default PrerequisiteChecker and returns // DefaultPrerequisitesChecker is the default PrerequisiteChecker and returns
// always true. // always true.
func DefaultPrerequisitesChecker(context.Context) (bool, error) { func DefaultPrerequisitesChecker(ctx context.Context) (bool, error) {
return true, nil return true, nil
} }

View file

@ -12,12 +12,6 @@ import (
// account. // account.
var ErrNotFound = errors.New("not found") var ErrNotFound = errors.New("not found")
// IsErrNotFound returns true if the error is a "not found" error. Returns false
// otherwise.
func IsErrNotFound(err error) bool {
return errors.Is(err, ErrNotFound)
}
// DB is the DB interface expected by the step-ca ACME API. // DB is the DB interface expected by the step-ca ACME API.
type DB interface { type DB interface {
CreateAccount(ctx context.Context, acc *Account) error CreateAccount(ctx context.Context, acc *Account) error

View file

@ -13,14 +13,12 @@ import (
// dbAccount represents an ACME account. // dbAccount represents an ACME account.
type dbAccount struct { type dbAccount struct {
ID string `json:"id"` ID string `json:"id"`
Key *jose.JSONWebKey `json:"key"` Key *jose.JSONWebKey `json:"key"`
Contact []string `json:"contact,omitempty"` Contact []string `json:"contact,omitempty"`
Status acme.Status `json:"status"` Status acme.Status `json:"status"`
LocationPrefix string `json:"locationPrefix"` CreatedAt time.Time `json:"createdAt"`
ProvisionerName string `json:"provisionerName"` DeactivatedAt time.Time `json:"deactivatedAt"`
CreatedAt time.Time `json:"createdAt"`
DeactivatedAt time.Time `json:"deactivatedAt"`
} }
func (dba *dbAccount) clone() *dbAccount { func (dba *dbAccount) clone() *dbAccount {
@ -28,7 +26,7 @@ func (dba *dbAccount) clone() *dbAccount {
return &nu return &nu
} }
func (db *DB) getAccountIDByKeyID(_ context.Context, kid string) (string, error) { func (db *DB) getAccountIDByKeyID(ctx context.Context, kid string) (string, error) {
id, err := db.db.Get(accountByKeyIDTable, []byte(kid)) id, err := db.db.Get(accountByKeyIDTable, []byte(kid))
if err != nil { if err != nil {
if nosqlDB.IsErrNotFound(err) { if nosqlDB.IsErrNotFound(err) {
@ -40,7 +38,7 @@ func (db *DB) getAccountIDByKeyID(_ context.Context, kid string) (string, error)
} }
// getDBAccount retrieves and unmarshals dbAccount. // getDBAccount retrieves and unmarshals dbAccount.
func (db *DB) getDBAccount(_ context.Context, id string) (*dbAccount, error) { func (db *DB) getDBAccount(ctx context.Context, id string) (*dbAccount, error) {
data, err := db.db.Get(accountTable, []byte(id)) data, err := db.db.Get(accountTable, []byte(id))
if err != nil { if err != nil {
if nosqlDB.IsErrNotFound(err) { if nosqlDB.IsErrNotFound(err) {
@ -64,12 +62,10 @@ func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error)
} }
return &acme.Account{ return &acme.Account{
Status: dbacc.Status, Status: dbacc.Status,
Contact: dbacc.Contact, Contact: dbacc.Contact,
Key: dbacc.Key, Key: dbacc.Key,
ID: dbacc.ID, ID: dbacc.ID,
LocationPrefix: dbacc.LocationPrefix,
ProvisionerName: dbacc.ProvisionerName,
}, nil }, nil
} }
@ -91,13 +87,11 @@ func (db *DB) CreateAccount(ctx context.Context, acc *acme.Account) error {
} }
dba := &dbAccount{ dba := &dbAccount{
ID: acc.ID, ID: acc.ID,
Key: acc.Key, Key: acc.Key,
Contact: acc.Contact, Contact: acc.Contact,
Status: acc.Status, Status: acc.Status,
CreatedAt: clock.Now(), CreatedAt: clock.Now(),
LocationPrefix: acc.LocationPrefix,
ProvisionerName: acc.ProvisionerName,
} }
kid, err := acme.KeyToID(dba.Key) kid, err := acme.KeyToID(dba.Key)

View file

@ -95,16 +95,16 @@ func TestDB_getDBAccount(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if dbacc, err := d.getDBAccount(context.Background(), accID); err != nil { if dbacc, err := d.getDBAccount(context.Background(), accID); err != nil {
var acmeErr *acme.Error switch k := err.(type) {
if errors.As(err, &acmeErr) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -174,16 +174,16 @@ func TestDB_getAccountIDByKeyID(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if retAccID, err := d.getAccountIDByKeyID(context.Background(), kid); err != nil { if retAccID, err := d.getAccountIDByKeyID(context.Background(), kid); err != nil {
var acmeErr *acme.Error switch k := err.(type) {
if errors.As(err, &acmeErr) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -197,8 +197,6 @@ func TestDB_getAccountIDByKeyID(t *testing.T) {
func TestDB_GetAccount(t *testing.T) { func TestDB_GetAccount(t *testing.T) {
accID := "accID" accID := "accID"
locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/"
provisionerName := "foo"
type test struct { type test struct {
db nosql.DB db nosql.DB
err error err error
@ -224,14 +222,12 @@ func TestDB_GetAccount(t *testing.T) {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err) assert.FatalError(t, err)
dbacc := &dbAccount{ dbacc := &dbAccount{
ID: accID, ID: accID,
Status: acme.StatusDeactivated, Status: acme.StatusDeactivated,
CreatedAt: now, CreatedAt: now,
DeactivatedAt: now, DeactivatedAt: now,
Contact: []string{"foo", "bar"}, Contact: []string{"foo", "bar"},
Key: jwk, Key: jwk,
LocationPrefix: locationPrefix,
ProvisionerName: provisionerName,
} }
b, err := json.Marshal(dbacc) b, err := json.Marshal(dbacc)
assert.FatalError(t, err) assert.FatalError(t, err)
@ -252,16 +248,16 @@ func TestDB_GetAccount(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if acc, err := d.GetAccount(context.Background(), accID); err != nil { if acc, err := d.GetAccount(context.Background(), accID); err != nil {
var acmeErr *acme.Error switch k := err.(type) {
if errors.As(err, &acmeErr) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -270,8 +266,6 @@ func TestDB_GetAccount(t *testing.T) {
assert.Equals(t, acc.ID, tc.dbacc.ID) assert.Equals(t, acc.ID, tc.dbacc.ID)
assert.Equals(t, acc.Status, tc.dbacc.Status) assert.Equals(t, acc.Status, tc.dbacc.Status)
assert.Equals(t, acc.Contact, tc.dbacc.Contact) assert.Equals(t, acc.Contact, tc.dbacc.Contact)
assert.Equals(t, acc.LocationPrefix, tc.dbacc.LocationPrefix)
assert.Equals(t, acc.ProvisionerName, tc.dbacc.ProvisionerName)
assert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID) assert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID)
} }
}) })
@ -360,16 +354,16 @@ func TestDB_GetAccountByKeyID(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if acc, err := d.GetAccountByKeyID(context.Background(), kid); err != nil { if acc, err := d.GetAccountByKeyID(context.Background(), kid); err != nil {
var acmeErr *acme.Error switch k := err.(type) {
if errors.As(err, &acmeErr) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -385,7 +379,6 @@ func TestDB_GetAccountByKeyID(t *testing.T) {
} }
func TestDB_CreateAccount(t *testing.T) { func TestDB_CreateAccount(t *testing.T) {
locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/"
type test struct { type test struct {
db nosql.DB db nosql.DB
acc *acme.Account acc *acme.Account
@ -397,10 +390,9 @@ func TestDB_CreateAccount(t *testing.T) {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err) assert.FatalError(t, err)
acc := &acme.Account{ acc := &acme.Account{
Status: acme.StatusValid, Status: acme.StatusValid,
Contact: []string{"foo", "bar"}, Contact: []string{"foo", "bar"},
Key: jwk, Key: jwk,
LocationPrefix: locationPrefix,
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
@ -421,10 +413,9 @@ func TestDB_CreateAccount(t *testing.T) {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err) assert.FatalError(t, err)
acc := &acme.Account{ acc := &acme.Account{
Status: acme.StatusValid, Status: acme.StatusValid,
Contact: []string{"foo", "bar"}, Contact: []string{"foo", "bar"},
Key: jwk, Key: jwk,
LocationPrefix: locationPrefix,
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
@ -445,10 +436,9 @@ func TestDB_CreateAccount(t *testing.T) {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err) assert.FatalError(t, err)
acc := &acme.Account{ acc := &acme.Account{
Status: acme.StatusValid, Status: acme.StatusValid,
Contact: []string{"foo", "bar"}, Contact: []string{"foo", "bar"},
Key: jwk, Key: jwk,
LocationPrefix: locationPrefix,
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
@ -466,8 +456,6 @@ func TestDB_CreateAccount(t *testing.T) {
assert.FatalError(t, json.Unmarshal(nu, dbacc)) assert.FatalError(t, json.Unmarshal(nu, dbacc))
assert.Equals(t, dbacc.ID, string(key)) assert.Equals(t, dbacc.ID, string(key))
assert.Equals(t, dbacc.Contact, acc.Contact) assert.Equals(t, dbacc.Contact, acc.Contact)
assert.Equals(t, dbacc.LocationPrefix, acc.LocationPrefix)
assert.Equals(t, dbacc.ProvisionerName, acc.ProvisionerName)
assert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID) assert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID)
assert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt)) assert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt))
assert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt)) assert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt))
@ -491,10 +479,9 @@ func TestDB_CreateAccount(t *testing.T) {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err) assert.FatalError(t, err)
acc := &acme.Account{ acc := &acme.Account{
Status: acme.StatusValid, Status: acme.StatusValid,
Contact: []string{"foo", "bar"}, Contact: []string{"foo", "bar"},
Key: jwk, Key: jwk,
LocationPrefix: locationPrefix,
} }
return test{ return test{
db: &db.MockNoSQLDB{ db: &db.MockNoSQLDB{
@ -513,8 +500,6 @@ func TestDB_CreateAccount(t *testing.T) {
assert.FatalError(t, json.Unmarshal(nu, dbacc)) assert.FatalError(t, json.Unmarshal(nu, dbacc))
assert.Equals(t, dbacc.ID, string(key)) assert.Equals(t, dbacc.ID, string(key))
assert.Equals(t, dbacc.Contact, acc.Contact) assert.Equals(t, dbacc.Contact, acc.Contact)
assert.Equals(t, dbacc.LocationPrefix, acc.LocationPrefix)
assert.Equals(t, dbacc.ProvisionerName, acc.ProvisionerName)
assert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID) assert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID)
assert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt)) assert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt))
assert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt)) assert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt))
@ -554,14 +539,12 @@ func TestDB_UpdateAccount(t *testing.T) {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err) assert.FatalError(t, err)
dbacc := &dbAccount{ dbacc := &dbAccount{
ID: accID, ID: accID,
Status: acme.StatusDeactivated, Status: acme.StatusDeactivated,
CreatedAt: now, CreatedAt: now,
DeactivatedAt: now, DeactivatedAt: now,
Contact: []string{"foo", "bar"}, Contact: []string{"foo", "bar"},
LocationPrefix: "foo", Key: jwk,
ProvisionerName: "alpha",
Key: jwk,
} }
b, err := json.Marshal(dbacc) b, err := json.Marshal(dbacc)
assert.FatalError(t, err) assert.FatalError(t, err)
@ -661,12 +644,10 @@ func TestDB_UpdateAccount(t *testing.T) {
}, },
"ok": func(t *testing.T) test { "ok": func(t *testing.T) test {
acc := &acme.Account{ acc := &acme.Account{
ID: accID, ID: accID,
Status: acme.StatusDeactivated, Status: acme.StatusDeactivated,
Contact: []string{"baz", "zap"}, Contact: []string{"foo", "bar"},
LocationPrefix: "bar", Key: jwk,
ProvisionerName: "beta",
Key: jwk,
} }
return test{ return test{
acc: acc, acc: acc,
@ -685,10 +666,7 @@ func TestDB_UpdateAccount(t *testing.T) {
assert.FatalError(t, json.Unmarshal(nu, dbNew)) assert.FatalError(t, json.Unmarshal(nu, dbNew))
assert.Equals(t, dbNew.ID, dbacc.ID) assert.Equals(t, dbNew.ID, dbacc.ID)
assert.Equals(t, dbNew.Status, acc.Status) assert.Equals(t, dbNew.Status, acc.Status)
assert.Equals(t, dbNew.Contact, acc.Contact) assert.Equals(t, dbNew.Contact, dbacc.Contact)
// LocationPrefix should not change.
assert.Equals(t, dbNew.LocationPrefix, dbacc.LocationPrefix)
assert.Equals(t, dbNew.ProvisionerName, dbacc.ProvisionerName)
assert.Equals(t, dbNew.Key.KeyID, dbacc.Key.KeyID) assert.Equals(t, dbNew.Key.KeyID, dbacc.Key.KeyID)
assert.Equals(t, dbNew.CreatedAt, dbacc.CreatedAt) assert.Equals(t, dbNew.CreatedAt, dbacc.CreatedAt)
assert.True(t, dbNew.DeactivatedAt.Add(-time.Minute).Before(now)) assert.True(t, dbNew.DeactivatedAt.Add(-time.Minute).Before(now))
@ -708,7 +686,12 @@ func TestDB_UpdateAccount(t *testing.T) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
} else { } else {
assert.Nil(t, tc.err) if assert.Nil(t, tc.err) {
assert.Equals(t, tc.acc.ID, dbacc.ID)
assert.Equals(t, tc.acc.Status, dbacc.Status)
assert.Equals(t, tc.acc.Contact, dbacc.Contact)
assert.Equals(t, tc.acc.Key.KeyID, dbacc.Key.KeyID)
}
} }
}) })
} }

View file

@ -17,7 +17,6 @@ type dbAuthz struct {
Identifier acme.Identifier `json:"identifier"` Identifier acme.Identifier `json:"identifier"`
Status acme.Status `json:"status"` Status acme.Status `json:"status"`
Token string `json:"token"` Token string `json:"token"`
Fingerprint string `json:"fingerprint,omitempty"`
ChallengeIDs []string `json:"challengeIDs"` ChallengeIDs []string `json:"challengeIDs"`
Wildcard bool `json:"wildcard"` Wildcard bool `json:"wildcard"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
@ -32,7 +31,7 @@ func (ba *dbAuthz) clone() *dbAuthz {
// getDBAuthz retrieves and unmarshals a database representation of the // getDBAuthz retrieves and unmarshals a database representation of the
// ACME Authorization type. // ACME Authorization type.
func (db *DB) getDBAuthz(_ context.Context, id string) (*dbAuthz, error) { func (db *DB) getDBAuthz(ctx context.Context, id string) (*dbAuthz, error) {
data, err := db.db.Get(authzTable, []byte(id)) data, err := db.db.Get(authzTable, []byte(id))
if nosql.IsErrNotFound(err) { if nosql.IsErrNotFound(err) {
return nil, acme.NewError(acme.ErrorMalformedType, "authz %s not found", id) return nil, acme.NewError(acme.ErrorMalformedType, "authz %s not found", id)
@ -62,16 +61,15 @@ func (db *DB) GetAuthorization(ctx context.Context, id string) (*acme.Authorizat
} }
} }
return &acme.Authorization{ return &acme.Authorization{
ID: dbaz.ID, ID: dbaz.ID,
AccountID: dbaz.AccountID, AccountID: dbaz.AccountID,
Identifier: dbaz.Identifier, Identifier: dbaz.Identifier,
Status: dbaz.Status, Status: dbaz.Status,
Challenges: chs, Challenges: chs,
Wildcard: dbaz.Wildcard, Wildcard: dbaz.Wildcard,
ExpiresAt: dbaz.ExpiresAt, ExpiresAt: dbaz.ExpiresAt,
Token: dbaz.Token, Token: dbaz.Token,
Fingerprint: dbaz.Fingerprint, Error: dbaz.Error,
Error: dbaz.Error,
}, nil }, nil
} }
@ -99,7 +97,6 @@ func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) e
Identifier: az.Identifier, Identifier: az.Identifier,
ChallengeIDs: chIDs, ChallengeIDs: chIDs,
Token: az.Token, Token: az.Token,
Fingerprint: az.Fingerprint,
Wildcard: az.Wildcard, Wildcard: az.Wildcard,
} }
@ -114,14 +111,14 @@ func (db *DB) UpdateAuthorization(ctx context.Context, az *acme.Authorization) e
} }
nu := old.clone() nu := old.clone()
nu.Status = az.Status nu.Status = az.Status
nu.Fingerprint = az.Fingerprint
nu.Error = az.Error nu.Error = az.Error
return db.save(ctx, old.ID, nu, old, "authz", authzTable) return db.save(ctx, old.ID, nu, old, "authz", authzTable)
} }
// GetAuthorizationsByAccountID retrieves and unmarshals ACME authz types from the database. // GetAuthorizationsByAccountID retrieves and unmarshals ACME authz types from the database.
func (db *DB) GetAuthorizationsByAccountID(_ context.Context, accountID string) ([]*acme.Authorization, error) { func (db *DB) GetAuthorizationsByAccountID(ctx context.Context, accountID string) ([]*acme.Authorization, error) {
entries, err := db.db.List(authzTable) entries, err := db.db.List(authzTable)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "error listing authz") return nil, errors.Wrapf(err, "error listing authz")
@ -139,16 +136,15 @@ func (db *DB) GetAuthorizationsByAccountID(_ context.Context, accountID string)
continue continue
} }
authzs = append(authzs, &acme.Authorization{ authzs = append(authzs, &acme.Authorization{
ID: dbaz.ID, ID: dbaz.ID,
AccountID: dbaz.AccountID, AccountID: dbaz.AccountID,
Identifier: dbaz.Identifier, Identifier: dbaz.Identifier,
Status: dbaz.Status, Status: dbaz.Status,
Challenges: nil, // challenges not required for current use case Challenges: nil, // challenges not required for current use case
Wildcard: dbaz.Wildcard, Wildcard: dbaz.Wildcard,
ExpiresAt: dbaz.ExpiresAt, ExpiresAt: dbaz.ExpiresAt,
Token: dbaz.Token, Token: dbaz.Token,
Fingerprint: dbaz.Fingerprint, Error: dbaz.Error,
Error: dbaz.Error,
}) })
} }

View file

@ -101,16 +101,16 @@ func TestDB_getDBAuthz(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if dbaz, err := d.getDBAuthz(context.Background(), azID); err != nil { if dbaz, err := d.getDBAuthz(context.Background(), azID); err != nil {
var acmeErr *acme.Error switch k := err.(type) {
if errors.As(err, &acmeErr) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -295,16 +295,16 @@ func TestDB_GetAuthorization(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if az, err := d.GetAuthorization(context.Background(), azID); err != nil { if az, err := d.GetAuthorization(context.Background(), azID); err != nil {
var acmeErr *acme.Error switch k := err.(type) {
if errors.As(err, &acmeErr) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -473,7 +473,6 @@ func TestDB_UpdateAuthorization(t *testing.T) {
ExpiresAt: now.Add(5 * time.Minute), ExpiresAt: now.Add(5 * time.Minute),
ChallengeIDs: []string{"foo", "bar"}, ChallengeIDs: []string{"foo", "bar"},
Wildcard: true, Wildcard: true,
Fingerprint: "fingerprint",
} }
b, err := json.Marshal(dbaz) b, err := json.Marshal(dbaz)
assert.FatalError(t, err) assert.FatalError(t, err)
@ -550,11 +549,10 @@ func TestDB_UpdateAuthorization(t *testing.T) {
{ID: "foo"}, {ID: "foo"},
{ID: "bar"}, {ID: "bar"},
}, },
Token: dbaz.Token, Token: dbaz.Token,
Wildcard: dbaz.Wildcard, Wildcard: dbaz.Wildcard,
ExpiresAt: dbaz.ExpiresAt, ExpiresAt: dbaz.ExpiresAt,
Fingerprint: "fingerprint", Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
} }
return test{ return test{
az: updAz, az: updAz,
@ -584,7 +582,6 @@ func TestDB_UpdateAuthorization(t *testing.T) {
assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard) assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard)
assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt) assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt)
assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt) 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()) assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
return nu, true, nil return nu, true, nil
}, },
@ -748,16 +745,16 @@ func TestDB_GetAuthorizationsByAccountID(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if azs, err := d.GetAuthorizationsByAccountID(context.Background(), accountID); err != nil { if azs, err := d.GetAuthorizationsByAccountID(context.Background(), accountID); err != nil {
var acmeErr *acme.Error switch k := err.(type) {
if errors.As(err, &acmeErr) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }

View file

@ -69,7 +69,7 @@ func (db *DB) CreateCertificate(ctx context.Context, cert *acme.Certificate) err
// GetCertificate retrieves and unmarshals an ACME certificate type from the // GetCertificate retrieves and unmarshals an ACME certificate type from the
// datastore. // datastore.
func (db *DB) GetCertificate(_ context.Context, id string) (*acme.Certificate, error) { func (db *DB) GetCertificate(ctx context.Context, id string) (*acme.Certificate, error) {
b, err := db.db.Get(certTable, []byte(id)) b, err := db.db.Get(certTable, []byte(id))
if nosql.IsErrNotFound(err) { if nosql.IsErrNotFound(err) {
return nil, acme.NewError(acme.ErrorMalformedType, "certificate %s not found", id) return nil, acme.NewError(acme.ErrorMalformedType, "certificate %s not found", id)
@ -138,4 +138,5 @@ func parseBundle(b []byte) ([]*x509.Certificate, error) {
return nil, errors.New("error decoding PEM: unexpected data") return nil, errors.New("error decoding PEM: unexpected data")
} }
return bundle, nil return bundle, nil
} }

View file

@ -250,16 +250,16 @@ func TestDB_GetCertificate(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
cert, err := d.GetCertificate(context.Background(), certID) cert, err := d.GetCertificate(context.Background(), certID)
if err != nil { if err != nil {
var acmeErr *acme.Error switch k := err.(type) {
if errors.As(err, &acmeErr) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -444,16 +444,16 @@ func TestDB_GetCertificateBySerial(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
cert, err := d.GetCertificateBySerial(context.Background(), serial) cert, err := d.GetCertificateBySerial(context.Background(), serial)
if err != nil { if err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }

View file

@ -6,10 +6,8 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/nosql"
"github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/acme"
"github.com/smallstep/nosql"
) )
type dbChallenge struct { type dbChallenge struct {
@ -21,7 +19,7 @@ type dbChallenge struct {
Value string `json:"value"` Value string `json:"value"`
ValidatedAt string `json:"validatedAt"` ValidatedAt string `json:"validatedAt"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
Error *acme.Error `json:"error"` // TODO(hs): a bit dangerous; should become db-specific type Error *acme.Error `json:"error"`
} }
func (dbc *dbChallenge) clone() *dbChallenge { func (dbc *dbChallenge) clone() *dbChallenge {
@ -29,7 +27,7 @@ func (dbc *dbChallenge) clone() *dbChallenge {
return &u return &u
} }
func (db *DB) getDBChallenge(_ context.Context, id string) (*dbChallenge, error) { func (db *DB) getDBChallenge(ctx context.Context, id string) (*dbChallenge, error) {
data, err := db.db.Get(challengeTable, []byte(id)) data, err := db.db.Get(challengeTable, []byte(id))
if nosql.IsErrNotFound(err) { if nosql.IsErrNotFound(err) {
return nil, acme.NewError(acme.ErrorMalformedType, "challenge %s not found", id) return nil, acme.NewError(acme.ErrorMalformedType, "challenge %s not found", id)
@ -69,7 +67,6 @@ func (db *DB) CreateChallenge(ctx context.Context, ch *acme.Challenge) error {
// GetChallenge retrieves and unmarshals an ACME challenge type from the database. // GetChallenge retrieves and unmarshals an ACME challenge type from the database.
// Implements the acme.DB GetChallenge interface. // Implements the acme.DB GetChallenge interface.
func (db *DB) GetChallenge(ctx context.Context, id, authzID string) (*acme.Challenge, error) { func (db *DB) GetChallenge(ctx context.Context, id, authzID string) (*acme.Challenge, error) {
_ = authzID // unused input
dbch, err := db.getDBChallenge(ctx, id) dbch, err := db.getDBChallenge(ctx, id)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -94,16 +94,16 @@ func TestDB_getDBChallenge(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if ch, err := d.getDBChallenge(context.Background(), chID); err != nil { if ch, err := d.getDBChallenge(context.Background(), chID); err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -286,16 +286,16 @@ func TestDB_GetChallenge(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if ch, err := d.GetChallenge(context.Background(), chID, azID); err != nil { if ch, err := d.GetChallenge(context.Background(), chID, azID); err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }

View file

@ -35,7 +35,7 @@ type dbExternalAccountKeyReference struct {
} }
// getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey. // getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey.
func (db *DB) getDBExternalAccountKey(_ context.Context, id string) (*dbExternalAccountKey, error) { func (db *DB) getDBExternalAccountKey(ctx context.Context, id string) (*dbExternalAccountKey, error) {
data, err := db.db.Get(externalAccountKeyTable, []byte(id)) data, err := db.db.Get(externalAccountKeyTable, []byte(id))
if err != nil { if err != nil {
if nosqlDB.IsErrNotFound(err) { if nosqlDB.IsErrNotFound(err) {
@ -54,6 +54,7 @@ func (db *DB) getDBExternalAccountKey(_ context.Context, id string) (*dbExternal
// CreateExternalAccountKey creates a new External Account Binding key with a name // CreateExternalAccountKey creates a new External Account Binding key with a name
func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) { func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
externalAccountKeyMutex.Lock() externalAccountKeyMutex.Lock()
defer externalAccountKeyMutex.Unlock() defer externalAccountKeyMutex.Unlock()
@ -160,8 +161,6 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID
// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner // GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner
func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
_, _ = cursor, limit // unused input
externalAccountKeyMutex.RLock() externalAccountKeyMutex.RLock()
defer externalAccountKeyMutex.RUnlock() defer externalAccountKeyMutex.RUnlock()
@ -211,7 +210,6 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI
defer externalAccountKeyMutex.RUnlock() defer externalAccountKeyMutex.RUnlock()
if reference == "" { if reference == "" {
//nolint:nilnil // legacy
return nil, nil return nil, nil
} }
@ -229,8 +227,7 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI
return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID) return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID)
} }
func (db *DB) GetExternalAccountKeyByAccountID(context.Context, string, string) (*acme.ExternalAccountKey, error) { func (db *DB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
//nolint:nilnil // legacy
return nil, nil return nil, nil
} }
@ -374,6 +371,7 @@ func sliceIndex(slice []string, item string) int {
// removeElement deletes the item if it exists in the // removeElement deletes the item if it exists in the
// slice. It returns a new slice, keeping the old one intact. // slice. It returns a new slice, keeping the old one intact.
func removeElement(slice []string, item string) []string { func removeElement(slice []string, item string) []string {
newSlice := make([]string, 0) newSlice := make([]string, 0)
index := sliceIndex(slice, item) index := sliceIndex(slice, item)
if index < 0 { if index < 0 {

View file

@ -93,16 +93,16 @@ func TestDB_getDBExternalAccountKey(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil { if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -210,16 +210,16 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil { if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -374,16 +374,16 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil { if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -580,16 +580,16 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
cursor, limit := "", 0 cursor, limit := "", 0
if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil { if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil {
assert.Equals(t, "", nextCursor) assert.Equals(t, "", nextCursor)
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.Equals(t, tc.err.Error(), err.Error()) assert.Equals(t, tc.err.Error(), err.Error())
} }
@ -672,7 +672,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
return errors.New("force default") return errors.New("force default")
} }
}, },
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) {
fmt.Println(string(bucket)) fmt.Println(string(bucket))
switch string(bucket) { switch string(bucket) {
case string(externalAccountKeyIDsByReferenceTable): case string(externalAccountKeyIDsByReferenceTable):
@ -882,16 +882,16 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil { if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.Equals(t, err.Error(), tc.err.Error()) assert.Equals(t, err.Error(), tc.err.Error())
} }

View file

@ -39,7 +39,7 @@ func (db *DB) CreateNonce(ctx context.Context) (acme.Nonce, error) {
// DeleteNonce verifies that the nonce is valid (by checking if it exists), // DeleteNonce verifies that the nonce is valid (by checking if it exists),
// and if so, consumes the nonce resource by deleting it from the database. // and if so, consumes the nonce resource by deleting it from the database.
func (db *DB) DeleteNonce(_ context.Context, nonce acme.Nonce) error { func (db *DB) DeleteNonce(ctx context.Context, nonce acme.Nonce) error {
err := db.db.Update(&database.Tx{ err := db.db.Update(&database.Tx{
Operations: []*database.TxEntry{ Operations: []*database.TxEntry{
{ {

View file

@ -146,16 +146,16 @@ func TestDB_DeleteNonce(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if err := d.DeleteNonce(context.Background(), acme.Nonce(nonceID)); err != nil { if err := d.DeleteNonce(context.Background(), acme.Nonce(nonceID)); err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }

View file

@ -48,7 +48,7 @@ func New(db nosqlDB.DB) (*DB, error) {
// save writes the new data to the database, overwriting the old data if it // save writes the new data to the database, overwriting the old data if it
// existed. // existed.
func (db *DB) save(_ context.Context, id string, nu, old interface{}, typ string, table []byte) error { func (db *DB) save(ctx context.Context, id string, nu, old interface{}, typ string, table []byte) error {
var ( var (
err error err error
newB []byte newB []byte

View file

@ -35,7 +35,7 @@ func (a *dbOrder) clone() *dbOrder {
} }
// getDBOrder retrieves and unmarshals an ACME Order type from the database. // getDBOrder retrieves and unmarshals an ACME Order type from the database.
func (db *DB) getDBOrder(_ context.Context, id string) (*dbOrder, error) { func (db *DB) getDBOrder(ctx context.Context, id string) (*dbOrder, error) {
b, err := db.db.Get(orderTable, []byte(id)) b, err := db.db.Get(orderTable, []byte(id))
if nosql.IsErrNotFound(err) { if nosql.IsErrNotFound(err) {
return nil, acme.NewError(acme.ErrorMalformedType, "order %s not found", id) return nil, acme.NewError(acme.ErrorMalformedType, "order %s not found", id)

View file

@ -102,16 +102,16 @@ func TestDB_getDBOrder(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if dbo, err := d.getDBOrder(context.Background(), orderID); err != nil { if dbo, err := d.getDBOrder(context.Background(), orderID); err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -206,16 +206,16 @@ func TestDB_GetOrder(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if o, err := d.GetOrder(context.Background(), orderID); err != nil { if o, err := d.GetOrder(context.Background(), orderID); err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -1003,16 +1003,16 @@ func TestDB_updateAddOrderIDs(t *testing.T) {
} }
if err != nil { if err != nil {
var ae *acme.Error switch k := err.(type) {
if errors.As(err, &ae) { case *acme.Error:
if assert.NotNil(t, tc.acmeErr) { if assert.NotNil(t, tc.acmeErr) {
assert.Equals(t, ae.Type, tc.acmeErr.Type) assert.Equals(t, k.Type, tc.acmeErr.Type)
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
assert.Equals(t, ae.Status, tc.acmeErr.Status) assert.Equals(t, k.Status, tc.acmeErr.Status)
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
assert.Equals(t, ae.Detail, tc.acmeErr.Detail) assert.Equals(t, k.Detail, tc.acmeErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }

View file

@ -65,8 +65,6 @@ const (
ErrorUserActionRequiredType ErrorUserActionRequiredType
// ErrorNotImplementedType operation is not implemented // ErrorNotImplementedType operation is not implemented
ErrorNotImplementedType ErrorNotImplementedType
// ErrorNNSType was a problem with a NNS query during identifier validation
ErrorNNSType
) )
// String returns the string representation of the acme problem type, // String returns the string representation of the acme problem type,
@ -77,8 +75,6 @@ func (ap ProblemType) String() string {
return "accountDoesNotExist" return "accountDoesNotExist"
case ErrorAlreadyRevokedType: case ErrorAlreadyRevokedType:
return "alreadyRevoked" return "alreadyRevoked"
case ErrorBadAttestationStatementType:
return "badAttestationStatement"
case ErrorBadCSRType: case ErrorBadCSRType:
return "badCSR" return "badCSR"
case ErrorBadNonceType: case ErrorBadNonceType:
@ -123,8 +119,6 @@ func (ap ProblemType) String() string {
return "userActionRequired" return "userActionRequired"
case ErrorNotImplementedType: case ErrorNotImplementedType:
return "notImplemented" return "notImplemented"
case ErrorNNSType:
return "nns"
default: default:
return fmt.Sprintf("unsupported type ACME error type '%d'", int(ap)) return fmt.Sprintf("unsupported type ACME error type '%d'", int(ap))
} }
@ -274,34 +268,14 @@ var (
} }
) )
// Error represents an ACME Error // Error represents an ACME
type Error struct { type Error struct {
Type string `json:"type"` Type string `json:"type"`
Detail string `json:"detail"` Detail string `json:"detail"`
Subproblems []Subproblem `json:"subproblems,omitempty"` Subproblems []interface{} `json:"subproblems,omitempty"`
Err error `json:"-"` Identifier interface{} `json:"identifier,omitempty"`
Status int `json:"-"` 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. // NewError creates a new Error type.
@ -309,26 +283,6 @@ func NewError(pt ProblemType, msg string, args ...interface{}) *Error {
return newError(pt, errors.Errorf(msg, args...)) 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 { func newError(pt ProblemType, err error) *Error {
meta, ok := errorMap[pt] meta, ok := errorMap[pt]
if !ok { if !ok {
@ -356,11 +310,10 @@ func NewErrorISE(msg string, args ...interface{}) *Error {
// WrapError attempts to wrap the internal error. // WrapError attempts to wrap the internal error.
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error { func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
var e *Error switch e := err.(type) {
switch { case nil:
case err == nil:
return nil return nil
case errors.As(err, &e): case *Error:
if e.Err == nil { if e.Err == nil {
e.Err = errors.Errorf(msg+"; "+e.Detail, args...) e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
} else { } else {

View file

@ -1,122 +0,0 @@
package acme
import (
"context"
"errors"
"fmt"
"net/url"
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// multiSchemeClient unites invoker.RPCInvoke and common interface of
// rpcclient.Client and rpcclient.WSClient.
type multiSchemeClient interface {
invoker.RPCInvoke
// Init turns client to "ready-to-work" state.
Init() error
// Close closes connections.
Close()
// GetContractStateByID returns state of the NNS contract on 1 input.
GetContractStateByID(int32) (*state.Contract, error)
}
// NNS is used to interact with NNS contract.
// Before work, the connection to the NNS server must be established using Dial method.
type NNS struct {
nnsContract util.Uint160
client multiSchemeClient
}
// NNSContext is used to store info about NNS server.
type NNSContext struct {
nnsServer string
}
type nnsKey struct{}
// NewNNSContext adds new NNSContext with given params to the context.
func NewNNSContext(ctx context.Context, nnsServer string) context.Context {
return context.WithValue(ctx, nnsKey{}, NNSContext{nnsServer: nnsServer})
}
// GetNNSContext returns NNSContext from the given context.
func GetNNSContext(ctx context.Context) (NNSContext, bool) {
c, ok := ctx.Value(nnsKey{}).(NNSContext)
return c, ok
}
// Dial connects to the address of the NNS server.
// If URL address scheme is 'ws' or 'wss', then WebSocket protocol is used, otherwise HTTP.
func (n *NNS) Dial(address string) error {
var err error
uri, err := url.Parse(address)
if err == nil && (uri.Scheme == "ws" || uri.Scheme == "wss") {
n.client, err = rpcclient.NewWS(context.Background(), address, rpcclient.WSOptions{})
if err != nil {
return fmt.Errorf("create Neo WebSocket client: %w", err)
}
} else {
n.client, err = rpcclient.New(context.Background(), address, rpcclient.Options{})
if err != nil {
return fmt.Errorf("create Neo HTTP client: %w", err)
}
}
if err = n.client.Init(); err != nil {
return fmt.Errorf("initialize Neo client: %w", err)
}
nnsContract, err := n.client.GetContractStateByID(1)
if err != nil {
return fmt.Errorf("get NNS contract state: %w", err)
}
n.nnsContract = nnsContract.Hash
return nil
}
// Close closes connections of multiSchemeClient.
func (n *NNS) Close() {
n.client.Close()
}
// GetTXTRecords returns TXT records of the provided domain by calling `getRecords` method of NNS contract.
func (n *NNS) GetTXTRecords(name string) ([]string, error) {
params, err := smartcontract.NewParametersFromValues(name, int64(nns.TXT))
if err != nil {
return make([]string, 0), fmt.Errorf("create slice of params: %w", err)
}
item, err := unwrap.Item(n.client.InvokeFunction(n.nnsContract, "getRecords", params, nil))
if err != nil {
return make([]string, 0), fmt.Errorf("contract invocation: %w", err)
}
if _, ok := item.(stackitem.Null); !ok {
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return make([]string, 0), errors.New("invalid cast to stack item slice")
}
var result = make([]string, 0, len(arr))
for i := range arr {
recordValue, err := arr[i].TryBytes()
if err != nil {
return make([]string, 0), fmt.Errorf("convert array item to byte slice: %w", err)
}
result = append(result, string(recordValue))
}
return result, nil
}
return make([]string, 0), errors.New("records not found")
}

View file

@ -3,7 +3,6 @@ package acme
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/subtle"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"net" "net"
@ -12,7 +11,6 @@ import (
"time" "time"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/x509util" "go.step.sm/crypto/x509util"
) )
@ -127,27 +125,6 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
return nil 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 // Finalize signs a certificate if the necessary conditions for Order completion
// have been met. // have been met.
// //
@ -173,24 +150,6 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID) 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 // canonicalize the CSR to allow for comparison
csr = canonicalize(csr) csr = canonicalize(csr)
@ -206,15 +165,6 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
for i := range o.Identifiers { for i := range o.Identifiers {
if o.Identifiers[i].Type == PermanentIdentifier { if o.Identifiers[i].Type == PermanentIdentifier {
permanentIdentifier = o.Identifiers[i].Value 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 break
} }
} }
@ -244,14 +194,6 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
if err != nil { if err != nil {
return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner") return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner")
} }
// Unlike most of the provisioners, ACME's AuthorizeSign method doesn't
// define the templates, and the template data used in WebHooks is not
// available.
for _, signOp := range signOps {
if wc, ok := signOp.(*provisioner.WebhookController); ok {
wc.TemplateData = data
}
}
templateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate) templateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate)
if err != nil { if err != nil {
@ -382,6 +324,7 @@ func numberOfIdentifierType(typ IdentifierType, ids []Identifier) int {
// addresses or DNS names slice, depending on whether it can be parsed as an IP // addresses or DNS names slice, depending on whether it can be parsed as an IP
// or not. This might result in an additional SAN in the final certificate. // or not. This might result in an additional SAN in the final certificate.
func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) { func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) {
// for clarity only; we're operating on the same object by pointer // for clarity only; we're operating on the same object by pointer
canonicalized = csr canonicalized = csr

View file

@ -2,12 +2,9 @@ package acme
import ( import (
"context" "context"
"crypto"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/asn1"
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"net/url" "net/url"
"reflect" "reflect"
@ -19,7 +16,6 @@ import (
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/x509util" "go.step.sm/crypto/x509util"
) )
@ -251,14 +247,14 @@ func TestOrder_UpdateStatus(t *testing.T) {
tc := run(t) tc := run(t)
if err := tc.o.UpdateStatus(context.Background(), tc.db); err != nil { if err := tc.o.UpdateStatus(context.Background(), tc.db); err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var k *Error switch k := err.(type) {
if errors.As(err, &k) { case *Error:
assert.Equals(t, k.Type, tc.err.Type) assert.Equals(t, k.Type, tc.err.Type)
assert.Equals(t, k.Detail, tc.err.Detail) assert.Equals(t, k.Detail, tc.err.Detail)
assert.Equals(t, k.Status, tc.err.Status) assert.Equals(t, k.Status, tc.err.Status)
assert.Equals(t, k.Err.Error(), tc.err.Err.Error()) assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
assert.Equals(t, k.Detail, tc.err.Detail) assert.Equals(t, k.Detail, tc.err.Detail)
} else { default:
assert.FatalError(t, errors.New("unexpected error type")) assert.FatalError(t, errors.New("unexpected error type"))
} }
} }
@ -301,7 +297,7 @@ func (m *mockSignAuth) LoadProvisionerByName(name string) (provisioner.Interface
return m.ret1.(provisioner.Interface), m.err return m.ret1.(provisioner.Interface), m.err
} }
func (m *mockSignAuth) IsRevoked(string) (bool, error) { func (m *mockSignAuth) IsRevoked(sn string) (bool, error) {
return false, nil return false, nil
} }
@ -310,14 +306,6 @@ func (m *mockSignAuth) Revoke(context.Context, *authority.RevokeOptions) error {
} }
func TestOrder_Finalize(t *testing.T) { 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 { type test struct {
o *Order o *Order
err *Error err *Error
@ -398,72 +386,6 @@ func TestOrder_Finalize(t *testing.T) {
err: NewErrorISE("unrecognized order status: %s", o.Status), 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 { "fail/error-provisioner-auth": func(t *testing.T) test {
now := clock.Now() now := clock.Now()
o := &Order{ o := &Order{
@ -493,11 +415,6 @@ func TestOrder_Finalize(t *testing.T) {
return nil, errors.New("force") 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"), err: NewErrorISE("error retrieving authorization options from ACME provisioner: force"),
} }
}, },
@ -537,11 +454,6 @@ 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')"), err: NewErrorISE("error creating template options from ACME provisioner: error unmarshaling template data: invalid character 'o' in literal false (expecting 'a')"),
} }
}, },
@ -583,11 +495,6 @@ func TestOrder_Finalize(t *testing.T) {
return nil, errors.New("force") 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"), err: NewErrorISE("error signing certificate for order oID: force"),
} }
}, },
@ -634,9 +541,6 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ 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 { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID) assert.Equals(t, cert.OrderID, o.ID)
@ -691,9 +595,6 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ 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 { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID" cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
@ -716,297 +617,6 @@ func TestOrder_Finalize(t *testing.T) {
err: NewErrorISE("error updating order oID: force"), 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 { "ok/new-cert-dns": func(t *testing.T) test {
now := clock.Now() now := clock.Now()
o := &Order{ o := &Order{
@ -1050,9 +660,6 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ 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 { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID" cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
@ -1114,9 +721,6 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ 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 { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID" cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
@ -1181,9 +785,6 @@ func TestOrder_Finalize(t *testing.T) {
}, },
}, },
db: &MockDB{ 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 { MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID" cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID) assert.Equals(t, cert.AccountID, o.AccountID)
@ -1211,14 +812,14 @@ func TestOrder_Finalize(t *testing.T) {
tc := run(t) tc := run(t)
if err := tc.o.Finalize(context.Background(), tc.db, tc.csr, tc.ca, tc.prov); err != nil { if err := tc.o.Finalize(context.Background(), tc.db, tc.csr, tc.ca, tc.prov); err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var k *Error switch k := err.(type) {
if errors.As(err, &k) { case *Error:
assert.Equals(t, k.Type, tc.err.Type) assert.Equals(t, k.Type, tc.err.Type)
assert.Equals(t, k.Detail, tc.err.Detail) assert.Equals(t, k.Detail, tc.err.Detail)
assert.Equals(t, k.Status, tc.err.Status) assert.Equals(t, k.Status, tc.err.Status)
assert.Equals(t, k.Err.Error(), tc.err.Err.Error()) assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
assert.Equals(t, k.Detail, tc.err.Detail) assert.Equals(t, k.Detail, tc.err.Detail)
} else { default:
assert.FatalError(t, errors.New("unexpected error type")) assert.FatalError(t, errors.New("unexpected error type"))
} }
} }
@ -1873,14 +1474,14 @@ func TestOrder_sans(t *testing.T) {
t.Errorf("Order.sans() = %v, want error; got none", got) t.Errorf("Order.sans() = %v, want error; got none", got)
return return
} }
var k *Error switch k := err.(type) {
if errors.As(err, &k) { case *Error:
assert.Equals(t, k.Type, tt.err.Type) assert.Equals(t, k.Type, tt.err.Type)
assert.Equals(t, k.Detail, tt.err.Detail) assert.Equals(t, k.Detail, tt.err.Detail)
assert.Equals(t, k.Status, tt.err.Status) assert.Equals(t, k.Status, tt.err.Status)
assert.Equals(t, k.Err.Error(), tt.err.Err.Error()) assert.Equals(t, k.Err.Error(), tt.err.Err.Error())
assert.Equals(t, k.Detail, tt.err.Detail) assert.Equals(t, k.Detail, tt.err.Detail)
} else { default:
assert.FatalError(t, errors.New("unexpected error type")) assert.FatalError(t, errors.New("unexpected error type"))
} }
return return
@ -1891,55 +1492,3 @@ 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)
}
})
}
}

View file

@ -1,10 +1,9 @@
package api package api
import ( import (
"bytes"
"context" "context"
"crypto" "crypto"
"crypto/dsa" //nolint:staticcheck // support legacy algorithms "crypto/dsa" //nolint
"crypto/ecdsa" "crypto/ecdsa"
"crypto/ed25519" "crypto/ed25519"
"crypto/rsa" "crypto/rsa"
@ -21,8 +20,6 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.step.sm/crypto/sshutil"
"golang.org/x/crypto/ssh"
"github.com/smallstep/certificates/api/log" "github.com/smallstep/certificates/api/log"
"github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/api/render"
@ -43,7 +40,6 @@ type Authority interface {
Root(shasum string) (*x509.Certificate, error) Root(shasum string) (*x509.Certificate, error)
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
Renew(peer *x509.Certificate) ([]*x509.Certificate, error) Renew(peer *x509.Certificate) ([]*x509.Certificate, error)
RenewContext(ctx context.Context, peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
Rekey(peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) Rekey(peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error) LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
LoadProvisionerByName(string) (provisioner.Interface, error) LoadProvisionerByName(string) (provisioner.Interface, error)
@ -53,7 +49,6 @@ type Authority interface {
GetRoots() ([]*x509.Certificate, error) GetRoots() ([]*x509.Certificate, error)
GetFederation() ([]*x509.Certificate, error) GetFederation() ([]*x509.Certificate, error)
Version() authority.Version Version() authority.Version
GetCertificateRevocationList() ([]byte, error)
} }
// mustAuthority will be replaced on unit tests. // mustAuthority will be replaced on unit tests.
@ -227,39 +222,8 @@ type RootResponse struct {
// ProvisionersResponse is the response object that returns the list of // ProvisionersResponse is the response object that returns the list of
// provisioners. // provisioners.
type ProvisionersResponse struct { type ProvisionersResponse struct {
Provisioners provisioner.List Provisioners provisioner.List `json:"provisioners"`
NextCursor string NextCursor string `json:"nextCursor"`
}
// MarshalJSON implements json.Marshaler. It marshals the ProvisionersResponse
// into a byte slice.
//
// Special treatment is given to the SCEP provisioner, as it contains a
// challenge secret that MUST NOT be leaked in (public) HTTP responses. The
// challenge value is thus redacted in HTTP responses.
func (p ProvisionersResponse) MarshalJSON() ([]byte, error) {
for _, item := range p.Provisioners {
scepProv, ok := item.(*provisioner.SCEP)
if !ok {
continue
}
old := scepProv.ChallengePassword
scepProv.ChallengePassword = "*** REDACTED ***"
defer func(p string) { //nolint:gocritic // defer in loop required to restore initial state of provisioners
scepProv.ChallengePassword = p
}(old)
}
var list = struct {
Provisioners []provisioner.Interface `json:"provisioners"`
NextCursor string `json:"nextCursor"`
}{
Provisioners: []provisioner.Interface(p.Provisioners),
NextCursor: p.NextCursor,
}
return json.Marshal(list)
} }
// ProvisionerKeyResponse is the response object that returns the encrypted key // ProvisionerKeyResponse is the response object that returns the encrypted key
@ -291,7 +255,7 @@ func (h *caHandler) Route(r Router) {
// New creates a new RouterHandler with the CA endpoints. // New creates a new RouterHandler with the CA endpoints.
// //
// Deprecated: Use api.Route(r Router) // Deprecated: Use api.Route(r Router)
func New(Authority) RouterHandler { func New(auth Authority) RouterHandler {
return &caHandler{} return &caHandler{}
} }
@ -303,7 +267,6 @@ func Route(r Router) {
r.MethodFunc("POST", "/renew", Renew) r.MethodFunc("POST", "/renew", Renew)
r.MethodFunc("POST", "/rekey", Rekey) r.MethodFunc("POST", "/rekey", Rekey)
r.MethodFunc("POST", "/revoke", Revoke) r.MethodFunc("POST", "/revoke", Revoke)
r.MethodFunc("GET", "/crl", CRL)
r.MethodFunc("GET", "/provisioners", Provisioners) r.MethodFunc("GET", "/provisioners", Provisioners)
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey) r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
r.MethodFunc("GET", "/roots", Roots) r.MethodFunc("GET", "/roots", Roots)
@ -338,7 +301,7 @@ func Version(w http.ResponseWriter, r *http.Request) {
} }
// Health is an HTTP handler that returns the status of the server. // Health is an HTTP handler that returns the status of the server.
func Health(w http.ResponseWriter, _ *http.Request) { func Health(w http.ResponseWriter, r *http.Request) {
render.JSON(w, HealthResponse{Status: "ok"}) render.JSON(w, HealthResponse{Status: "ok"})
} }
@ -472,7 +435,7 @@ func logOtt(w http.ResponseWriter, token string) {
} }
} }
// LogCertificate adds certificate fields to the log message. // LogCertificate add certificate fields to the log message.
func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) { func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) {
if rl, ok := w.(logging.ResponseLogger); ok { if rl, ok := w.(logging.ResponseLogger); ok {
m := map[string]interface{}{ m := map[string]interface{}{
@ -504,41 +467,6 @@ func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) {
} }
} }
// LogSSHCertificate adds SSH certificate fields to the log message.
func LogSSHCertificate(w http.ResponseWriter, cert *ssh.Certificate) {
if rl, ok := w.(logging.ResponseLogger); ok {
mak := bytes.TrimSpace(ssh.MarshalAuthorizedKey(cert))
var certificate string
parts := strings.Split(string(mak), " ")
if len(parts) > 1 {
certificate = parts[1]
}
var userOrHost string
if cert.CertType == ssh.HostCert {
userOrHost = "host"
} else {
userOrHost = "user"
}
certificateType := fmt.Sprintf("%s %s certificate", parts[0], userOrHost) // e.g. ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate
m := map[string]interface{}{
"serial": cert.Serial,
"principals": cert.ValidPrincipals,
"valid-from": time.Unix(int64(cert.ValidAfter), 0).Format(time.RFC3339),
"valid-to": time.Unix(int64(cert.ValidBefore), 0).Format(time.RFC3339),
"certificate": certificate,
"certificate-type": certificateType,
}
fingerprint, err := sshutil.FormatFingerprint(mak, sshutil.DefaultFingerprint)
if err == nil {
fpParts := strings.Split(fingerprint, " ")
if len(fpParts) > 3 {
m["public-key"] = fmt.Sprintf("%s %s", fpParts[1], fpParts[len(fpParts)-1])
}
}
rl.WithFields(m)
}
}
// ParseCursor parses the cursor and limit from the request query params. // ParseCursor parses the cursor and limit from the request query params.
func ParseCursor(r *http.Request) (cursor string, limit int, err error) { func ParseCursor(r *http.Request) (cursor string, limit int, err error) {
q := r.URL.Query() q := r.URL.Query()

View file

@ -4,7 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"crypto" "crypto"
"crypto/dsa" //nolint:staticcheck // support legacy algorithms "crypto/dsa" //nolint
"crypto/ecdsa" "crypto/ecdsa"
"crypto/ed25519" "crypto/ed25519"
"crypto/elliptic" "crypto/elliptic"
@ -28,15 +28,12 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/pkg/errors" "github.com/pkg/errors"
sassert "github.com/stretchr/testify/assert" "golang.org/x/crypto/ssh"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/x509util" "go.step.sm/crypto/x509util"
"golang.org/x/crypto/ssh"
squarejose "gopkg.in/square/go-jose.v2"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/errs"
@ -195,7 +192,6 @@ type mockAuthority struct {
sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
renew func(cert *x509.Certificate) ([]*x509.Certificate, error) renew func(cert *x509.Certificate) ([]*x509.Certificate, error)
rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
renewContext func(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error) loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error)
loadProvisionerByName func(name string) (provisioner.Interface, error) loadProvisionerByName func(name string) (provisioner.Interface, error)
getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error) getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
@ -203,7 +199,6 @@ type mockAuthority struct {
getEncryptedKey func(kid string) (string, error) getEncryptedKey func(kid string) (string, error)
getRoots func() ([]*x509.Certificate, error) getRoots func() ([]*x509.Certificate, error)
getFederation func() ([]*x509.Certificate, error) getFederation func() ([]*x509.Certificate, error)
getCRL func() ([]byte, error)
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
@ -217,14 +212,6 @@ type mockAuthority struct {
version func() authority.Version version func() authority.Version
} }
func (m *mockAuthority) GetCertificateRevocationList() ([]byte, error) {
if m.getCRL != nil {
return m.getCRL()
}
return m.ret1.([]byte), m.err
}
// TODO: remove once Authorize is deprecated. // TODO: remove once Authorize is deprecated.
func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
if m.authorize != nil { if m.authorize != nil {
@ -268,13 +255,6 @@ func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, erro
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
} }
func (m *mockAuthority) RenewContext(ctx context.Context, oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
if m.renewContext != nil {
return m.renewContext(ctx, oldcert, pk)
}
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
}
func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) { func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
if m.rekey != nil { if m.rekey != nil {
return m.rekey(oldcert, pk) return m.rekey(oldcert, pk)
@ -792,45 +772,6 @@ func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) (
return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err
} }
func Test_CRLGeneration(t *testing.T) {
tests := []struct {
name string
err error
statusCode int
expected []byte
}{
{"empty", nil, http.StatusOK, nil},
}
chiCtx := chi.NewRouteContext()
req := httptest.NewRequest("GET", "http://example.com/crl", nil)
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockMustAuthority(t, &mockAuthority{ret1: tt.expected, err: tt.err})
w := httptest.NewRecorder()
CRL(w, req)
res := w.Result()
if res.StatusCode != tt.statusCode {
t.Errorf("caHandler.CRL StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Errorf("caHandler.Root unexpected error = %v", err)
}
if tt.statusCode == 200 {
if !bytes.Equal(bytes.TrimSpace(body), tt.expected) {
t.Errorf("caHandler.Root CRL = %s, wants %s", body, tt.expected)
}
}
})
}
}
func Test_caHandler_Route(t *testing.T) { func Test_caHandler_Route(t *testing.T) {
type fields struct { type fields struct {
Authority Authority Authority Authority
@ -1567,122 +1508,3 @@ func mustCertificate(t *testing.T, pub, priv interface{}) *x509.Certificate {
} }
return cert return cert
} }
func TestProvisionersResponse_MarshalJSON(t *testing.T) {
k := map[string]any{
"use": "sig",
"kty": "EC",
"kid": "4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc",
"crv": "P-256",
"alg": "ES256",
"x": "7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8",
"y": "sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y",
}
key := squarejose.JSONWebKey{}
b, err := json.Marshal(k)
assert.FatalError(t, err)
err = json.Unmarshal(b, &key)
assert.FatalError(t, err)
r := ProvisionersResponse{
Provisioners: provisioner.List{
&provisioner.SCEP{
Name: "scep",
Type: "scep",
ChallengePassword: "not-so-secret",
MinimumPublicKeyLength: 2048,
EncryptionAlgorithmIdentifier: 2,
},
&provisioner.JWK{
EncryptedKey: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg",
Key: &key,
Name: "step-cli",
Type: "JWK",
},
},
NextCursor: "next",
}
expected := map[string]any{
"provisioners": []map[string]any{
{
"type": "scep",
"name": "scep",
"challenge": "*** REDACTED ***",
"minimumPublicKeyLength": 2048,
"encryptionAlgorithmIdentifier": 2,
},
{
"type": "JWK",
"name": "step-cli",
"key": map[string]any{
"use": "sig",
"kty": "EC",
"kid": "4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc",
"crv": "P-256",
"alg": "ES256",
"x": "7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8",
"y": "sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y",
},
"encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg",
},
},
"nextCursor": "next",
}
expBytes, err := json.Marshal(expected)
sassert.NoError(t, err)
br, err := r.MarshalJSON()
sassert.NoError(t, err)
sassert.JSONEq(t, string(expBytes), string(br))
keyCopy := key
expList := provisioner.List{
&provisioner.SCEP{
Name: "scep",
Type: "scep",
ChallengePassword: "not-so-secret",
MinimumPublicKeyLength: 2048,
EncryptionAlgorithmIdentifier: 2,
},
&provisioner.JWK{
EncryptedKey: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg",
Key: &keyCopy,
Name: "step-cli",
Type: "JWK",
},
}
// MarshalJSON must not affect the struct properties itself
sassert.Equal(t, expList, r.Provisioners)
}
const (
fixtureECDSACertificate = `ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI herman`
)
func TestLogSSHCertificate(t *testing.T) {
out, _, _, _, err := ssh.ParseAuthorizedKey([]byte(fixtureECDSACertificate))
require.NoError(t, err)
cert, ok := out.(*ssh.Certificate)
require.True(t, ok)
w := httptest.NewRecorder()
rl := logging.NewResponseLogger(w)
LogSSHCertificate(rl, cert)
sassert.Equal(t, 200, w.Result().StatusCode)
fields := rl.Fields()
sassert.Equal(t, uint64(14376510277651266987), fields["serial"])
sassert.Equal(t, []string{"herman"}, fields["principals"])
sassert.Equal(t, "ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate", fields["certificate-type"])
sassert.Equal(t, time.Unix(1674129191, 0).Format(time.RFC3339), fields["valid-from"])
sassert.Equal(t, time.Unix(1674186851, 0).Format(time.RFC3339), fields["valid-to"])
sassert.Equal(t, "AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"])
sassert.Equal(t, "SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 (ECDSA-CERT)", fields["public-key"])
}

View file

@ -1,32 +0,0 @@
package api
import (
"encoding/pem"
"net/http"
"github.com/smallstep/certificates/api/render"
)
// CRL is an HTTP handler that returns the current CRL in DER or PEM format
func CRL(w http.ResponseWriter, r *http.Request) {
crlBytes, err := mustAuthority(r.Context()).GetCertificateRevocationList()
if err != nil {
render.Error(w, err)
return
}
_, formatAsPEM := r.URL.Query()["pem"]
if formatAsPEM {
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,
})
} else {
w.Header().Add("Content-Type", "application/pkix-crl")
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"")
w.Write(crlBytes)
}
}

View file

@ -7,6 +7,8 @@ import (
"os" "os"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/logging"
) )
// StackTracedError is the set of errors implementing the StackTrace function. // StackTracedError is the set of errors implementing the StackTrace function.
@ -19,21 +21,16 @@ type StackTracedError interface {
StackTrace() errors.StackTrace StackTrace() errors.StackTrace
} }
type fieldCarrier interface {
WithFields(map[string]any)
Fields() map[string]any
}
// Error adds to the response writer the given error if it implements // Error adds to the response writer the given error if it implements
// logging.ResponseLogger. If it does not implement it, then writes the error // logging.ResponseLogger. If it does not implement it, then writes the error
// using the log package. // using the log package.
func Error(rw http.ResponseWriter, err error) { func Error(rw http.ResponseWriter, err error) {
fc, ok := rw.(fieldCarrier) rl, ok := rw.(logging.ResponseLogger)
if !ok { if !ok {
return return
} }
fc.WithFields(map[string]any{ rl.WithFields(map[string]interface{}{
"error": err, "error": err,
}) })
@ -41,19 +38,23 @@ func Error(rw http.ResponseWriter, err error) {
return return
} }
var st StackTracedError e, ok := err.(StackTracedError)
if errors.As(err, &st) { if !ok {
fc.WithFields(map[string]any{ e, ok = errors.Cause(err).(StackTracedError)
"stack-trace": fmt.Sprintf("%+v", st.StackTrace()), }
if ok {
rl.WithFields(map[string]interface{}{
"stack-trace": fmt.Sprintf("%+v", e.StackTrace()),
}) })
} }
} }
// EnabledResponse log the response object if it implements the EnableLogger // EnabledResponse log the response object if it implements the EnableLogger
// interface. // interface.
func EnabledResponse(rw http.ResponseWriter, v any) { func EnabledResponse(rw http.ResponseWriter, v interface{}) {
type enableLogger interface { type enableLogger interface {
ToLog() (any, error) ToLog() (interface{}, error)
} }
if el, ok := v.(enableLogger); ok { if el, ok := v.(enableLogger); ok {
@ -64,8 +65,8 @@ func EnabledResponse(rw http.ResponseWriter, v any) {
return return
} }
if rl, ok := rw.(fieldCarrier); ok { if rl, ok := rw.(logging.ResponseLogger); ok {
rl.WithFields(map[string]any{ rl.WithFields(map[string]interface{}{
"response": out, "response": out,
}) })
} }

View file

@ -1,78 +1,43 @@
package log package log
import ( import (
"errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"testing" "testing"
"unsafe"
pkgerrors "github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/smallstep/certificates/logging" "github.com/smallstep/certificates/logging"
) )
type stackTracedError struct{}
func (stackTracedError) Error() string {
return "a stacktraced error"
}
func (stackTracedError) StackTrace() pkgerrors.StackTrace {
f := struct{}{}
return pkgerrors.StackTrace{ // fake stacktrace
pkgerrors.Frame(unsafe.Pointer(&f)),
pkgerrors.Frame(unsafe.Pointer(&f)),
}
}
func TestError(t *testing.T) { func TestError(t *testing.T) {
theError := errors.New("the error")
type args struct {
rw http.ResponseWriter
err error
}
tests := []struct { tests := []struct {
name string name string
error args args
rw http.ResponseWriter withFields bool
isFieldCarrier bool
stepDebug bool
expectStackTrace bool
}{ }{
{"noLogger", nil, nil, false, false, false}, {"normalLogger", args{httptest.NewRecorder(), theError}, false},
{"noError", nil, logging.NewResponseLogger(httptest.NewRecorder()), true, false, false}, {"responseLogger", args{logging.NewResponseLogger(httptest.NewRecorder()), theError}, true},
{"noErrorDebug", nil, logging.NewResponseLogger(httptest.NewRecorder()), true, true, false},
{"anError", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), true, false, false},
{"anErrorDebug", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), true, true, false},
{"stackTracedError", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), true, true, true},
{"stackTracedErrorDebug", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), true, true, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if tt.stepDebug { Error(tt.args.rw, tt.args.err)
t.Setenv("STEPDEBUG", "1") if tt.withFields {
} else { if rl, ok := tt.args.rw.(logging.ResponseLogger); ok {
t.Setenv("STEPDEBUG", "0") fields := rl.Fields()
} if !reflect.DeepEqual(fields["error"], theError) {
t.Errorf("ResponseLogger[\"error\"] = %s, wants %s", fields["error"], theError)
Error(tt.rw, tt.error) }
} else {
// return early if test case doesn't use logger t.Error("ResponseWriter does not implement logging.ResponseLogger")
if !tt.isFieldCarrier { }
return
}
fields := tt.rw.(logging.ResponseLogger).Fields()
// expect the error field to be (not) set and to be the same error that was fed to Error
if tt.error == nil {
assert.Nil(t, fields["error"])
} else {
assert.Same(t, tt.error, fields["error"])
}
// check if stack-trace is set when expected
if _, hasStackTrace := fields["stack-trace"]; tt.expectStackTrace && !hasStackTrace {
t.Error(`ResponseLogger["stack-trace"] not set`)
} else if !tt.expectStackTrace && hasStackTrace {
t.Error(`ResponseLogger["stack-trace"] was set`)
} }
}) })
} }

View file

@ -41,8 +41,8 @@ func TestJSON(t *testing.T) {
} }
if tt.wantErr { if tt.wantErr {
var e *errs.Error e, ok := err.(*errs.Error)
if errors.As(err, &e) { if ok {
if code := e.StatusCode(); code != 400 { if code := e.StatusCode(); code != 400 {
t.Errorf("error.StatusCode() = %v, wants 400", code) t.Errorf("error.StatusCode() = %v, wants 400", code)
} }
@ -102,15 +102,14 @@ func TestProtoJSON(t *testing.T) {
} }
if tt.wantErr { if tt.wantErr {
var ( switch err.(type) {
ee *errs.Error case badProtoJSONError:
bpe badProtoJSONError
)
switch {
case errors.As(err, &bpe):
assert.Contains(t, err.Error(), "syntax error") assert.Contains(t, err.Error(), "syntax error")
case errors.As(err, &ee): case *errs.Error:
assert.Equal(t, http.StatusBadRequest, ee.Status) var ee *errs.Error
if errors.As(err, &ee) {
assert.Equal(t, http.StatusBadRequest, ee.Status)
}
} }
return return
} }

View file

@ -2,8 +2,8 @@
package render package render
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/protojson"
@ -23,25 +23,14 @@ func JSON(w http.ResponseWriter, v interface{}) {
// JSONStatus sets the Content-Type of w to application/json unless one is // JSONStatus sets the Content-Type of w to application/json unless one is
// specified. // specified.
func JSONStatus(w http.ResponseWriter, v interface{}, status int) { func JSONStatus(w http.ResponseWriter, v interface{}, status int) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(v); err != nil {
panic(err)
}
setContentTypeUnlessPresent(w, "application/json") setContentTypeUnlessPresent(w, "application/json")
w.WriteHeader(status) w.WriteHeader(status)
_, _ = b.WriteTo(w)
if err := json.NewEncoder(w).Encode(v); err != nil {
var errUnsupportedType *json.UnsupportedTypeError
if errors.As(err, &errUnsupportedType) {
panic(err)
}
var errUnsupportedValue *json.UnsupportedValueError
if errors.As(err, &errUnsupportedValue) {
panic(err)
}
var errMarshalError *json.MarshalerError
if errors.As(err, &errMarshalError) {
panic(err)
}
}
log.EnabledResponse(w, v) log.EnabledResponse(w, v)
} }
@ -88,9 +77,8 @@ type RenderableError interface {
func Error(w http.ResponseWriter, err error) { func Error(w http.ResponseWriter, err error) {
log.Error(w, err) log.Error(w, err)
var r RenderableError if e, ok := err.(RenderableError); ok {
if errors.As(err, &r) { e.Render(w)
r.Render(w)
return return
} }
@ -117,18 +105,17 @@ func statusCodeFromError(err error) (code int) {
} }
for err != nil { for err != nil {
var sc StatusCodedError if sc, ok := err.(StatusCodedError); ok {
if errors.As(err, &sc) {
code = sc.StatusCode() code = sc.StatusCode()
break break
} }
var c causer cause, ok := err.(causer)
if !errors.As(err, &c) { if !ok {
break break
} }
err = c.Cause() err = cause.Cause()
} }
return return

View file

@ -1,10 +1,8 @@
package render package render
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"math"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strconv" "strconv"
@ -28,43 +26,10 @@ func TestJSON(t *testing.T) {
assert.Empty(t, rw.Fields()) assert.Empty(t, rw.Fields())
} }
func TestJSONPanicsOnUnsupportedType(t *testing.T) { func TestJSONPanics(t *testing.T) {
jsonPanicTest[json.UnsupportedTypeError](t, make(chan struct{})) assert.Panics(t, func() {
} JSON(httptest.NewRecorder(), make(chan struct{}))
})
func TestJSONPanicsOnUnsupportedValue(t *testing.T) {
jsonPanicTest[json.UnsupportedValueError](t, math.NaN())
}
func TestJSONPanicsOnMarshalerError(t *testing.T) {
var v erroneousJSONMarshaler
jsonPanicTest[json.MarshalerError](t, v)
}
type erroneousJSONMarshaler struct{}
func (erroneousJSONMarshaler) MarshalJSON() ([]byte, error) {
return nil, assert.AnError
}
func jsonPanicTest[T json.UnsupportedTypeError | json.UnsupportedValueError | json.MarshalerError](t *testing.T, v any) {
t.Helper()
defer func() {
var err error
if r := recover(); r == nil {
t.Fatal("expected panic")
} else if e, ok := r.(error); !ok {
t.Fatalf("did not panic with an error (%T)", r)
} else {
err = e
}
var e *T
assert.ErrorAs(t, err, &e)
}()
JSON(httptest.NewRecorder(), v)
} }
type renderableError struct { type renderableError struct {

View file

@ -6,7 +6,6 @@ import (
"strings" "strings"
"github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/errs"
) )
@ -18,22 +17,14 @@ const (
// Renew uses the information of certificate in the TLS connection to create a // Renew uses the information of certificate in the TLS connection to create a
// new one. // new one.
func Renew(w http.ResponseWriter, r *http.Request) { func Renew(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() cert, err := getPeerCertificate(r)
// Get the leaf certificate from the peer or the token.
cert, token, err := getPeerCertificate(r)
if err != nil { if err != nil {
render.Error(w, err) render.Error(w, err)
return return
} }
// The token can be used by RAs to renew a certificate. a := mustAuthority(r.Context())
if token != "" { certChain, err := a.Renew(cert)
ctx = authority.NewTokenContext(ctx, token)
}
a := mustAuthority(ctx)
certChain, err := a.RenewContext(ctx, cert, nil)
if err != nil { if err != nil {
render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew")) render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
return return
@ -53,16 +44,15 @@ func Renew(w http.ResponseWriter, r *http.Request) {
}, http.StatusCreated) }, http.StatusCreated)
} }
func getPeerCertificate(r *http.Request) (*x509.Certificate, string, error) { func getPeerCertificate(r *http.Request) (*x509.Certificate, error) {
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
return r.TLS.PeerCertificates[0], "", nil return r.TLS.PeerCertificates[0], nil
} }
if s := r.Header.Get(authorizationHeader); s != "" { if s := r.Header.Get(authorizationHeader); s != "" {
if parts := strings.SplitN(s, bearerScheme+" ", 2); len(parts) == 2 { if parts := strings.SplitN(s, bearerScheme+" ", 2); len(parts) == 2 {
ctx := r.Context() ctx := r.Context()
peer, err := mustAuthority(ctx).AuthorizeRenewToken(ctx, parts[1]) return mustAuthority(ctx).AuthorizeRenewToken(ctx, parts[1])
return peer, parts[1], err
} }
} }
return nil, "", errs.BadRequest("missing client certificate") return nil, errs.BadRequest("missing client certificate")
} }

View file

@ -62,12 +62,12 @@ func TestRevokeRequestValidate(t *testing.T) {
for name, tc := range tests { for name, tc := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
if err := tc.rr.Validate(); err != nil { if err := tc.rr.Validate(); err != nil {
var ee *errs.Error switch v := err.(type) {
if errors.As(err, &ee) { case *errs.Error:
assert.HasPrefix(t, ee.Error(), tc.err.Error()) assert.HasPrefix(t, v.Error(), tc.err.Error())
assert.Equals(t, ee.StatusCode(), tc.err.Status) assert.Equals(t, v.StatusCode(), tc.err.Status)
} else { default:
t.Errorf("unexpected error type: %T", err) t.Errorf("unexpected error type: %T", v)
} }
} else { } else {
assert.Nil(t, tc.err) assert.Nil(t, tc.err)

View file

@ -88,7 +88,6 @@ func Sign(w http.ResponseWriter, r *http.Request) {
if len(certChainPEM) > 1 { if len(certChainPEM) > 1 {
caPEM = certChainPEM[1] caPEM = certChainPEM[1]
} }
LogCertificate(w, certChain[0]) LogCertificate(w, certChain[0])
render.JSONStatus(w, &SignResponse{ render.JSONStatus(w, &SignResponse{
ServerPEM: certChainPEM[0], ServerPEM: certChainPEM[0],

View file

@ -338,7 +338,6 @@ func SSHSign(w http.ResponseWriter, r *http.Request) {
identityCertificate = certChainToPEM(certChain) identityCertificate = certChainToPEM(certChain)
} }
LogSSHCertificate(w, cert)
render.JSONStatus(w, &SSHSignResponse{ render.JSONStatus(w, &SSHSignResponse{
Certificate: SSHCertificate{cert}, Certificate: SSHCertificate{cert},
AddUserCertificate: addUserCertificate, AddUserCertificate: addUserCertificate,

View file

@ -89,7 +89,6 @@ func SSHRekey(w http.ResponseWriter, r *http.Request) {
return return
} }
LogSSHCertificate(w, newCert)
render.JSONStatus(w, &SSHRekeyResponse{ render.JSONStatus(w, &SSHRekeyResponse{
Certificate: SSHCertificate{newCert}, Certificate: SSHCertificate{newCert},
IdentityCertificate: identity, IdentityCertificate: identity,

View file

@ -81,7 +81,6 @@ func SSHRenew(w http.ResponseWriter, r *http.Request) {
return return
} }
LogSSHCertificate(w, newCert)
render.JSONStatus(w, &SSHSignResponse{ render.JSONStatus(w, &SSHSignResponse{
Certificate: SSHCertificate{newCert}, Certificate: SSHCertificate{newCert},
IdentityCertificate: identity, IdentityCertificate: identity,

View file

@ -69,21 +69,22 @@ func NewACMEAdminResponder() ACMEAdminResponder {
} }
// GetExternalAccountKeys writes the response for the EAB keys GET endpoint // GetExternalAccountKeys writes the response for the EAB keys GET endpoint
func (h *acmeAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, _ *http.Request) { func (h *acmeAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm")) render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
} }
// CreateExternalAccountKey writes the response for the EAB key POST endpoint // CreateExternalAccountKey writes the response for the EAB key POST endpoint
func (h *acmeAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, _ *http.Request) { func (h *acmeAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) {
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm")) render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
} }
// DeleteExternalAccountKey writes the response for the EAB key DELETE endpoint // DeleteExternalAccountKey writes the response for the EAB key DELETE endpoint
func (h *acmeAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, _ *http.Request) { func (h *acmeAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm")) render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
} }
func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey { func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey {
if k == nil { if k == nil {
return nil return nil
} }

View file

@ -229,13 +229,11 @@ func TestCreateAdminRequest_Validate(t *testing.T) {
if err != nil { if err != nil {
assert.Type(t, &admin.Error{}, err) assert.Type(t, &admin.Error{}, err)
var adminErr *admin.Error adminErr, _ := err.(*admin.Error)
if assert.True(t, errors.As(err, &adminErr)) { assert.Equals(t, tt.err.Type, adminErr.Type)
assert.Equals(t, tt.err.Type, adminErr.Type) assert.Equals(t, tt.err.Detail, adminErr.Detail)
assert.Equals(t, tt.err.Detail, adminErr.Detail) assert.Equals(t, tt.err.Status, adminErr.Status)
assert.Equals(t, tt.err.Status, adminErr.Status) assert.Equals(t, tt.err.Message, adminErr.Message)
assert.Equals(t, tt.err.Message, adminErr.Message)
}
} }
}) })
} }
@ -280,13 +278,11 @@ func TestUpdateAdminRequest_Validate(t *testing.T) {
if err != nil { if err != nil {
assert.Type(t, &admin.Error{}, err) assert.Type(t, &admin.Error{}, err)
var ae *admin.Error adminErr, _ := err.(*admin.Error)
if assert.True(t, errors.As(err, &ae)) { assert.Equals(t, tt.err.Type, adminErr.Type)
assert.Equals(t, tt.err.Type, ae.Type) assert.Equals(t, tt.err.Detail, adminErr.Detail)
assert.Equals(t, tt.err.Detail, ae.Detail) assert.Equals(t, tt.err.Status, adminErr.Status)
assert.Equals(t, tt.err.Status, ae.Status) assert.Equals(t, tt.err.Message, adminErr.Message)
assert.Equals(t, tt.err.Message, ae.Message)
}
} }
}) })
} }

View file

@ -4,47 +4,41 @@ import (
"context" "context"
"net/http" "net/http"
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/admin"
) )
// Handler is the Admin API request handler.
type Handler struct {
acmeResponder ACMEAdminResponder
policyResponder PolicyAdminResponder
}
// Route traffic and implement the Router interface.
//
// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder)
func (h *Handler) Route(r api.Router) {
Route(r, h.acmeResponder, h.policyResponder)
}
// NewHandler returns a new Authority Config Handler.
//
// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder)
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) api.RouterHandler {
return &Handler{
acmeResponder: acmeResponder,
policyResponder: policyResponder,
}
}
var mustAuthority = func(ctx context.Context) adminAuthority { var mustAuthority = func(ctx context.Context) adminAuthority {
return authority.MustFromContext(ctx) return authority.MustFromContext(ctx)
} }
type router struct {
acmeResponder ACMEAdminResponder
policyResponder PolicyAdminResponder
webhookResponder WebhookAdminResponder
}
type RouterOption func(*router)
func WithACMEResponder(acmeResponder ACMEAdminResponder) RouterOption {
return func(r *router) {
r.acmeResponder = acmeResponder
}
}
func WithPolicyResponder(policyResponder PolicyAdminResponder) RouterOption {
return func(r *router) {
r.policyResponder = policyResponder
}
}
func WithWebhookResponder(webhookResponder WebhookAdminResponder) RouterOption {
return func(r *router) {
r.webhookResponder = webhookResponder
}
}
// Route traffic and implement the Router interface. // Route traffic and implement the Router interface.
func Route(r api.Router, options ...RouterOption) { func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) {
router := &router{}
for _, fn := range options {
fn(router)
}
authnz := func(next http.HandlerFunc) http.HandlerFunc { authnz := func(next http.HandlerFunc) http.HandlerFunc {
return extractAuthorizeTokenAdmin(requireAPIEnabled(next)) return extractAuthorizeTokenAdmin(requireAPIEnabled(next))
} }
@ -73,10 +67,6 @@ func Route(r api.Router, options ...RouterOption) {
return authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next))))) return authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next)))))
} }
webhookMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
return authnz(loadProvisionerByName(next))
}
// Provisioners // Provisioners
r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner)) r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner))
r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners)) r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners))
@ -92,42 +82,36 @@ func Route(r api.Router, options ...RouterOption) {
r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin)) r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin))
// ACME responder // ACME responder
if router.acmeResponder != nil { if acmeResponder != nil {
// ACME External Account Binding Keys // ACME External Account Binding Keys
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys)) r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys)) r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.CreateExternalAccountKey)) r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.CreateExternalAccountKey))
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(router.acmeResponder.DeleteExternalAccountKey)) r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(acmeResponder.DeleteExternalAccountKey))
} }
// Policy responder // Policy responder
if router.policyResponder != nil { if policyResponder != nil {
// Policy - Authority // Policy - Authority
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(router.policyResponder.GetAuthorityPolicy)) r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(policyResponder.GetAuthorityPolicy))
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(router.policyResponder.CreateAuthorityPolicy)) r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(policyResponder.CreateAuthorityPolicy))
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(router.policyResponder.UpdateAuthorityPolicy)) r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(policyResponder.UpdateAuthorityPolicy))
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(router.policyResponder.DeleteAuthorityPolicy)) r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(policyResponder.DeleteAuthorityPolicy))
// Policy - Provisioner // Policy - Provisioner
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.GetProvisionerPolicy)) r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.GetProvisionerPolicy))
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.CreateProvisionerPolicy)) r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.CreateProvisionerPolicy))
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.UpdateProvisionerPolicy)) r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.UpdateProvisionerPolicy))
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.DeleteProvisionerPolicy)) r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.DeleteProvisionerPolicy))
// Policy - ACME Account // Policy - ACME Account
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy)) r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy)) r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy)) r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy)) r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy)) r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy)) r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy)) r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy)) r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
}
if router.webhookResponder != nil {
r.MethodFunc("POST", "/provisioners/{provisionerName}/webhooks", webhookMiddleware(router.webhookResponder.CreateProvisionerWebhook))
r.MethodFunc("PUT", "/provisioners/{provisionerName}/webhooks/{webhookName}", webhookMiddleware(router.webhookResponder.UpdateProvisionerWebhook))
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/webhooks/{webhookName}", webhookMiddleware(router.webhookResponder.DeleteProvisionerWebhook))
} }
} }

View file

@ -30,6 +30,7 @@ func requireAPIEnabled(next http.HandlerFunc) http.HandlerFunc {
// extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token. // extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token.
func extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc { func extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
tok := r.Header.Get("Authorization") tok := r.Header.Get("Authorization")
if tok == "" { if tok == "" {
render.Error(w, admin.NewError(admin.ErrorUnauthorizedType, render.Error(w, admin.NewError(admin.ErrorUnauthorizedType,

View file

@ -50,8 +50,7 @@ func (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *ht
auth := mustAuthority(ctx) auth := mustAuthority(ctx)
authorityPolicy, err := auth.GetAuthorityPolicy(r.Context()) authorityPolicy, err := auth.GetAuthorityPolicy(r.Context())
var ae *admin.Error if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy")) render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
return return
} }
@ -75,8 +74,7 @@ func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r
auth := mustAuthority(ctx) auth := mustAuthority(ctx)
authorityPolicy, err := auth.GetAuthorityPolicy(ctx) authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
var ae *admin.Error if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy")) render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
return return
} }
@ -127,8 +125,7 @@ func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r
auth := mustAuthority(ctx) auth := mustAuthority(ctx)
authorityPolicy, err := auth.GetAuthorityPolicy(ctx) authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
var ae *admin.Error if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy")) render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
return return
} }
@ -178,8 +175,7 @@ func (par *policyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r
auth := mustAuthority(ctx) auth := mustAuthority(ctx)
authorityPolicy, err := auth.GetAuthorityPolicy(ctx) authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
var ae *admin.Error if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy")) render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
return return
} }
@ -472,6 +468,7 @@ func isBadRequest(err error) bool {
} }
func validatePolicy(p *linkedca.Policy) error { func validatePolicy(p *linkedca.Policy) error {
// convert the policy; return early if nil // convert the policy; return early if nil
options := policy.LinkedToCertificates(p) options := policy.LinkedToCertificates(p)
if options == nil { if options == nil {

View file

@ -1,235 +0,0 @@
package api
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"github.com/go-chi/chi"
"github.com/smallstep/certificates/api/read"
"github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority/admin"
"go.step.sm/crypto/randutil"
"go.step.sm/linkedca"
)
// WebhookAdminResponder is the interface responsible for writing webhook admin
// responses.
type WebhookAdminResponder interface {
CreateProvisionerWebhook(w http.ResponseWriter, r *http.Request)
UpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request)
DeleteProvisionerWebhook(w http.ResponseWriter, r *http.Request)
}
// webhoookAdminResponder implements WebhookAdminResponder
type webhookAdminResponder struct{}
// NewWebhookAdminResponder returns a new WebhookAdminResponder
func NewWebhookAdminResponder() WebhookAdminResponder {
return &webhookAdminResponder{}
}
func validateWebhook(webhook *linkedca.Webhook) error {
if webhook == nil {
return nil
}
// name
if webhook.Name == "" {
return admin.NewError(admin.ErrorBadRequestType, "webhook name is required")
}
// url
parsedURL, err := url.Parse(webhook.Url)
if err != nil {
return admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
}
if parsedURL.Host == "" {
return admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
}
if parsedURL.Scheme != "https" {
return admin.NewError(admin.ErrorBadRequestType, "webhook url must use https")
}
if parsedURL.User != nil {
return admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password")
}
// kind
switch webhook.Kind {
case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING, linkedca.Webhook_SCEPCHALLENGE:
default:
return admin.NewError(admin.ErrorBadRequestType, "webhook kind %q is invalid", webhook.Kind)
}
return nil
}
func (war *webhookAdminResponder) CreateProvisionerWebhook(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
auth := mustAuthority(ctx)
prov := linkedca.MustProvisionerFromContext(ctx)
var newWebhook = new(linkedca.Webhook)
if err := read.ProtoJSON(r.Body, newWebhook); err != nil {
render.Error(w, err)
return
}
if err := validateWebhook(newWebhook); err != nil {
render.Error(w, err)
return
}
if newWebhook.Secret != "" {
err := admin.NewError(admin.ErrorBadRequestType, "webhook secret must not be set")
render.Error(w, err)
return
}
if newWebhook.Id != "" {
err := admin.NewError(admin.ErrorBadRequestType, "webhook ID must not be set")
render.Error(w, err)
return
}
id, err := randutil.UUIDv4()
if err != nil {
render.Error(w, admin.WrapErrorISE(err, "error generating webhook id"))
return
}
newWebhook.Id = id
// verify the name is unique
for _, wh := range prov.Webhooks {
if wh.Name == newWebhook.Name {
err := admin.NewError(admin.ErrorConflictType, "provisioner %q already has a webhook with the name %q", prov.Name, newWebhook.Name)
render.Error(w, err)
return
}
}
secret, err := randutil.Bytes(64)
if err != nil {
render.Error(w, admin.WrapErrorISE(err, "error generating webhook secret"))
return
}
newWebhook.Secret = base64.StdEncoding.EncodeToString(secret)
prov.Webhooks = append(prov.Webhooks, newWebhook)
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
if isBadRequest(err) {
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner webhook"))
return
}
render.Error(w, admin.WrapErrorISE(err, "error creating provisioner webhook"))
return
}
render.ProtoJSONStatus(w, newWebhook, http.StatusCreated)
}
func (war *webhookAdminResponder) DeleteProvisionerWebhook(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
auth := mustAuthority(ctx)
prov := linkedca.MustProvisionerFromContext(ctx)
webhookName := chi.URLParam(r, "webhookName")
found := false
for i, wh := range prov.Webhooks {
if wh.Name == webhookName {
prov.Webhooks = append(prov.Webhooks[0:i], prov.Webhooks[i+1:]...)
found = true
break
}
}
if !found {
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
return
}
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
if isBadRequest(err) {
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error deleting provisioner webhook"))
return
}
render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner webhook"))
return
}
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
}
func (war *webhookAdminResponder) UpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
auth := mustAuthority(ctx)
prov := linkedca.MustProvisionerFromContext(ctx)
var newWebhook = new(linkedca.Webhook)
if err := read.ProtoJSON(r.Body, newWebhook); err != nil {
render.Error(w, err)
return
}
if err := validateWebhook(newWebhook); err != nil {
render.Error(w, err)
return
}
found := false
for i, wh := range prov.Webhooks {
if wh.Name != newWebhook.Name {
continue
}
if newWebhook.Secret != "" && newWebhook.Secret != wh.Secret {
err := admin.NewError(admin.ErrorBadRequestType, "webhook secret cannot be updated")
render.Error(w, err)
return
}
newWebhook.Secret = wh.Secret
if newWebhook.Id != "" && newWebhook.Id != wh.Id {
err := admin.NewError(admin.ErrorBadRequestType, "webhook ID cannot be updated")
render.Error(w, err)
return
}
newWebhook.Id = wh.Id
prov.Webhooks[i] = newWebhook
found = true
break
}
if !found {
msg := fmt.Sprintf("provisioner %q has no webhook with the name %q", prov.Name, newWebhook.Name)
err := admin.NewError(admin.ErrorNotFoundType, msg)
render.Error(w, err)
return
}
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
if isBadRequest(err) {
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner webhook"))
return
}
render.Error(w, admin.WrapErrorISE(err, "error updating provisioner webhook"))
return
}
// Return a copy without the signing secret. Include the client-supplied
// auth secrets since those may have been updated in this request and we
// should show in the response that they changed
whResponse := &linkedca.Webhook{
Id: newWebhook.Id,
Name: newWebhook.Name,
Url: newWebhook.Url,
Kind: newWebhook.Kind,
CertType: newWebhook.CertType,
Auth: newWebhook.Auth,
DisableTlsClientAuth: newWebhook.DisableTlsClientAuth,
}
render.ProtoJSONStatus(w, whResponse, http.StatusCreated)
}

View file

@ -1,688 +0,0 @@
package api
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http/httptest"
"strings"
"testing"
"github.com/go-chi/chi"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/admin"
"github.com/stretchr/testify/assert"
"go.step.sm/linkedca"
"google.golang.org/protobuf/encoding/protojson"
)
// ignore secret and id since those are set by the server
func assertEqualWebhook(t *testing.T, a, b *linkedca.Webhook) {
assert.Equal(t, a.Name, b.Name)
assert.Equal(t, a.Url, b.Url)
assert.Equal(t, a.Kind, b.Kind)
assert.Equal(t, a.CertType, b.CertType)
assert.Equal(t, a.DisableTlsClientAuth, b.DisableTlsClientAuth)
assert.Equal(t, a.GetAuth(), b.GetAuth())
}
func TestWebhookAdminResponder_CreateProvisionerWebhook(t *testing.T) {
type test struct {
auth adminAuthority
body []byte
ctx context.Context
err *admin.Error
response *linkedca.Webhook
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"fail/existing-webhook": func(t *testing.T) test {
webhook := &linkedca.Webhook{
Name: "already-exists",
Url: "https://example.com",
}
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{webhook},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
err := admin.NewError(admin.ErrorConflictType, `provisioner "provName" already has a webhook with the name "already-exists"`)
err.Message = `provisioner "provName" already has a webhook with the name "already-exists"`
body := []byte(`
{
"name": "already-exists",
"url": "https://example.com",
"kind": "ENRICHING"
}`)
return test{
ctx: ctx,
body: body,
err: err,
statusCode: 409,
}
},
"fail/read.ProtoJSON": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?")
adminErr.Message = "proto: syntax error (line 1:2): invalid value ?"
body := []byte("{?}")
return test{
ctx: ctx,
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/missing-name": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook name is required")
adminErr.Message = "webhook name is required"
body := []byte(`{"url": "https://example.com", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/missing-url": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
adminErr.Message = "webhook url is invalid"
body := []byte(`{"name": "metadata", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/relative-url": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
adminErr.Message = "webhook url is invalid"
body := []byte(`{"name": "metadata", "url": "example.com/path", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/http-url": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url must use https")
adminErr.Message = "webhook url must use https"
body := []byte(`{"name": "metadata", "url": "http://example.com", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/basic-auth-in-url": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password")
adminErr.Message = "webhook url may not contain username or password"
body := []byte(`
{
"name": "metadata",
"url": "https://user:pass@example.com",
"kind": "ENRICHING"
}`)
return test{
ctx: ctx,
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/secret-in-request": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook secret must not be set")
adminErr.Message = "webhook secret must not be set"
body := []byte(`
{
"name": "metadata",
"url": "https://example.com",
"kind": "ENRICHING",
"secret": "secret"
}`)
return test{
ctx: ctx,
body: body,
err: adminErr,
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",
}
prov := &linkedca.Provisioner{
Name: "provName",
}
ctx := linkedca.NewContextWithAdmin(context.Background(), adm)
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
adminErr := admin.NewError(admin.ErrorServerInternalType, "error creating provisioner webhook: force")
adminErr.Message = "error creating provisioner webhook: force"
body := []byte(`{"name": "metadata", "url": "https://example.com", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
auth: &mockAdminAuthority{
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
return &authority.PolicyError{
Typ: authority.StoreFailure,
Err: errors.New("force"),
}
},
},
body: body,
err: adminErr,
statusCode: 500,
}
},
"ok": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
body := []byte(`{"name": "metadata", "url": "https://example.com", "kind": "ENRICHING", "certType": "X509"}`)
return test{
ctx: ctx,
auth: &mockAdminAuthority{
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
assert.Equal(t, linkedca.Webhook_X509, nu.Webhooks[0].CertType)
return nil
},
},
body: body,
response: &linkedca.Webhook{
Name: "metadata",
Url: "https://example.com",
Kind: linkedca.Webhook_ENRICHING,
CertType: linkedca.Webhook_X509,
},
statusCode: 201,
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
ctx := admin.NewContext(tc.ctx, &admin.MockDB{})
war := NewWebhookAdminResponder()
req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
req = req.WithContext(ctx)
w := httptest.NewRecorder()
war.CreateProvisionerWebhook(w, req)
res := w.Result()
assert.Equal(t, tc.statusCode, res.StatusCode)
if res.StatusCode >= 400 {
body, err := io.ReadAll(res.Body)
res.Body.Close()
assert.NoError(t, err)
ae := testAdminError{}
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
assert.Equal(t, tc.err.Type, ae.Type)
assert.Equal(t, tc.err.StatusCode(), res.StatusCode)
assert.Equal(t, tc.err.Detail, ae.Detail)
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
// when the error message starts with "proto", we expect it to have
// a syntax error (in the tests). If the message doesn't start with "proto",
// we expect a full string match.
if strings.HasPrefix(tc.err.Message, "proto:") {
assert.True(t, strings.Contains(ae.Message, "syntax error"))
} else {
assert.Equal(t, tc.err.Message, ae.Message)
}
return
}
resp := &linkedca.Webhook{}
body, err := io.ReadAll(res.Body)
assert.NoError(t, err)
assert.NoError(t, protojson.Unmarshal(body, resp))
assertEqualWebhook(t, tc.response, resp)
assert.NotEmpty(t, resp.Secret)
assert.NotEmpty(t, resp.Id)
})
}
}
func TestWebhookAdminResponder_DeleteProvisionerWebhook(t *testing.T) {
type test struct {
auth adminAuthority
err *admin.Error
statusCode int
provisionerWebhooks []*linkedca.Webhook
webhookName string
}
var tests = map[string]func(t *testing.T) test{
"fail/auth.UpdateProvisioner-error": func(t *testing.T) test {
adminErr := admin.NewError(admin.ErrorServerInternalType, "error deleting provisioner webhook: force")
adminErr.Message = "error deleting provisioner webhook: force"
return test{
err: adminErr,
auth: &mockAdminAuthority{
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
return &authority.PolicyError{
Typ: authority.StoreFailure,
Err: errors.New("force"),
}
},
},
statusCode: 500,
webhookName: "my-webhook",
provisionerWebhooks: []*linkedca.Webhook{
{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING},
},
}
},
"ok/not-found": func(t *testing.T) test {
return test{
statusCode: 200,
webhookName: "no-exists",
provisionerWebhooks: nil,
}
},
"ok": func(t *testing.T) test {
return test{
statusCode: 200,
webhookName: "exists",
auth: &mockAdminAuthority{
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
assert.Equal(t, nu.Webhooks, []*linkedca.Webhook{
{Name: "my-2nd-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING},
})
return nil
},
},
provisionerWebhooks: []*linkedca.Webhook{
{Name: "exists", Url: "https.example.com", Kind: linkedca.Webhook_ENRICHING},
{Name: "my-2nd-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING},
},
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("webhookName", tc.webhookName)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: tc.provisionerWebhooks,
}
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
ctx = admin.NewContext(ctx, &admin.MockDB{})
req := httptest.NewRequest("DELETE", "/foo", nil).WithContext(ctx)
war := NewWebhookAdminResponder()
w := httptest.NewRecorder()
war.DeleteProvisionerWebhook(w, req)
res := w.Result()
assert.Equal(t, tc.statusCode, res.StatusCode)
if res.StatusCode >= 400 {
body, err := io.ReadAll(res.Body)
res.Body.Close()
assert.NoError(t, err)
ae := testAdminError{}
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
assert.Equal(t, tc.err.Type, ae.Type)
assert.Equal(t, tc.err.StatusCode(), res.StatusCode)
assert.Equal(t, tc.err.Detail, ae.Detail)
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
// when the error message starts with "proto", we expect it to have
// a syntax error (in the tests). If the message doesn't start with "proto",
// we expect a full string match.
if strings.HasPrefix(tc.err.Message, "proto:") {
assert.True(t, strings.Contains(ae.Message, "syntax error"))
} else {
assert.Equal(t, tc.err.Message, ae.Message)
}
return
}
body, err := io.ReadAll(res.Body)
assert.NoError(t, err)
res.Body.Close()
response := DeleteResponse{}
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response))
assert.Equal(t, "ok", response.Status)
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
})
}
}
func TestWebhookAdminResponder_UpdateProvisionerWebhook(t *testing.T) {
type test struct {
auth adminAuthority
adminDB admin.DB
body []byte
ctx context.Context
err *admin.Error
response *linkedca.Webhook
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"fail/not-found": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{{Name: "exists", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
err := admin.NewError(admin.ErrorNotFoundType, `provisioner "provName" has no webhook with the name "no-exists"`)
err.Message = `provisioner "provName" has no webhook with the name "no-exists"`
body := []byte(`
{
"name": "no-exists",
"url": "https://example.com",
"kind": "ENRICHING"
}`)
return test{
ctx: ctx,
adminDB: &admin.MockDB{},
body: body,
err: err,
statusCode: 404,
}
},
"fail/read.ProtoJSON": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "proto: syntax error (line 1:2): invalid value ?")
adminErr.Message = "proto: syntax error (line 1:2): invalid value ?"
body := []byte("{?}")
return test{
ctx: ctx,
adminDB: &admin.MockDB{},
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/missing-name": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook name is required")
adminErr.Message = "webhook name is required"
body := []byte(`{"url": "https://example.com", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
adminDB: &admin.MockDB{},
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/missing-url": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
adminErr.Message = "webhook url is invalid"
body := []byte(`{"name": "metadata", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
adminDB: &admin.MockDB{},
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/relative-url": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url is invalid")
adminErr.Message = "webhook url is invalid"
body := []byte(`{"name": "metadata", "url": "example.com/path", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
adminDB: &admin.MockDB{},
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/http-url": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url must use https")
adminErr.Message = "webhook url must use https"
body := []byte(`{"name": "metadata", "url": "http://example.com", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
adminDB: &admin.MockDB{},
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/basic-auth-in-url": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook url may not contain username or password")
adminErr.Message = "webhook url may not contain username or password"
body := []byte(`
{
"name": "my-webhook",
"url": "https://user:pass@example.com",
"kind": "ENRICHING"
}`)
return test{
ctx: ctx,
adminDB: &admin.MockDB{},
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/different-secret-in-request": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING, Secret: "c2VjcmV0"}},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorBadRequestType, "webhook secret cannot be updated")
adminErr.Message = "webhook secret cannot be updated"
body := []byte(`
{
"name": "my-webhook",
"url": "https://example.com",
"kind": "ENRICHING",
"secret": "secret"
}`)
return test{
ctx: ctx,
body: body,
err: adminErr,
statusCode: 400,
}
},
"fail/auth.UpdateProvisioner-error": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
adminErr := admin.NewError(admin.ErrorServerInternalType, "error updating provisioner webhook: force")
adminErr.Message = "error updating provisioner webhook: force"
body := []byte(`{"name": "my-webhook", "url": "https://example.com", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
adminDB: &admin.MockDB{},
auth: &mockAdminAuthority{
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
return &authority.PolicyError{
Typ: authority.StoreFailure,
Err: errors.New("force"),
}
},
},
body: body,
err: adminErr,
statusCode: 500,
}
},
"ok": func(t *testing.T) test {
prov := &linkedca.Provisioner{
Name: "provName",
Webhooks: []*linkedca.Webhook{{Name: "my-webhook", Url: "https://example.com", Kind: linkedca.Webhook_ENRICHING}},
}
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
body := []byte(`{"name": "my-webhook", "url": "https://example.com", "kind": "ENRICHING"}`)
return test{
ctx: ctx,
adminDB: &admin.MockDB{},
auth: &mockAdminAuthority{
MockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {
return nil
},
},
body: body,
response: &linkedca.Webhook{
Name: "my-webhook",
Url: "https://example.com",
Kind: linkedca.Webhook_ENRICHING,
},
statusCode: 201,
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
ctx := admin.NewContext(tc.ctx, tc.adminDB)
war := NewWebhookAdminResponder()
req := httptest.NewRequest("PUT", "/foo", io.NopCloser(bytes.NewBuffer(tc.body)))
req = req.WithContext(ctx)
w := httptest.NewRecorder()
war.UpdateProvisionerWebhook(w, req)
res := w.Result()
assert.Equal(t, tc.statusCode, res.StatusCode)
if res.StatusCode >= 400 {
body, err := io.ReadAll(res.Body)
res.Body.Close()
assert.NoError(t, err)
ae := testAdminError{}
assert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
assert.Equal(t, tc.err.Type, ae.Type)
assert.Equal(t, tc.err.StatusCode(), res.StatusCode)
assert.Equal(t, tc.err.Detail, ae.Detail)
assert.Equal(t, []string{"application/json"}, res.Header["Content-Type"])
// when the error message starts with "proto", we expect it to have
// a syntax error (in the tests). If the message doesn't start with "proto",
// we expect a full string match.
if strings.HasPrefix(tc.err.Message, "proto:") {
assert.True(t, strings.Contains(ae.Message, "syntax error"))
} else {
assert.Equal(t, tc.err.Message, ae.Message)
}
return
}
resp := &linkedca.Webhook{}
body, err := io.ReadAll(res.Body)
assert.NoError(t, err)
assert.NoError(t, protojson.Unmarshal(body, resp))
assertEqualWebhook(t, tc.response, resp)
})
}
}

View file

@ -40,7 +40,7 @@ func (dba *dbAdmin) clone() *dbAdmin {
return &u return &u
} }
func (db *DB) getDBAdminBytes(_ context.Context, id string) ([]byte, error) { func (db *DB) getDBAdminBytes(ctx context.Context, id string) ([]byte, error) {
data, err := db.db.Get(adminsTable, []byte(id)) data, err := db.db.Get(adminsTable, []byte(id))
if nosql.IsErrNotFound(err) { if nosql.IsErrNotFound(err) {
return nil, admin.NewError(admin.ErrorNotFoundType, "admin %s not found", id) return nil, admin.NewError(admin.ErrorNotFoundType, "admin %s not found", id)
@ -102,7 +102,7 @@ func (db *DB) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error)
// GetAdmins retrieves and unmarshals all active (not deleted) admins // GetAdmins retrieves and unmarshals all active (not deleted) admins
// from the database. // from the database.
// TODO should we be paginating? // TODO should we be paginating?
func (db *DB) GetAdmins(context.Context) ([]*linkedca.Admin, error) { func (db *DB) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) {
dbEntries, err := db.db.List(adminsTable) dbEntries, err := db.db.List(adminsTable)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error loading admins") return nil, errors.Wrap(err, "error loading admins")
@ -111,14 +111,16 @@ func (db *DB) GetAdmins(context.Context) ([]*linkedca.Admin, error) {
for _, entry := range dbEntries { for _, entry := range dbEntries {
adm, err := db.unmarshalAdmin(entry.Value, string(entry.Key)) adm, err := db.unmarshalAdmin(entry.Value, string(entry.Key))
if err != nil { if err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if ae.IsType(admin.ErrorDeletedType) || ae.IsType(admin.ErrorAuthorityMismatchType) { if k.IsType(admin.ErrorDeletedType) || k.IsType(admin.ErrorAuthorityMismatchType) {
continue continue
} else {
return nil, err
} }
default:
return nil, err return nil, err
} }
return nil, err
} }
if adm.AuthorityId != db.authorityID { if adm.AuthorityId != db.authorityID {
continue continue

View file

@ -68,16 +68,16 @@ func TestDB_getDBAdminBytes(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if b, err := d.getDBAdminBytes(context.Background(), adminID); err != nil { if b, err := d.getDBAdminBytes(context.Background(), adminID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -192,16 +192,16 @@ func TestDB_getDBAdmin(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if dba, err := d.getDBAdmin(context.Background(), adminID); err != nil { if dba, err := d.getDBAdmin(context.Background(), adminID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -280,16 +280,16 @@ func TestDB_unmarshalDBAdmin(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{authorityID: admin.DefaultAuthorityID} d := DB{authorityID: admin.DefaultAuthorityID}
if dba, err := d.unmarshalDBAdmin(tc.in, adminID); err != nil { if dba, err := d.unmarshalDBAdmin(tc.in, adminID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -355,16 +355,16 @@ func TestDB_unmarshalAdmin(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{authorityID: admin.DefaultAuthorityID} d := DB{authorityID: admin.DefaultAuthorityID}
if adm, err := d.unmarshalAdmin(tc.in, adminID); err != nil { if adm, err := d.unmarshalAdmin(tc.in, adminID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -509,16 +509,16 @@ func TestDB_GetAdmin(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if adm, err := d.GetAdmin(context.Background(), adminID); err != nil { if adm, err := d.GetAdmin(context.Background(), adminID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -661,16 +661,16 @@ func TestDB_DeleteAdmin(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if err := d.DeleteAdmin(context.Background(), adminID); err != nil { if err := d.DeleteAdmin(context.Background(), adminID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -812,16 +812,16 @@ func TestDB_UpdateAdmin(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if err := d.UpdateAdmin(context.Background(), tc.adm); err != nil { if err := d.UpdateAdmin(context.Background(), tc.adm); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -910,16 +910,16 @@ func TestDB_CreateAdmin(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if err := d.CreateAdmin(context.Background(), tc.adm); err != nil { if err := d.CreateAdmin(context.Background(), tc.adm); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -1086,16 +1086,16 @@ func TestDB_GetAdmins(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if admins, err := d.GetAdmins(context.Background()); err != nil { if admins, err := d.GetAdmins(context.Background()); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }

View file

@ -36,7 +36,7 @@ func New(db nosqlDB.DB, authorityID string) (*DB, error) {
// save writes the new data to the database, overwriting the old data if it // save writes the new data to the database, overwriting the old data if it
// existed. // existed.
func (db *DB) save(_ context.Context, id string, nu, old interface{}, typ string, table []byte) error { func (db *DB) save(ctx context.Context, id string, nu, old interface{}, typ string, table []byte) error {
var ( var (
err error err error
newB []byte newB []byte

View file

@ -71,7 +71,7 @@ func (dbap *dbAuthorityPolicy) convert() *linkedca.Policy {
return dbToLinked(dbap.Policy) return dbToLinked(dbap.Policy)
} }
func (db *DB) getDBAuthorityPolicyBytes(_ context.Context, authorityID string) ([]byte, error) { func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string) ([]byte, error) {
data, err := db.db.Get(authorityPoliciesTable, []byte(authorityID)) data, err := db.db.Get(authorityPoliciesTable, []byte(authorityID))
if nosql.IsErrNotFound(err) { if nosql.IsErrNotFound(err) {
return nil, admin.NewError(admin.ErrorNotFoundType, "authority policy not found") return nil, admin.NewError(admin.ErrorNotFoundType, "authority policy not found")
@ -83,7 +83,6 @@ func (db *DB) getDBAuthorityPolicyBytes(_ context.Context, authorityID string) (
func (db *DB) unmarshalDBAuthorityPolicy(data []byte) (*dbAuthorityPolicy, error) { func (db *DB) unmarshalDBAuthorityPolicy(data []byte) (*dbAuthorityPolicy, error) {
if len(data) == 0 { if len(data) == 0 {
//nolint:nilnil // legacy
return nil, nil return nil, nil
} }
var dba = new(dbAuthorityPolicy) var dba = new(dbAuthorityPolicy)
@ -103,7 +102,6 @@ func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*db
return nil, err return nil, err
} }
if dbap == nil { if dbap == nil {
//nolint:nilnil // legacy
return nil, nil return nil, nil
} }
if dbap.AuthorityID != authorityID { if dbap.AuthorityID != authorityID {
@ -114,6 +112,7 @@ func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*db
} }
func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error { func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
dbap := &dbAuthorityPolicy{ dbap := &dbAuthorityPolicy{
ID: db.authorityID, ID: db.authorityID,
AuthorityID: db.authorityID, AuthorityID: db.authorityID,
@ -229,6 +228,7 @@ func dbToLinked(p *dbPolicy) *linkedca.Policy {
} }
func linkedToDB(p *linkedca.Policy) *dbPolicy { func linkedToDB(p *linkedca.Policy) *dbPolicy {
if p == nil { if p == nil {
return nil return nil
} }

View file

@ -72,16 +72,16 @@ func TestDB_getDBAuthorityPolicyBytes(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if b, err := d.getDBAuthorityPolicyBytes(tc.ctx, tc.authorityID); err != nil { if b, err := d.getDBAuthorityPolicyBytes(tc.ctx, tc.authorityID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -208,16 +208,16 @@ func TestDB_getDBAuthorityPolicy(t *testing.T) {
dbp, err := d.getDBAuthorityPolicy(tc.ctx, tc.authorityID) dbp, err := d.getDBAuthorityPolicy(tc.ctx, tc.authorityID)
switch { switch {
case err != nil: case err != nil:
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -309,16 +309,16 @@ func TestDB_CreateAuthorityPolicy(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: tc.authorityID} d := DB{db: tc.db, authorityID: tc.authorityID}
if err := d.CreateAuthorityPolicy(tc.ctx, tc.policy); err != nil { if err := d.CreateAuthorityPolicy(tc.ctx, tc.policy); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -406,16 +406,16 @@ func TestDB_GetAuthorityPolicy(t *testing.T) {
d := DB{db: tc.db, authorityID: tc.authorityID} d := DB{db: tc.db, authorityID: tc.authorityID}
got, err := d.GetAuthorityPolicy(tc.ctx) got, err := d.GetAuthorityPolicy(tc.ctx)
if err != nil { if err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -578,16 +578,16 @@ func TestDB_UpdateAuthorityPolicy(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: tc.authorityID} d := DB{db: tc.db, authorityID: tc.authorityID}
if err := d.UpdateAuthorityPolicy(tc.ctx, tc.policy); err != nil { if err := d.UpdateAuthorityPolicy(tc.ctx, tc.policy); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -718,16 +718,16 @@ func TestDB_DeleteAuthorityPolicy(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: tc.authorityID} d := DB{db: tc.db, authorityID: tc.authorityID}
if err := d.DeleteAuthorityPolicy(tc.ctx); err != nil { if err := d.DeleteAuthorityPolicy(tc.ctx); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }

View file

@ -24,24 +24,6 @@ type dbProvisioner struct {
SSHTemplate *linkedca.Template `json:"sshTemplate"` SSHTemplate *linkedca.Template `json:"sshTemplate"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
DeletedAt time.Time `json:"deletedAt"` DeletedAt time.Time `json:"deletedAt"`
Webhooks []dbWebhook `json:"webhooks,omitempty"`
}
type dbBasicAuth struct {
Username string `json:"username"`
Password string `json:"password"`
}
type dbWebhook struct {
Name string `json:"name"`
ID string `json:"id"`
URL string `json:"url"`
Kind string `json:"kind"`
Secret string `json:"secret"`
BearerToken string `json:"bearerToken,omitempty"`
BasicAuth *dbBasicAuth `json:"basicAuth,omitempty"`
DisableTLSClientAuth bool `json:"disableTLSClientAuth,omitempty"`
CertType string `json:"certType,omitempty"`
} }
func (dbp *dbProvisioner) clone() *dbProvisioner { func (dbp *dbProvisioner) clone() *dbProvisioner {
@ -66,11 +48,10 @@ func (dbp *dbProvisioner) convert2linkedca() (*linkedca.Provisioner, error) {
SshTemplate: dbp.SSHTemplate, SshTemplate: dbp.SSHTemplate,
CreatedAt: timestamppb.New(dbp.CreatedAt), CreatedAt: timestamppb.New(dbp.CreatedAt),
DeletedAt: timestamppb.New(dbp.DeletedAt), DeletedAt: timestamppb.New(dbp.DeletedAt),
Webhooks: dbWebhooksToLinkedca(dbp.Webhooks),
}, nil }, nil
} }
func (db *DB) getDBProvisionerBytes(_ context.Context, id string) ([]byte, error) { func (db *DB) getDBProvisionerBytes(ctx context.Context, id string) ([]byte, error) {
data, err := db.db.Get(provisionersTable, []byte(id)) data, err := db.db.Get(provisionersTable, []byte(id))
if nosql.IsErrNotFound(err) { if nosql.IsErrNotFound(err) {
return nil, admin.NewError(admin.ErrorNotFoundType, "provisioner %s not found", id) return nil, admin.NewError(admin.ErrorNotFoundType, "provisioner %s not found", id)
@ -132,7 +113,7 @@ func (db *DB) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisio
// GetProvisioners retrieves and unmarshals all active (not deleted) provisioners // GetProvisioners retrieves and unmarshals all active (not deleted) provisioners
// from the database. // from the database.
func (db *DB) GetProvisioners(_ context.Context) ([]*linkedca.Provisioner, error) { func (db *DB) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) {
dbEntries, err := db.db.List(provisionersTable) dbEntries, err := db.db.List(provisionersTable)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error loading provisioners") return nil, errors.Wrap(err, "error loading provisioners")
@ -141,14 +122,16 @@ func (db *DB) GetProvisioners(_ context.Context) ([]*linkedca.Provisioner, error
for _, entry := range dbEntries { for _, entry := range dbEntries {
prov, err := db.unmarshalProvisioner(entry.Value, string(entry.Key)) prov, err := db.unmarshalProvisioner(entry.Value, string(entry.Key))
if err != nil { if err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if ae.IsType(admin.ErrorDeletedType) || ae.IsType(admin.ErrorAuthorityMismatchType) { if k.IsType(admin.ErrorDeletedType) || k.IsType(admin.ErrorAuthorityMismatchType) {
continue continue
} else {
return nil, err
} }
default:
return nil, err return nil, err
} }
return nil, err
} }
if prov.AuthorityId != db.authorityID { if prov.AuthorityId != db.authorityID {
continue continue
@ -181,7 +164,6 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
X509Template: prov.X509Template, X509Template: prov.X509Template,
SSHTemplate: prov.SshTemplate, SSHTemplate: prov.SshTemplate,
CreatedAt: clock.Now(), CreatedAt: clock.Now(),
Webhooks: linkedcaWebhooksToDB(prov.Webhooks),
} }
if err := db.save(ctx, prov.Id, dbp, nil, "provisioner", provisionersTable); err != nil { if err := db.save(ctx, prov.Id, dbp, nil, "provisioner", provisionersTable); err != nil {
@ -211,7 +193,6 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
} }
nu.X509Template = prov.X509Template nu.X509Template = prov.X509Template
nu.SSHTemplate = prov.SshTemplate nu.SSHTemplate = prov.SshTemplate
nu.Webhooks = linkedcaWebhooksToDB(prov.Webhooks)
return db.save(ctx, prov.Id, nu, old, "provisioner", provisionersTable) return db.save(ctx, prov.Id, nu, old, "provisioner", provisionersTable)
} }
@ -228,70 +209,3 @@ func (db *DB) DeleteProvisioner(ctx context.Context, id string) error {
return db.save(ctx, old.ID, nu, old, "provisioner", provisionersTable) return db.save(ctx, old.ID, nu, old, "provisioner", provisionersTable)
} }
func dbWebhooksToLinkedca(dbwhs []dbWebhook) []*linkedca.Webhook {
if len(dbwhs) == 0 {
return nil
}
lwhs := make([]*linkedca.Webhook, len(dbwhs))
for i, dbwh := range dbwhs {
lwh := &linkedca.Webhook{
Name: dbwh.Name,
Id: dbwh.ID,
Url: dbwh.URL,
Kind: linkedca.Webhook_Kind(linkedca.Webhook_Kind_value[dbwh.Kind]),
Secret: dbwh.Secret,
DisableTlsClientAuth: dbwh.DisableTLSClientAuth,
CertType: linkedca.Webhook_CertType(linkedca.Webhook_CertType_value[dbwh.CertType]),
}
if dbwh.BearerToken != "" {
lwh.Auth = &linkedca.Webhook_BearerToken{
BearerToken: &linkedca.BearerToken{
BearerToken: dbwh.BearerToken,
},
}
} else if dbwh.BasicAuth != nil && (dbwh.BasicAuth.Username != "" || dbwh.BasicAuth.Password != "") {
lwh.Auth = &linkedca.Webhook_BasicAuth{
BasicAuth: &linkedca.BasicAuth{
Username: dbwh.BasicAuth.Username,
Password: dbwh.BasicAuth.Password,
},
}
}
lwhs[i] = lwh
}
return lwhs
}
func linkedcaWebhooksToDB(lwhs []*linkedca.Webhook) []dbWebhook {
if len(lwhs) == 0 {
return nil
}
dbwhs := make([]dbWebhook, len(lwhs))
for i, lwh := range lwhs {
dbwh := dbWebhook{
Name: lwh.Name,
ID: lwh.Id,
URL: lwh.Url,
Kind: lwh.Kind.String(),
Secret: lwh.Secret,
DisableTLSClientAuth: lwh.DisableTlsClientAuth,
CertType: lwh.CertType.String(),
}
switch a := lwh.GetAuth().(type) {
case *linkedca.Webhook_BearerToken:
dbwh.BearerToken = a.BearerToken.BearerToken
case *linkedca.Webhook_BasicAuth:
dbwh.BasicAuth = &dbBasicAuth{
Username: a.BasicAuth.Username,
Password: a.BasicAuth.Password,
}
}
dbwhs[i] = dbwh
}
return dbwhs
}

View file

@ -67,16 +67,16 @@ func TestDB_getDBProvisionerBytes(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db} d := DB{db: tc.db}
if b, err := d.getDBProvisionerBytes(context.Background(), provID); err != nil { if b, err := d.getDBProvisionerBytes(context.Background(), provID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -137,7 +137,6 @@ func TestDB_getDBProvisioner(t *testing.T) {
} }
}, },
"fail/deleted": func(t *testing.T) test { "fail/deleted": func(t *testing.T) test {
now := clock.Now() now := clock.Now()
dbp := &dbProvisioner{ dbp := &dbProvisioner{
ID: provID, ID: provID,
@ -190,16 +189,16 @@ func TestDB_getDBProvisioner(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if dbp, err := d.getDBProvisioner(context.Background(), provID); err != nil { if dbp, err := d.getDBProvisioner(context.Background(), provID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -211,7 +210,6 @@ func TestDB_getDBProvisioner(t *testing.T) {
assert.Equals(t, dbp.Name, tc.dbp.Name) assert.Equals(t, dbp.Name, tc.dbp.Name)
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt) assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
assert.Fatal(t, dbp.DeletedAt.IsZero()) assert.Fatal(t, dbp.DeletedAt.IsZero())
assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)
} }
}) })
} }
@ -277,16 +275,16 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{authorityID: admin.DefaultAuthorityID} d := DB{authorityID: admin.DefaultAuthorityID}
if dbp, err := d.unmarshalDBProvisioner(tc.in, provID); err != nil { if dbp, err := d.unmarshalDBProvisioner(tc.in, provID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -302,7 +300,6 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) {
assert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate) assert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate)
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt) assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
assert.Fatal(t, dbp.DeletedAt.IsZero()) assert.Fatal(t, dbp.DeletedAt.IsZero())
assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)
} }
}) })
} }
@ -356,15 +353,6 @@ func defaultDBP(t *testing.T) *dbProvisioner {
Data: []byte("zap"), Data: []byte("zap"),
}, },
CreatedAt: clock.Now(), CreatedAt: clock.Now(),
Webhooks: []dbWebhook{
{
Name: "metadata",
URL: "https://inventory.smallstep.com",
Kind: linkedca.Webhook_ENRICHING.String(),
Secret: "secret",
BearerToken: "token",
},
},
} }
} }
@ -409,16 +397,16 @@ func TestDB_unmarshalProvisioner(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{authorityID: admin.DefaultAuthorityID} d := DB{authorityID: admin.DefaultAuthorityID}
if prov, err := d.unmarshalProvisioner(tc.in, provID); err != nil { if prov, err := d.unmarshalProvisioner(tc.in, provID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -431,7 +419,6 @@ func TestDB_unmarshalProvisioner(t *testing.T) {
assert.Equals(t, prov.Claims, tc.dbp.Claims) assert.Equals(t, prov.Claims, tc.dbp.Claims)
assert.Equals(t, prov.X509Template, tc.dbp.X509Template) assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate) assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))
retDetailsBytes, err := json.Marshal(prov.Details.GetData()) retDetailsBytes, err := json.Marshal(prov.Details.GetData())
assert.FatalError(t, err) assert.FatalError(t, err)
@ -548,16 +535,16 @@ func TestDB_GetProvisioner(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if prov, err := d.GetProvisioner(context.Background(), provID); err != nil { if prov, err := d.GetProvisioner(context.Background(), provID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -570,7 +557,6 @@ func TestDB_GetProvisioner(t *testing.T) {
assert.Equals(t, prov.Claims, tc.dbp.Claims) assert.Equals(t, prov.Claims, tc.dbp.Claims)
assert.Equals(t, prov.X509Template, tc.dbp.X509Template) assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate) assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))
retDetailsBytes, err := json.Marshal(prov.Details.GetData()) retDetailsBytes, err := json.Marshal(prov.Details.GetData())
assert.FatalError(t, err) assert.FatalError(t, err)
@ -643,7 +629,6 @@ func TestDB_DeleteProvisioner(t *testing.T) {
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate) assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt) assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
assert.Equals(t, _dbp.Details, dbp.Details) assert.Equals(t, _dbp.Details, dbp.Details)
assert.Equals(t, _dbp.Webhooks, dbp.Webhooks)
assert.True(t, _dbp.DeletedAt.Before(time.Now())) assert.True(t, _dbp.DeletedAt.Before(time.Now()))
assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute))) assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))
@ -683,7 +668,6 @@ func TestDB_DeleteProvisioner(t *testing.T) {
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate) assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt) assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
assert.Equals(t, _dbp.Details, dbp.Details) assert.Equals(t, _dbp.Details, dbp.Details)
assert.Equals(t, _dbp.Webhooks, dbp.Webhooks)
assert.True(t, _dbp.DeletedAt.Before(time.Now())) assert.True(t, _dbp.DeletedAt.Before(time.Now()))
assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute))) assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))
@ -699,16 +683,16 @@ func TestDB_DeleteProvisioner(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if err := d.DeleteProvisioner(context.Background(), provID); err != nil { if err := d.DeleteProvisioner(context.Background(), provID); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -835,7 +819,6 @@ func TestDB_GetProvisioners(t *testing.T) {
assert.Equals(t, provs[0].Claims, fooProv.Claims) assert.Equals(t, provs[0].Claims, fooProv.Claims)
assert.Equals(t, provs[0].X509Template, fooProv.X509Template) assert.Equals(t, provs[0].X509Template, fooProv.X509Template)
assert.Equals(t, provs[0].SshTemplate, fooProv.SSHTemplate) assert.Equals(t, provs[0].SshTemplate, fooProv.SSHTemplate)
assert.Equals(t, provs[0].Webhooks, dbWebhooksToLinkedca(fooProv.Webhooks))
retDetailsBytes, err := json.Marshal(provs[0].Details.GetData()) retDetailsBytes, err := json.Marshal(provs[0].Details.GetData())
assert.FatalError(t, err) assert.FatalError(t, err)
@ -848,7 +831,6 @@ func TestDB_GetProvisioners(t *testing.T) {
assert.Equals(t, provs[1].Claims, zapProv.Claims) assert.Equals(t, provs[1].Claims, zapProv.Claims)
assert.Equals(t, provs[1].X509Template, zapProv.X509Template) assert.Equals(t, provs[1].X509Template, zapProv.X509Template)
assert.Equals(t, provs[1].SshTemplate, zapProv.SSHTemplate) assert.Equals(t, provs[1].SshTemplate, zapProv.SSHTemplate)
assert.Equals(t, provs[1].Webhooks, dbWebhooksToLinkedca(zapProv.Webhooks))
retDetailsBytes, err = json.Marshal(provs[1].Details.GetData()) retDetailsBytes, err = json.Marshal(provs[1].Details.GetData())
assert.FatalError(t, err) assert.FatalError(t, err)
@ -862,16 +844,16 @@ func TestDB_GetProvisioners(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if provs, err := d.GetProvisioners(context.Background()); err != nil { if provs, err := d.GetProvisioners(context.Background()); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -913,7 +895,6 @@ func TestDB_CreateProvisioner(t *testing.T) {
assert.Equals(t, _dbp.Claims, prov.Claims) assert.Equals(t, _dbp.Claims, prov.Claims)
assert.Equals(t, _dbp.X509Template, prov.X509Template) assert.Equals(t, _dbp.X509Template, prov.X509Template)
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate) assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
retDetailsBytes, err := json.Marshal(prov.Details.GetData()) retDetailsBytes, err := json.Marshal(prov.Details.GetData())
assert.FatalError(t, err) assert.FatalError(t, err)
@ -951,7 +932,6 @@ func TestDB_CreateProvisioner(t *testing.T) {
assert.Equals(t, _dbp.Claims, prov.Claims) assert.Equals(t, _dbp.Claims, prov.Claims)
assert.Equals(t, _dbp.X509Template, prov.X509Template) assert.Equals(t, _dbp.X509Template, prov.X509Template)
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate) assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
retDetailsBytes, err := json.Marshal(prov.Details.GetData()) retDetailsBytes, err := json.Marshal(prov.Details.GetData())
assert.FatalError(t, err) assert.FatalError(t, err)
@ -972,16 +952,16 @@ func TestDB_CreateProvisioner(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if err := d.CreateProvisioner(context.Background(), tc.prov); err != nil { if err := d.CreateProvisioner(context.Background(), tc.prov); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -1100,7 +1080,6 @@ func TestDB_UpdateProvisioner(t *testing.T) {
assert.Equals(t, _dbp.Claims, prov.Claims) assert.Equals(t, _dbp.Claims, prov.Claims)
assert.Equals(t, _dbp.X509Template, prov.X509Template) assert.Equals(t, _dbp.X509Template, prov.X509Template)
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate) assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
retDetailsBytes, err := json.Marshal(prov.Details.GetData()) retDetailsBytes, err := json.Marshal(prov.Details.GetData())
assert.FatalError(t, err) assert.FatalError(t, err)
@ -1162,12 +1141,6 @@ func TestDB_UpdateProvisioner(t *testing.T) {
}, },
}, },
} }
prov.Webhooks = []*linkedca.Webhook{
{
Name: "users",
Url: "https://example.com/users",
},
}
data, err := json.Marshal(dbp) data, err := json.Marshal(dbp)
assert.FatalError(t, err) assert.FatalError(t, err)
@ -1195,7 +1168,6 @@ func TestDB_UpdateProvisioner(t *testing.T) {
assert.Equals(t, _dbp.Claims, prov.Claims) assert.Equals(t, _dbp.Claims, prov.Claims)
assert.Equals(t, _dbp.X509Template, prov.X509Template) assert.Equals(t, _dbp.X509Template, prov.X509Template)
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate) assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
retDetailsBytes, err := json.Marshal(prov.Details.GetData()) retDetailsBytes, err := json.Marshal(prov.Details.GetData())
assert.FatalError(t, err) assert.FatalError(t, err)
@ -1216,16 +1188,16 @@ func TestDB_UpdateProvisioner(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID} d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
if err := d.UpdateProvisioner(context.Background(), tc.prov); err != nil { if err := d.UpdateProvisioner(context.Background(), tc.prov); err != nil {
var ae *admin.Error switch k := err.(type) {
if errors.As(err, &ae) { case *admin.Error:
if assert.NotNil(t, tc.adminErr) { if assert.NotNil(t, tc.adminErr) {
assert.Equals(t, ae.Type, tc.adminErr.Type) assert.Equals(t, k.Type, tc.adminErr.Type)
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
assert.Equals(t, ae.Status, tc.adminErr.Status) assert.Equals(t, k.Status, tc.adminErr.Status)
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error()) assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
assert.Equals(t, ae.Detail, tc.adminErr.Detail) assert.Equals(t, k.Detail, tc.adminErr.Detail)
} }
} else { default:
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -1234,164 +1206,3 @@ func TestDB_UpdateProvisioner(t *testing.T) {
}) })
} }
} }
func Test_linkedcaWebhooksToDB(t *testing.T) {
type test struct {
in []*linkedca.Webhook
want []dbWebhook
}
var tests = map[string]test{
"nil": {
in: nil,
want: nil,
},
"zero": {
in: []*linkedca.Webhook{},
want: nil,
},
"bearer": {
in: []*linkedca.Webhook{
{
Name: "bearer",
Url: "https://example.com",
Kind: linkedca.Webhook_ENRICHING,
Secret: "secret",
Auth: &linkedca.Webhook_BearerToken{
BearerToken: &linkedca.BearerToken{
BearerToken: "token",
},
},
DisableTlsClientAuth: true,
CertType: linkedca.Webhook_X509,
},
},
want: []dbWebhook{
{
Name: "bearer",
URL: "https://example.com",
Kind: "ENRICHING",
Secret: "secret",
BearerToken: "token",
DisableTLSClientAuth: true,
CertType: linkedca.Webhook_X509.String(),
},
},
},
"basic": {
in: []*linkedca.Webhook{
{
Name: "basic",
Url: "https://example.com",
Kind: linkedca.Webhook_ENRICHING,
Secret: "secret",
Auth: &linkedca.Webhook_BasicAuth{
BasicAuth: &linkedca.BasicAuth{
Username: "user",
Password: "pass",
},
},
},
},
want: []dbWebhook{
{
Name: "basic",
URL: "https://example.com",
Kind: "ENRICHING",
Secret: "secret",
BasicAuth: &dbBasicAuth{
Username: "user",
Password: "pass",
},
CertType: linkedca.Webhook_ALL.String(),
},
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := linkedcaWebhooksToDB(tc.in)
assert.Equals(t, tc.want, got)
})
}
}
func Test_dbWebhooksToLinkedca(t *testing.T) {
type test struct {
in []dbWebhook
want []*linkedca.Webhook
}
var tests = map[string]test{
"nil": {
in: nil,
want: nil,
},
"zero": {
in: []dbWebhook{},
want: nil,
},
"bearer": {
in: []dbWebhook{
{
Name: "bearer",
ID: "69350cb6-6c31-4b5e-bf25-affd5053427d",
URL: "https://example.com",
Kind: "ENRICHING",
Secret: "secret",
BearerToken: "token",
DisableTLSClientAuth: true,
},
},
want: []*linkedca.Webhook{
{
Name: "bearer",
Id: "69350cb6-6c31-4b5e-bf25-affd5053427d",
Url: "https://example.com",
Kind: linkedca.Webhook_ENRICHING,
Secret: "secret",
Auth: &linkedca.Webhook_BearerToken{
BearerToken: &linkedca.BearerToken{
BearerToken: "token",
},
},
DisableTlsClientAuth: true,
},
},
},
"basic": {
in: []dbWebhook{
{
Name: "basic",
ID: "69350cb6-6c31-4b5e-bf25-affd5053427d",
URL: "https://example.com",
Kind: "ENRICHING",
Secret: "secret",
BasicAuth: &dbBasicAuth{
Username: "user",
Password: "pass",
},
},
},
want: []*linkedca.Webhook{
{
Name: "basic",
Id: "69350cb6-6c31-4b5e-bf25-affd5053427d",
Url: "https://example.com",
Kind: linkedca.Webhook_ENRICHING,
Secret: "secret",
Auth: &linkedca.Webhook_BasicAuth{
BasicAuth: &linkedca.BasicAuth{
Username: "user",
Password: "pass",
},
},
},
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := dbWebhooksToLinkedca(tc.in)
assert.Equals(t, tc.want, got)
})
}
}

View file

@ -156,17 +156,16 @@ func NewErrorISE(msg string, args ...interface{}) *Error {
// WrapError attempts to wrap the internal error. // WrapError attempts to wrap the internal error.
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error { func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
var ee *Error switch e := err.(type) {
switch { case nil:
case err == nil:
return nil return nil
case errors.As(err, &ee): case *Error:
if ee.Err == nil { if e.Err == nil {
ee.Err = errors.Errorf(msg+"; "+ee.Detail, args...) e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
} else { } else {
ee.Err = errors.Wrapf(ee.Err, msg, args...) e.Err = errors.Wrapf(e.Err, msg, args...)
} }
return ee return e
default: default:
return newError(typ, errors.Wrapf(err, msg, args...)) return newError(typ, errors.Wrapf(err, msg, args...))
} }

View file

@ -1,14 +1,12 @@
package authority package authority
import ( import (
"bytes"
"context" "context"
"crypto" "crypto"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex" "encoding/hex"
"log" "log"
"net/http"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -26,7 +24,6 @@ import (
adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql" adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql"
"github.com/smallstep/certificates/authority/administrator" "github.com/smallstep/certificates/authority/administrator"
"github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/internal/constraints"
"github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/policy"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/cas" "github.com/smallstep/certificates/cas"
@ -47,18 +44,16 @@ type Authority struct {
adminDB admin.DB adminDB admin.DB
templates *templates.Templates templates *templates.Templates
linkedCAToken string linkedCAToken string
webhookClient *http.Client
// X509 CA // X509 CA
password []byte password []byte
issuerPassword []byte issuerPassword []byte
x509CAService cas.CertificateAuthorityService x509CAService cas.CertificateAuthorityService
rootX509Certs []*x509.Certificate rootX509Certs []*x509.Certificate
rootX509CertPool *x509.CertPool rootX509CertPool *x509.CertPool
federatedX509Certs []*x509.Certificate federatedX509Certs []*x509.Certificate
intermediateX509Certs []*x509.Certificate certificates *sync.Map
certificates *sync.Map x509Enforcers []provisioner.CertificateEnforcer
x509Enforcers []provisioner.CertificateEnforcer
// SCEP CA // SCEP CA
scepService *scep.Service scepService *scep.Service
@ -73,12 +68,7 @@ type Authority struct {
sshCAUserFederatedCerts []ssh.PublicKey sshCAUserFederatedCerts []ssh.PublicKey
sshCAHostFederatedCerts []ssh.PublicKey sshCAHostFederatedCerts []ssh.PublicKey
// CRL vars // Do not re-initialize
crlTicker *time.Ticker
crlStopper chan struct{}
crlMutex sync.Mutex
// If true, do not re-initialize
initOnce bool initOnce bool
startTime time.Time startTime time.Time
@ -90,17 +80,13 @@ type Authority struct {
authorizeRenewFunc provisioner.AuthorizeRenewFunc authorizeRenewFunc provisioner.AuthorizeRenewFunc
authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc
// Constraints and Policy engines // Policy engines
constraintsEngine *constraints.Engine policyEngine *policy.Engine
policyEngine *policy.Engine
adminMutex sync.RWMutex adminMutex sync.RWMutex
// If true, do not initialize the authority // Do Not initialize the authority
skipInit bool skipInit bool
// If true, do not output initialization logs
quietInit bool
} }
// Info contains information about the authority. // Info contains information about the authority.
@ -382,17 +368,11 @@ func (a *Authority) init() error {
} }
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey, SigningKey: a.config.IntermediateKey,
Password: a.password, Password: []byte(a.password),
}) })
if err != nil { if err != nil {
return err return err
} }
// If not defined with an option, add intermediates to the list of
// certificates used for name constraints validation at issuance
// time.
if len(a.intermediateX509Certs) == 0 {
a.intermediateX509Certs = append(a.intermediateX509Certs, options.CertificateChain...)
}
} }
a.x509CAService, err = cas.New(ctx, options) a.x509CAService, err = cas.New(ctx, options)
if err != nil { if err != nil {
@ -413,13 +393,13 @@ func (a *Authority) init() error {
// Read root certificates and store them in the certificates map. // Read root certificates and store them in the certificates map.
if len(a.rootX509Certs) == 0 { if len(a.rootX509Certs) == 0 {
a.rootX509Certs = make([]*x509.Certificate, 0, len(a.config.Root)) a.rootX509Certs = make([]*x509.Certificate, len(a.config.Root))
for _, path := range a.config.Root { for i, path := range a.config.Root {
crts, err := pemutil.ReadCertificateBundle(path) crt, err := pemutil.ReadCertificate(path)
if err != nil { if err != nil {
return err return err
} }
a.rootX509Certs = append(a.rootX509Certs, crts...) a.rootX509Certs[i] = crt
} }
} }
for _, crt := range a.rootX509Certs { for _, crt := range a.rootX509Certs {
@ -434,13 +414,13 @@ func (a *Authority) init() error {
// Read federated certificates and store them in the certificates map. // Read federated certificates and store them in the certificates map.
if len(a.federatedX509Certs) == 0 { if len(a.federatedX509Certs) == 0 {
a.federatedX509Certs = make([]*x509.Certificate, 0, len(a.config.FederatedRoots)) a.federatedX509Certs = make([]*x509.Certificate, len(a.config.FederatedRoots))
for _, path := range a.config.FederatedRoots { for i, path := range a.config.FederatedRoots {
crts, err := pemutil.ReadCertificateBundle(path) crt, err := pemutil.ReadCertificate(path)
if err != nil { if err != nil {
return err return err
} }
a.federatedX509Certs = append(a.federatedX509Certs, crts...) a.federatedX509Certs[i] = crt
} }
} }
for _, crt := range a.federatedX509Certs { for _, crt := range a.federatedX509Certs {
@ -454,7 +434,7 @@ func (a *Authority) init() error {
if a.config.SSH.HostKey != "" { if a.config.SSH.HostKey != "" {
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.SSH.HostKey, SigningKey: a.config.SSH.HostKey,
Password: a.sshHostPassword, Password: []byte(a.sshHostPassword),
}) })
if err != nil { if err != nil {
return err return err
@ -480,7 +460,7 @@ func (a *Authority) init() error {
if a.config.SSH.UserKey != "" { if a.config.SSH.UserKey != "" {
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.SSH.UserKey, SigningKey: a.config.SSH.UserKey,
Password: a.sshUserPassword, Password: []byte(a.sshUserPassword),
}) })
if err != nil { if err != nil {
return err return err
@ -545,101 +525,6 @@ func (a *Authority) init() error {
tmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts...) tmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts...)
} }
if a.config.AuthorityConfig.EnableAdmin {
// Initialize step-ca Admin Database if it's not already initialized using
// WithAdminDB.
if a.adminDB == nil {
if linkedcaClient != nil {
a.adminDB = linkedcaClient
} else {
a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID)
if err != nil {
return err
}
}
}
provs, err := a.adminDB.GetProvisioners(ctx)
if err != nil {
return admin.WrapErrorISE(err, "error loading provisioners to initialize authority")
}
if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") {
// Migration will currently only be kicked off once, because either one or more provisioners
// are migrated or a default JWK provisioner will be created in the DB. It won't run for
// linked or hosted deployments. Not for linked, because that case is explicitly checked
// for above. Not for hosted, because there'll be at least an existing OIDC provisioner.
var firstJWKProvisioner *linkedca.Provisioner
if len(a.config.AuthorityConfig.Provisioners) > 0 {
// Existing provisioners detected; try migrating them to DB storage.
a.initLogf("Starting migration of provisioners")
for _, p := range a.config.AuthorityConfig.Provisioners {
lp, err := ProvisionerToLinkedca(p)
if err != nil {
return admin.WrapErrorISE(err, "error transforming provisioner %q while migrating", p.GetName())
}
// Store the provisioner to be migrated
if err := a.adminDB.CreateProvisioner(ctx, lp); err != nil {
return admin.WrapErrorISE(err, "error creating provisioner %q while migrating", p.GetName())
}
// Mark the first JWK provisioner, so that it can be used for administration purposes
if firstJWKProvisioner == nil && lp.Type == linkedca.Provisioner_JWK {
firstJWKProvisioner = lp
a.initLogf("Migrated JWK provisioner %q with admin permissions", p.GetName())
} else {
a.initLogf("Migrated %s provisioner %q", p.GetType(), p.GetName())
}
}
c := a.config
if c.WasLoadedFromFile() {
// The provisioners in the configuration file can be deleted from
// the file by editing it. Automatic rewriting of the file was considered
// to be too surprising for users and not the right solution for all
// use cases, so we leave it up to users to this themselves.
a.initLogf("Provisioners that were migrated can now be removed from `ca.json` by editing it")
}
a.initLogf("Finished migrating provisioners")
}
// Create first JWK provisioner for remote administration purposes if none exists yet
if firstJWKProvisioner == nil {
firstJWKProvisioner, err = CreateFirstProvisioner(ctx, a.adminDB, string(a.password))
if err != nil {
return admin.WrapErrorISE(err, "error creating first provisioner")
}
a.initLogf("Created JWK provisioner %q with admin permissions", firstJWKProvisioner.GetName())
}
// Create first super admin, belonging to the first JWK provisioner
// TODO(hs): pass a user-provided first super admin subject to here. With `ca init` it's
// added to the DB immediately if using remote management. But when migrating from
// ca.json to the DB, this option doesn't exist. Adding a flag just to do it during
// migration isn't nice. We could opt for a user to change it afterwards. There exist
// cases in which creation of `step` could lock out a user from API access. This is the
// case if `step` isn't allowed to be signed by Name Constraints or the X.509 policy.
// We have protection for that when creating and updating a policy, but if a policy or
// Name Constraints are in use at the time of migration, that could lock the user out.
superAdminSubject := "step"
if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{
ProvisionerId: firstJWKProvisioner.Id,
Subject: superAdminSubject,
Type: linkedca.Admin_SUPER_ADMIN,
}); err != nil {
return admin.WrapErrorISE(err, "error creating first admin")
}
a.initLogf("Created super admin %q for JWK provisioner %q", superAdminSubject, firstJWKProvisioner.GetName())
}
}
// Load Provisioners and Admins
if err := a.ReloadAdminResources(ctx); err != nil {
return err
}
// Check if a KMS with decryption capability is required and available // Check if a KMS with decryption capability is required and available
if a.requiresDecrypter() { if a.requiresDecrypter() {
if _, ok := a.keyManager.(kmsapi.Decrypter); !ok { if _, ok := a.keyManager.(kmsapi.Decrypter); !ok {
@ -660,7 +545,7 @@ func (a *Authority) init() error {
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...) options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...)
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey, SigningKey: a.config.IntermediateKey,
Password: a.password, Password: []byte(a.password),
}) })
if err != nil { if err != nil {
return err return err
@ -669,7 +554,7 @@ func (a *Authority) init() error {
if km, ok := a.keyManager.(kmsapi.Decrypter); ok { if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{ options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: a.config.IntermediateKey, DecryptionKey: a.config.IntermediateKey,
Password: a.password, Password: []byte(a.password),
}) })
if err != nil { if err != nil {
return err return err
@ -684,19 +569,45 @@ func (a *Authority) init() error {
// TODO: mimick the x509CAService GetCertificateAuthority here too? // TODO: mimick the x509CAService GetCertificateAuthority here too?
} }
// Load X509 constraints engine. if a.config.AuthorityConfig.EnableAdmin {
// // Initialize step-ca Admin Database if it's not already initialized using
// This is currently only available in CA mode. // WithAdminDB.
if size := len(a.intermediateX509Certs); size > 0 { if a.adminDB == nil {
last := a.intermediateX509Certs[size-1] if linkedcaClient != nil {
constraintCerts := make([]*x509.Certificate, 0, size+1) a.adminDB = linkedcaClient
constraintCerts = append(constraintCerts, a.intermediateX509Certs...) } else {
for _, root := range a.rootX509Certs { a.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID)
if bytes.Equal(last.RawIssuer, root.RawSubject) && bytes.Equal(last.AuthorityKeyId, root.SubjectKeyId) { if err != nil {
constraintCerts = append(constraintCerts, root) return err
}
} }
} }
a.constraintsEngine = constraints.New(constraintCerts...)
provs, err := a.adminDB.GetProvisioners(ctx)
if err != nil {
return admin.WrapErrorISE(err, "error loading provisioners to initialize authority")
}
if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") {
// Create First Provisioner
prov, err := CreateFirstProvisioner(ctx, a.adminDB, string(a.password))
if err != nil {
return admin.WrapErrorISE(err, "error creating first provisioner")
}
// Create first admin
if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{
ProvisionerId: prov.Id,
Subject: "step",
Type: linkedca.Admin_SUPER_ADMIN,
}); err != nil {
return admin.WrapErrorISE(err, "error creating first admin")
}
}
}
// Load Provisioners and Admins
if err := a.ReloadAdminResources(ctx); err != nil {
return err
} }
// Load x509 and SSH Policy Engines // Load x509 and SSH Policy Engines
@ -716,18 +627,6 @@ func (a *Authority) init() error {
a.templates.Data["Step"] = tmplVars a.templates.Data["Step"] = tmplVars
} }
// Start the CRL generator, we can assume the configuration is validated.
if a.config.CRL.IsEnabled() {
// Default cache duration to the default one
if v := a.config.CRL.CacheDuration; v == nil || v.Duration <= 0 {
a.config.CRL.CacheDuration = config.DefaultCRLCacheDuration
}
// Start CRL generator
if err := a.startCRLGenerator(); err != nil {
return err
}
}
// JWT numeric dates are seconds. // JWT numeric dates are seconds.
a.startTime = time.Now().Truncate(time.Second) a.startTime = time.Now().Truncate(time.Second)
// Set flag indicating that initialization has been completed, and should // Set flag indicating that initialization has been completed, and should
@ -737,14 +636,6 @@ func (a *Authority) init() error {
return nil return nil
} }
// initLogf is used to log initialization information. The output
// can be disabled by starting the CA with the `--quiet` flag.
func (a *Authority) initLogf(format string, v ...any) {
if !a.quietInit {
log.Printf(format, v...)
}
}
// GetID returns the define authority id or a zero uuid. // GetID returns the define authority id or a zero uuid.
func (a *Authority) GetID() string { func (a *Authority) GetID() string {
const zeroUUID = "00000000-0000-0000-0000-000000000000" const zeroUUID = "00000000-0000-0000-0000-000000000000"
@ -794,11 +685,6 @@ func (a *Authority) IsAdminAPIEnabled() bool {
// Shutdown safely shuts down any clients, databases, etc. held by the Authority. // Shutdown safely shuts down any clients, databases, etc. held by the Authority.
func (a *Authority) Shutdown() error { func (a *Authority) Shutdown() error {
if a.crlTicker != nil {
a.crlTicker.Stop()
close(a.crlStopper)
}
if err := a.keyManager.Close(); err != nil { if err := a.keyManager.Close(); err != nil {
log.Printf("error closing the key manager: %v", err) log.Printf("error closing the key manager: %v", err)
} }
@ -807,11 +693,6 @@ func (a *Authority) Shutdown() error {
// CloseForReload closes internal services, to allow a safe reload. // CloseForReload closes internal services, to allow a safe reload.
func (a *Authority) CloseForReload() { func (a *Authority) CloseForReload() {
if a.crlTicker != nil {
a.crlTicker.Stop()
close(a.crlStopper)
}
if err := a.keyManager.Close(); err != nil { if err := a.keyManager.Close(); err != nil {
log.Printf("error closing the key manager: %v", err) log.Printf("error closing the key manager: %v", err)
} }
@ -852,49 +733,11 @@ func (a *Authority) requiresSCEPService() bool {
return false return false
} }
// GetSCEPService returns the configured SCEP Service. // GetSCEPService returns the configured SCEP Service
// // TODO: this function is intended to exist temporarily
// TODO: this function is intended to exist temporarily in order to make SCEP // in order to make SCEP work more easily. It can be
// work more easily. It can be made more correct by using the right // made more correct by using the right interfaces/abstractions
// interfaces/abstractions after it works as expected. // after it works as expected.
func (a *Authority) GetSCEPService() *scep.Service { func (a *Authority) GetSCEPService() *scep.Service {
return a.scepService return a.scepService
} }
func (a *Authority) startCRLGenerator() error {
if !a.config.CRL.IsEnabled() {
return nil
}
// Check that there is a valid CRL in the DB right now. If it doesn't exist
// or is expired, generate one now
_, ok := a.db.(db.CertificateRevocationListDB)
if !ok {
return errors.Errorf("CRL Generation requested, but database does not support CRL generation")
}
// Always create a new CRL on startup in case the CA has been down and the
// time to next expected CRL update is less than the cache duration.
if err := a.GenerateCertificateRevocationList(); err != nil {
return errors.Wrap(err, "could not generate a CRL")
}
a.crlStopper = make(chan struct{}, 1)
a.crlTicker = time.NewTicker(a.config.CRL.TickerDuration())
go func() {
for {
select {
case <-a.crlTicker.C:
log.Println("Regenerating CRL")
if err := a.GenerateCertificateRevocationList(); err != nil {
log.Printf("error regenerating the CRL: %v", err)
}
case <-a.crlStopper:
return
}
}
}()
return nil
}

View file

@ -6,10 +6,8 @@ import (
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex" "encoding/hex"
"encoding/pem"
"net" "net"
"os" "os"
"path/filepath"
"reflect" "reflect"
"testing" "testing"
"time" "time"
@ -20,7 +18,6 @@ import (
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/db" "github.com/smallstep/certificates/db"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/pemutil" "go.step.sm/crypto/pemutil"
) )
@ -175,130 +172,6 @@ func TestAuthorityNew(t *testing.T) {
} }
} }
func TestAuthorityNew_bundles(t *testing.T) {
ca0, err := minica.New()
if err != nil {
t.Fatal(err)
}
ca1, err := minica.New()
if err != nil {
t.Fatal(err)
}
ca2, err := minica.New()
if err != nil {
t.Fatal(err)
}
rootPath := t.TempDir()
writeCert := func(fn string, certs ...*x509.Certificate) error {
var b []byte
for _, crt := range certs {
b = append(b, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: crt.Raw,
})...)
}
return os.WriteFile(filepath.Join(rootPath, fn), b, 0600)
}
writeKey := func(fn string, signer crypto.Signer) error {
_, err := pemutil.Serialize(signer, pemutil.ToFile(filepath.Join(rootPath, fn), 0600))
return err
}
if err := writeCert("root0.crt", ca0.Root); err != nil {
t.Fatal(err)
}
if err := writeCert("int0.crt", ca0.Intermediate); err != nil {
t.Fatal(err)
}
if err := writeKey("int0.key", ca0.Signer); err != nil {
t.Fatal(err)
}
if err := writeCert("root1.crt", ca1.Root); err != nil {
t.Fatal(err)
}
if err := writeCert("int1.crt", ca1.Intermediate); err != nil {
t.Fatal(err)
}
if err := writeKey("int1.key", ca1.Signer); err != nil {
t.Fatal(err)
}
if err := writeCert("bundle0.crt", ca0.Root, ca1.Root); err != nil {
t.Fatal(err)
}
if err := writeCert("bundle1.crt", ca1.Root, ca2.Root); err != nil {
t.Fatal(err)
}
tests := []struct {
name string
config *config.Config
wantErr bool
}{
{"ok ca0", &config.Config{
Address: "127.0.0.1:443",
Root: []string{filepath.Join(rootPath, "root0.crt")},
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
IntermediateKey: filepath.Join(rootPath, "int0.key"),
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &AuthConfig{},
}, false},
{"ok bundle", &config.Config{
Address: "127.0.0.1:443",
Root: []string{filepath.Join(rootPath, "bundle0.crt")},
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
IntermediateKey: filepath.Join(rootPath, "int0.key"),
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &AuthConfig{},
}, false},
{"ok federated ca1", &config.Config{
Address: "127.0.0.1:443",
Root: []string{filepath.Join(rootPath, "root0.crt")},
FederatedRoots: []string{filepath.Join(rootPath, "root1.crt")},
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
IntermediateKey: filepath.Join(rootPath, "int0.key"),
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &AuthConfig{},
}, false},
{"ok federated bundle", &config.Config{
Address: "127.0.0.1:443",
Root: []string{filepath.Join(rootPath, "root0.crt")},
FederatedRoots: []string{filepath.Join(rootPath, "bundle1.crt")},
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
IntermediateKey: filepath.Join(rootPath, "int0.key"),
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &AuthConfig{},
}, false},
{"fail root", &config.Config{
Address: "127.0.0.1:443",
Root: []string{filepath.Join(rootPath, "missing.crt")},
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
IntermediateKey: filepath.Join(rootPath, "int0.key"),
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &AuthConfig{},
}, true},
{"fail federated", &config.Config{
Address: "127.0.0.1:443",
Root: []string{filepath.Join(rootPath, "root0.crt")},
FederatedRoots: []string{filepath.Join(rootPath, "missing.crt")},
IntermediateCert: filepath.Join(rootPath, "int0.crt"),
IntermediateKey: filepath.Join(rootPath, "int0.key"),
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &AuthConfig{},
}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := New(tt.config)
if (err != nil) != tt.wantErr {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestAuthority_GetDatabase(t *testing.T) { func TestAuthority_GetDatabase(t *testing.T) {
auth := testAuthority(t) auth := testAuthority(t)
authWithDatabase, err := New(auth.config, WithDatabase(auth.db)) authWithDatabase, err := New(auth.config, WithDatabase(auth.db))

View file

@ -12,7 +12,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs" "github.com/smallstep/certificates/errs"
@ -286,7 +285,7 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error {
// extra extension cannot be found, authorize the renewal by default. // extra extension cannot be found, authorize the renewal by default.
// //
// TODO(mariano): should we authorize by default? // TODO(mariano): should we authorize by default?
func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate) error { func (a *Authority) authorizeRenew(cert *x509.Certificate) error {
serial := cert.SerialNumber.String() serial := cert.SerialNumber.String()
var opts = []interface{}{errs.WithKeyVal("serialNumber", serial)} var opts = []interface{}{errs.WithKeyVal("serialNumber", serial)}
@ -308,14 +307,14 @@ func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate)
return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...) return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...)
} }
} }
if err := p.AuthorizeRenew(ctx, cert); err != nil { if err := p.AuthorizeRenew(context.Background(), cert); err != nil {
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...) return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
} }
return nil return nil
} }
// authorizeSSHCertificate returns an error if the given certificate is revoked. // authorizeSSHCertificate returns an error if the given certificate is revoked.
func (a *Authority) authorizeSSHCertificate(_ context.Context, cert *ssh.Certificate) error { func (a *Authority) authorizeSSHCertificate(ctx context.Context, cert *ssh.Certificate) error {
var err error var err error
var isRevoked bool var isRevoked bool
@ -394,7 +393,7 @@ func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error
// AuthorizeRenewToken validates the renew token and returns the leaf // AuthorizeRenewToken validates the renew token and returns the leaf
// certificate in the x5cInsecure header. // certificate in the x5cInsecure header.
func (a *Authority) AuthorizeRenewToken(_ context.Context, ott string) (*x509.Certificate, error) { func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error) {
var claims jose.Claims var claims jose.Claims
jwt, chain, err := jose.ParseX5cInsecure(ott, a.rootX509Certs) jwt, chain, err := jose.ParseX5cInsecure(ott, a.rootX509Certs)
if err != nil { if err != nil {
@ -417,16 +416,16 @@ func (a *Authority) AuthorizeRenewToken(_ context.Context, ott string) (*x509.Ce
Subject: leaf.Subject.CommonName, Subject: leaf.Subject.CommonName,
Time: time.Now().UTC(), Time: time.Now().UTC(),
}, time.Minute); err != nil { }, time.Minute); err != nil {
switch { switch err {
case errors.Is(err, jose.ErrInvalidIssuer): case jose.ErrInvalidIssuer:
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid issuer claim (iss)")) return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid issuer claim (iss)"))
case errors.Is(err, jose.ErrInvalidSubject): case jose.ErrInvalidSubject:
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid subject claim (sub)")) return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid subject claim (sub)"))
case errors.Is(err, jose.ErrNotValidYet): case jose.ErrNotValidYet:
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token not valid yet (nbf)")) return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token not valid yet (nbf)"))
case errors.Is(err, jose.ErrExpired): case jose.ErrExpired:
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token is expired (exp)")) return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token is expired (exp)"))
case errors.Is(err, jose.ErrIssuedInTheFuture): case jose.ErrIssuedInTheFuture:
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token issued in the future (iat)")) return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token issued in the future (iat)"))
default: default:
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token")) return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token"))
@ -434,7 +433,7 @@ func (a *Authority) AuthorizeRenewToken(_ context.Context, ott string) (*x509.Ce
} }
audiences := a.config.GetAudiences().Renew audiences := a.config.GetAudiences().Renew
if !matchesAudience(claims.Audience, audiences) && !isRAProvisioner(p) { if !matchesAudience(claims.Audience, audiences) {
return nil, errs.InternalServerErr(jose.ErrInvalidAudience, errs.WithMessage("error validating renew token: invalid audience claim (aud)")) return nil, errs.InternalServerErr(jose.ErrInvalidAudience, errs.WithMessage("error validating renew token: invalid audience claim (aud)"))
} }

View file

@ -313,8 +313,8 @@ func TestAuthority_authorizeToken(t *testing.T) {
p, err := tc.auth.authorizeToken(context.Background(), tc.token) p, err := tc.auth.authorizeToken(context.Background(), tc.token)
if err != nil { if err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var sc render.StatusCodedError sc, ok := err.(render.StatusCodedError)
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code) assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -399,8 +399,8 @@ func TestAuthority_authorizeRevoke(t *testing.T) {
if err := tc.auth.authorizeRevoke(context.Background(), tc.token); err != nil { if err := tc.auth.authorizeRevoke(context.Background(), tc.token); err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var sc render.StatusCodedError sc, ok := err.(render.StatusCodedError)
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code) assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -484,14 +484,14 @@ func TestAuthority_authorizeSign(t *testing.T) {
got, err := tc.auth.authorizeSign(context.Background(), tc.token) got, err := tc.auth.authorizeSign(context.Background(), tc.token)
if err != nil { if err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var sc render.StatusCodedError sc, ok := err.(render.StatusCodedError)
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code) assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
} else { } else {
if assert.Nil(t, tc.err) { if assert.Nil(t, tc.err) {
assert.Equals(t, 10, len(got)) // number of provisioner.SignOptions returned assert.Equals(t, 9, len(got)) // number of provisioner.SignOptions returned
} }
} }
}) })
@ -743,13 +743,13 @@ func TestAuthority_Authorize(t *testing.T) {
if err != nil { if err != nil {
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) { if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
assert.Nil(t, got) assert.Nil(t, got)
var sc render.StatusCodedError sc, ok := err.(render.StatusCodedError)
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code) assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
var ctxErr *errs.Error ctxErr, ok := err.(*errs.Error)
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error") assert.Fatal(t, ok, "error is not of type *errs.Error")
assert.Equals(t, ctxErr.Details["token"], tc.token) assert.Equals(t, ctxErr.Details["token"], tc.token)
} }
} else { } else {
@ -876,16 +876,16 @@ func TestAuthority_authorizeRenew(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
tc := genTestCase(t) tc := genTestCase(t)
err := tc.auth.authorizeRenew(context.Background(), tc.cert) err := tc.auth.authorizeRenew(tc.cert)
if err != nil { if err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var sc render.StatusCodedError sc, ok := err.(render.StatusCodedError)
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code) assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
var ctxErr *errs.Error ctxErr, ok := err.(*errs.Error)
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error") assert.Fatal(t, ok, "error is not of type *errs.Error")
assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String()) assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String())
} }
} else { } else {
@ -1027,14 +1027,14 @@ func TestAuthority_authorizeSSHSign(t *testing.T) {
got, err := tc.auth.authorizeSSHSign(context.Background(), tc.token) got, err := tc.auth.authorizeSSHSign(context.Background(), tc.token)
if err != nil { if err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var sc render.StatusCodedError sc, ok := err.(render.StatusCodedError)
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code) assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
} else { } else {
if assert.Nil(t, tc.err) { if assert.Nil(t, tc.err) {
assert.Len(t, 10, got) // number of provisioner.SignOptions returned assert.Len(t, 9, got) // number of provisioner.SignOptions returned
} }
} }
}) })
@ -1144,8 +1144,8 @@ func TestAuthority_authorizeSSHRenew(t *testing.T) {
got, err := tc.auth.authorizeSSHRenew(context.Background(), tc.token) got, err := tc.auth.authorizeSSHRenew(context.Background(), tc.token)
if err != nil { if err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var sc render.StatusCodedError sc, ok := err.(render.StatusCodedError)
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code) assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -1244,8 +1244,8 @@ func TestAuthority_authorizeSSHRevoke(t *testing.T) {
if err := tc.auth.authorizeSSHRevoke(context.Background(), tc.token); err != nil { if err := tc.auth.authorizeSSHRevoke(context.Background(), tc.token); err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var sc render.StatusCodedError sc, ok := err.(render.StatusCodedError)
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code) assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -1337,8 +1337,8 @@ func TestAuthority_authorizeSSHRekey(t *testing.T) {
cert, signOpts, err := tc.auth.authorizeSSHRekey(context.Background(), tc.token) cert, signOpts, err := tc.auth.authorizeSSHRekey(context.Background(), tc.token)
if err != nil { if err != nil {
if assert.NotNil(t, tc.err) { if assert.NotNil(t, tc.err) {
var sc render.StatusCodedError sc, ok := err.(render.StatusCodedError)
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code) assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error()) assert.HasPrefix(t, err.Error(), tc.err.Error())
} }
@ -1459,37 +1459,6 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) {
}) })
return nil return nil
})) }))
a4 := testAuthority(t)
a4.db = &db.MockAuthDB{
MUseToken: func(id, tok string) (bool, error) {
return true, nil
},
MGetCertificateData: func(serialNumber string) (*db.CertificateData, error) {
return &db.CertificateData{
Provisioner: &db.ProvisionerData{ID: "Max:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk", Name: "Max"},
RaInfo: &provisioner.RAInfo{ProvisionerName: "ra"},
}, nil
},
}
t4, c4 := generateX5cToken(a1, signer, jose.Claims{
Audience: []string{"https://ra.example.com/1.0/renew"},
Subject: "test.example.com",
Issuer: "step-ca-client/1.0",
NotBefore: jose.NewNumericDate(now),
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
cert.NotBefore = now
cert.NotAfter = now.Add(time.Hour)
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
if err != nil {
return err
}
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
Value: b,
})
return nil
}))
badSigner, _ := generateX5cToken(a1, otherSigner, jose.Claims{ badSigner, _ := generateX5cToken(a1, otherSigner, jose.Claims{
Audience: []string{"https://example.com/1.0/renew"}, Audience: []string{"https://example.com/1.0/renew"},
Subject: "test.example.com", Subject: "test.example.com",
@ -1658,7 +1627,6 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) {
{"ok", a1, args{ctx, t1}, c1, false}, {"ok", a1, args{ctx, t1}, c1, false},
{"ok expired cert", a1, args{ctx, t2}, c2, false}, {"ok expired cert", a1, args{ctx, t2}, c2, false},
{"ok provisioner issuer", a1, args{ctx, t3}, c3, false}, {"ok provisioner issuer", a1, args{ctx, t3}, c3, false},
{"ok ra provisioner", a4, args{ctx, t4}, c4, false},
{"fail token", a1, args{ctx, "not.a.token"}, nil, true}, {"fail token", a1, args{ctx, "not.a.token"}, nil, true},
{"fail token reuse", a1, args{ctx, t1}, nil, true}, {"fail token reuse", a1, args{ctx, t1}, nil, true},
{"fail token signature", a1, args{ctx, badSigner}, nil, true}, {"fail token signature", a1, args{ctx, badSigner}, nil, true},

View file

@ -35,13 +35,8 @@ var (
// DefaultEnableSSHCA enable SSH CA features per provisioner or globally // DefaultEnableSSHCA enable SSH CA features per provisioner or globally
// for all provisioners. // for all provisioners.
DefaultEnableSSHCA = false DefaultEnableSSHCA = false
// DefaultCRLCacheDuration is the default cache duration for the CRL. // GlobalProvisionerClaims default claims for the Authority. Can be overridden
DefaultCRLCacheDuration = &provisioner.Duration{Duration: 24 * time.Hour} // by provisioner specific claims.
// DefaultCRLExpiredDuration is the default duration in which expired
// certificates will remain in the CRL after expiration.
DefaultCRLExpiredDuration = time.Hour
// GlobalProvisionerClaims is the default duration that expired certificates
// remain in the CRL after expiration.
GlobalProvisionerClaims = provisioner.Claims{ GlobalProvisionerClaims = provisioner.Claims{
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
@ -77,62 +72,7 @@ type Config struct {
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
Templates *templates.Templates `json:"templates,omitempty"` Templates *templates.Templates `json:"templates,omitempty"`
CommonName string `json:"commonName,omitempty"` CommonName string `json:"commonName,omitempty"`
CRL *CRLConfig `json:"crl,omitempty"`
SkipValidation bool `json:"-"` SkipValidation bool `json:"-"`
NNSServer string `json:"nnsServer,omitempty"`
// Keeps record of the filename the Config is read from
loadedFromFilepath string
}
// CRLConfig represents config options for CRL generation
type CRLConfig struct {
Enabled bool `json:"enabled"`
GenerateOnRevoke bool `json:"generateOnRevoke,omitempty"`
CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"`
RenewPeriod *provisioner.Duration `json:"renewPeriod,omitempty"`
IDPurl string `json:"idpURL,omitempty"`
}
// IsEnabled returns if the CRL is enabled.
func (c *CRLConfig) IsEnabled() bool {
return c != nil && c.Enabled
}
// Validate validates the CRL configuration.
func (c *CRLConfig) Validate() error {
if c == nil {
return nil
}
if c.CacheDuration != nil && c.CacheDuration.Duration < 0 {
return errors.New("crl.cacheDuration must be greater than or equal to 0")
}
if c.RenewPeriod != nil && c.RenewPeriod.Duration < 0 {
return errors.New("crl.renewPeriod must be greater than or equal to 0")
}
if c.RenewPeriod != nil && c.CacheDuration != nil &&
c.RenewPeriod.Duration > c.CacheDuration.Duration {
return errors.New("crl.cacheDuration must be greater than or equal to crl.renewPeriod")
}
return nil
}
// TickerDuration the renewal ticker duration. This is set by renewPeriod, of it
// is not set is ~2/3 of cacheDuration.
func (c *CRLConfig) TickerDuration() time.Duration {
if !c.IsEnabled() {
return 0
}
if c.RenewPeriod != nil && c.RenewPeriod.Duration > 0 {
return c.RenewPeriod.Duration
}
return (c.CacheDuration.Duration / 3) * 2
} }
// ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer // ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer
@ -183,7 +123,7 @@ func (c *AuthConfig) init() {
} }
// Validate validates the authority configuration. // Validate validates the authority configuration.
func (c *AuthConfig) Validate(provisioner.Audiences) error { func (c *AuthConfig) Validate(audiences provisioner.Audiences) error {
if c == nil { if c == nil {
return errors.New("authority cannot be undefined") return errors.New("authority cannot be undefined")
} }
@ -223,10 +163,6 @@ func LoadConfiguration(filename string) (*Config, error) {
return nil, errors.Wrapf(err, "error parsing %s", filename) return nil, errors.Wrapf(err, "error parsing %s", filename)
} }
// store filename that was read to populate Config
c.loadedFromFilepath = filename
// initialize the Config
c.Init() c.Init()
return &c, nil return &c, nil
@ -247,9 +183,6 @@ func (c *Config) Init() {
if c.CommonName == "" { if c.CommonName == "" {
c.CommonName = "Step Online CA" c.CommonName = "Step Online CA"
} }
if c.CRL != nil && c.CRL.Enabled && c.CRL.CacheDuration == nil {
c.CRL.CacheDuration = DefaultCRLCacheDuration
}
c.AuthorityConfig.init() c.AuthorityConfig.init()
} }
@ -266,30 +199,6 @@ func (c *Config) Save(filename string) error {
return errors.Wrapf(enc.Encode(c), "error writing %s", filename) return errors.Wrapf(enc.Encode(c), "error writing %s", filename)
} }
// Commit saves the current configuration to the same
// file it was initially loaded from.
//
// TODO(hs): rename Save() to WriteTo() and replace this
// with Save()? Or is Commit clear enough.
func (c *Config) Commit() error {
if !c.WasLoadedFromFile() {
return errors.New("cannot commit configuration if not loaded from file")
}
return c.Save(c.loadedFromFilepath)
}
// WasLoadedFromFile returns whether or not the Config was
// loaded from a file.
func (c *Config) WasLoadedFromFile() bool {
return c.loadedFromFilepath != ""
}
// Filepath returns the path to the file the Config was
// loaded from.
func (c *Config) Filepath() string {
return c.loadedFromFilepath
}
// Validate validates the configuration. // Validate validates the configuration.
func (c *Config) Validate() error { func (c *Config) Validate() error {
switch { switch {
@ -360,11 +269,6 @@ func (c *Config) Validate() error {
return err return err
} }
// Validate crl config: nil is ok
if err := c.CRL.Validate(); err != nil {
return err
}
return c.AuthorityConfig.Validate(c.GetAudiences()) return c.AuthorityConfig.Validate(c.GetAudiences())
} }

View file

@ -169,7 +169,7 @@ func (t *TLSOptions) TLSConfig() *tls.Config {
rs = tls.RenegotiateNever rs = tls.RenegotiateNever
} }
//nolint:gosec // default MinVersion 1.2, if defined but empty 1.3 is used // nolint:gosec // default MinVersion 1.2, if defined but empty 1.3 is used
return &tls.Config{ return &tls.Config{
CipherSuites: t.CipherSuites.Value(), CipherSuites: t.CipherSuites.Value(),
MinVersion: t.MinVersion.Value(), MinVersion: t.MinVersion.Value(),

View file

@ -1,135 +0,0 @@
package constraints
import (
"crypto/x509"
"fmt"
"net"
"net/http"
"net/url"
"github.com/smallstep/certificates/errs"
)
// ConstraintError is the typed error that will be returned if a constraint
// error is found.
type ConstraintError struct {
Type string
Name string
Detail string
}
// Error implements the error interface.
func (e ConstraintError) Error() string {
return e.Detail
}
// As implements the As(any) bool interface and allows to use "errors.As()" to
// convert the ConstraintError to an errs.Error.
func (e ConstraintError) As(v any) bool {
if err, ok := v.(**errs.Error); ok {
*err = &errs.Error{
Status: http.StatusForbidden,
Msg: e.Detail,
Err: e,
}
return true
}
return false
}
// Engine implements a constraint validator for DNS names, IP addresses, Email
// addresses and URIs.
type Engine struct {
hasNameConstraints bool
permittedDNSDomains []string
excludedDNSDomains []string
permittedIPRanges []*net.IPNet
excludedIPRanges []*net.IPNet
permittedEmailAddresses []string
excludedEmailAddresses []string
permittedURIDomains []string
excludedURIDomains []string
}
// New creates a constraint validation engine that contains the given chain of
// certificates.
func New(chain ...*x509.Certificate) *Engine {
e := new(Engine)
for _, crt := range chain {
e.permittedDNSDomains = append(e.permittedDNSDomains, crt.PermittedDNSDomains...)
e.excludedDNSDomains = append(e.excludedDNSDomains, crt.ExcludedDNSDomains...)
e.permittedIPRanges = append(e.permittedIPRanges, crt.PermittedIPRanges...)
e.excludedIPRanges = append(e.excludedIPRanges, crt.ExcludedIPRanges...)
e.permittedEmailAddresses = append(e.permittedEmailAddresses, crt.PermittedEmailAddresses...)
e.excludedEmailAddresses = append(e.excludedEmailAddresses, crt.ExcludedEmailAddresses...)
e.permittedURIDomains = append(e.permittedURIDomains, crt.PermittedURIDomains...)
e.excludedURIDomains = append(e.excludedURIDomains, crt.ExcludedURIDomains...)
}
e.hasNameConstraints = len(e.permittedDNSDomains) > 0 || len(e.excludedDNSDomains) > 0 ||
len(e.permittedIPRanges) > 0 || len(e.excludedIPRanges) > 0 ||
len(e.permittedEmailAddresses) > 0 || len(e.excludedEmailAddresses) > 0 ||
len(e.permittedURIDomains) > 0 || len(e.excludedURIDomains) > 0
return e
}
// Validate checks the given names with the name constraints defined in the
// service.
func (e *Engine) Validate(dnsNames []string, ipAddresses []net.IP, emailAddresses []string, uris []*url.URL) error {
if e == nil || !e.hasNameConstraints {
return nil
}
for _, name := range dnsNames {
if err := checkNameConstraints("DNS name", name, name, e.permittedDNSDomains, e.excludedDNSDomains,
func(parsedName, constraint any) (bool, error) {
return matchDomainConstraint(parsedName.(string), constraint.(string))
},
); err != nil {
return err
}
}
for _, ip := range ipAddresses {
if err := checkNameConstraints("IP address", ip.String(), ip, e.permittedIPRanges, e.excludedIPRanges,
func(parsedName, constraint any) (bool, error) {
return matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))
},
); err != nil {
return err
}
}
for _, email := range emailAddresses {
mailbox, ok := parseRFC2821Mailbox(email)
if !ok {
return fmt.Errorf("cannot parse rfc822Name %q", email)
}
if err := checkNameConstraints("Email address", email, mailbox, e.permittedEmailAddresses, e.excludedEmailAddresses,
func(parsedName, constraint any) (bool, error) {
return matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string))
},
); err != nil {
return err
}
}
for _, uri := range uris {
if err := checkNameConstraints("URI", uri.String(), uri, e.permittedURIDomains, e.excludedURIDomains,
func(parsedName, constraint any) (bool, error) {
return matchURIConstraint(parsedName.(*url.URL), constraint.(string))
},
); err != nil {
return err
}
}
return nil
}
// ValidateCertificate validates the DNS names, IP addresses, Email addresses
// and URIs present in the given certificate.
func (e *Engine) ValidateCertificate(cert *x509.Certificate) error {
return e.Validate(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs)
}

View file

@ -1,334 +0,0 @@
package constraints
import (
"crypto/x509"
"net"
"net/url"
"reflect"
"testing"
"go.step.sm/crypto/minica"
)
func TestNew(t *testing.T) {
ca1, err := minica.New()
if err != nil {
t.Fatal(err)
}
ca2, err := minica.New(
minica.WithIntermediateTemplate(`{
"subject": {{ toJson .Subject }},
"keyUsage": ["certSign", "crlSign"],
"basicConstraints": {
"isCA": true,
"maxPathLen": 0
},
"nameConstraints": {
"critical": true,
"permittedDNSDomains": ["internal.example.org"],
"excludedDNSDomains": ["internal.example.com"],
"permittedIPRanges": ["192.168.1.0/24", "192.168.2.1/32"],
"excludedIPRanges": ["192.168.3.0/24", "192.168.4.0/28"],
"permittedEmailAddresses": ["root@example.org", "example.org", ".acme.org"],
"excludedEmailAddresses": ["root@example.com", "example.com", ".acme.com"],
"permittedURIDomains": ["host.example.org", ".acme.org"],
"excludedURIDomains": ["host.example.com", ".acme.com"]
}
}`),
)
if err != nil {
t.Fatal(err)
}
type args struct {
chain []*x509.Certificate
}
tests := []struct {
name string
args args
want *Engine
}{
{"ok", args{[]*x509.Certificate{ca1.Intermediate, ca1.Root}}, &Engine{
hasNameConstraints: false,
}},
{"ok with constraints", args{[]*x509.Certificate{ca2.Intermediate, ca2.Root}}, &Engine{
hasNameConstraints: true,
permittedDNSDomains: []string{"internal.example.org"},
excludedDNSDomains: []string{"internal.example.com"},
permittedIPRanges: []*net.IPNet{
{IP: net.ParseIP("192.168.1.0").To4(), Mask: net.IPMask{255, 255, 255, 0}},
{IP: net.ParseIP("192.168.2.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
},
excludedIPRanges: []*net.IPNet{
{IP: net.ParseIP("192.168.3.0").To4(), Mask: net.IPMask{255, 255, 255, 0}},
{IP: net.ParseIP("192.168.4.0").To4(), Mask: net.IPMask{255, 255, 255, 240}},
},
permittedEmailAddresses: []string{"root@example.org", "example.org", ".acme.org"},
excludedEmailAddresses: []string{"root@example.com", "example.com", ".acme.com"},
permittedURIDomains: []string{"host.example.org", ".acme.org"},
excludedURIDomains: []string{"host.example.com", ".acme.com"},
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := New(tt.args.chain...); !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want)
}
})
}
}
func TestNew_hasNameConstraints(t *testing.T) {
tests := []struct {
name string
fn func(c *x509.Certificate)
want bool
}{
{"no constraints", func(c *x509.Certificate) {}, false},
{"permittedDNSDomains", func(c *x509.Certificate) { c.PermittedDNSDomains = []string{"constraint"} }, true},
{"excludedDNSDomains", func(c *x509.Certificate) { c.ExcludedDNSDomains = []string{"constraint"} }, true},
{"permittedIPRanges", func(c *x509.Certificate) {
c.PermittedIPRanges = []*net.IPNet{{IP: net.ParseIP("192.168.3.0").To4(), Mask: net.IPMask{255, 255, 255, 0}}}
}, true},
{"excludedIPRanges", func(c *x509.Certificate) {
c.ExcludedIPRanges = []*net.IPNet{{IP: net.ParseIP("192.168.3.0").To4(), Mask: net.IPMask{255, 255, 255, 0}}}
}, true},
{"permittedEmailAddresses", func(c *x509.Certificate) { c.PermittedEmailAddresses = []string{"constraint"} }, true},
{"excludedEmailAddresses", func(c *x509.Certificate) { c.ExcludedEmailAddresses = []string{"constraint"} }, true},
{"permittedURIDomains", func(c *x509.Certificate) { c.PermittedURIDomains = []string{"constraint"} }, true},
{"excludedURIDomains", func(c *x509.Certificate) { c.ExcludedURIDomains = []string{"constraint"} }, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cert := &x509.Certificate{}
tt.fn(cert)
if e := New(cert); e.hasNameConstraints != tt.want {
t.Errorf("Engine.hasNameConstraints = %v, want %v", e.hasNameConstraints, tt.want)
}
})
}
}
func TestEngine_Validate(t *testing.T) {
type fields struct {
hasNameConstraints bool
permittedDNSDomains []string
excludedDNSDomains []string
permittedIPRanges []*net.IPNet
excludedIPRanges []*net.IPNet
permittedEmailAddresses []string
excludedEmailAddresses []string
permittedURIDomains []string
excludedURIDomains []string
}
type args struct {
dnsNames []string
ipAddresses []net.IP
emailAddresses []string
uris []*url.URL
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{"ok", fields{hasNameConstraints: false}, args{
dnsNames: []string{"example.com", "host.example.com"},
ipAddresses: []net.IP{{192, 168, 1, 1}, {0x26, 0x00, 0x1f, 0x1c, 0x47, 0x01, 0x9d, 0x00, 0xc3, 0xa7, 0x66, 0x94, 0x87, 0x0f, 0x20, 0x72}},
emailAddresses: []string{"root@example.com"},
uris: []*url.URL{{Scheme: "https", Host: "example.com", Path: "/uuid/c6d1a755-0c12-431e-9136-b64cb3173ec7"}},
}, false},
{"ok permitted dns", fields{
hasNameConstraints: true,
permittedDNSDomains: []string{"example.com"},
}, args{dnsNames: []string{"example.com", "www.example.com"}}, false},
{"ok not excluded dns", fields{
hasNameConstraints: true,
excludedDNSDomains: []string{"example.org"},
}, args{dnsNames: []string{"example.com", "www.example.com"}}, false},
{"ok permitted ip", fields{
hasNameConstraints: true,
permittedIPRanges: []*net.IPNet{
{IP: net.ParseIP("192.168.1.0"), Mask: net.IPMask{255, 255, 255, 0}},
{IP: net.ParseIP("192.168.2.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
{IP: net.ParseIP("2600:1700:22f8:2600:e559:bd88:350a:34d6"), Mask: net.IPMask{255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
},
}, args{ipAddresses: []net.IP{{192, 168, 1, 10}, {192, 168, 2, 1}, {0x26, 0x0, 0x17, 0x00, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc}}}, false},
{"ok not excluded ip", fields{
hasNameConstraints: true,
excludedIPRanges: []*net.IPNet{
{IP: net.ParseIP("192.168.1.0"), Mask: net.IPMask{255, 255, 255, 0}},
{IP: net.ParseIP("192.168.2.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
},
}, args{ipAddresses: []net.IP{{192, 168, 2, 2}, {192, 168, 3, 1}}}, false},
{"ok permitted emails", fields{
hasNameConstraints: true,
permittedEmailAddresses: []string{"root@example.com", "acme.org", ".acme.com"},
}, args{emailAddresses: []string{"root@example.com", "name@acme.org", "name@coyote.acme.com", `"(quoted)"@www.acme.com`}}, false},
{"ok not excluded emails", fields{
hasNameConstraints: true,
excludedEmailAddresses: []string{"root@example.com", "acme.org", ".acme.com"},
}, args{emailAddresses: []string{"name@example.com", "root@acme.com", "root@other.com"}}, false},
{"ok permitted uris", fields{
hasNameConstraints: true,
permittedURIDomains: []string{"example.com", ".acme.com"},
}, args{uris: []*url.URL{{Scheme: "https", Host: "example.com", Path: "/path"}, {Scheme: "https", Host: "www.acme.com", Path: "/path"}}}, false},
{"ok not excluded uris", fields{
hasNameConstraints: true,
excludedURIDomains: []string{"example.com", ".acme.com"},
}, args{uris: []*url.URL{{Scheme: "https", Host: "example.org", Path: "/path"}, {Scheme: "https", Host: "acme.com", Path: "/path"}}}, false},
{"fail permitted dns", fields{
hasNameConstraints: true,
permittedDNSDomains: []string{"example.com"},
}, args{dnsNames: []string{"www.example.com", "www.example.org"}}, true},
{"fail not excluded dns", fields{
hasNameConstraints: true,
excludedDNSDomains: []string{"example.org"},
}, args{dnsNames: []string{"example.com", "www.example.org"}}, true},
{"fail permitted ip", fields{
hasNameConstraints: true,
permittedIPRanges: []*net.IPNet{
{IP: net.ParseIP("192.168.1.0").To4(), Mask: net.IPMask{255, 255, 255, 0}},
{IP: net.ParseIP("192.168.2.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
},
}, args{ipAddresses: []net.IP{{192, 168, 1, 10}, {192, 168, 2, 10}}}, true},
{"fail not excluded ip", fields{
hasNameConstraints: true,
excludedIPRanges: []*net.IPNet{
{IP: net.ParseIP("192.168.1.0").To4(), Mask: net.IPMask{255, 255, 255, 0}},
{IP: net.ParseIP("192.168.2.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
},
}, args{ipAddresses: []net.IP{{192, 168, 2, 2}, {192, 168, 1, 1}}}, true},
{"fail permitted emails", fields{
hasNameConstraints: true,
permittedEmailAddresses: []string{"root@example.com", "acme.org", ".acme.com"},
}, args{emailAddresses: []string{"root@example.com", "name@acme.org", "name@acme.com"}}, true},
{"fail not excluded emails", fields{
hasNameConstraints: true,
excludedEmailAddresses: []string{"root@example.com", "acme.org", ".acme.com"},
}, args{emailAddresses: []string{"name@example.com", "root@example.com"}}, true},
{"fail permitted uris", fields{
hasNameConstraints: true,
permittedURIDomains: []string{"example.com", ".acme.com"},
}, args{uris: []*url.URL{{Scheme: "https", Host: "example.com", Path: "/path"}, {Scheme: "https", Host: "acme.com", Path: "/path"}}}, true},
{"fail not excluded uris", fields{
hasNameConstraints: true,
excludedURIDomains: []string{"example.com", ".acme.com"},
}, args{uris: []*url.URL{{Scheme: "https", Host: "www.example.com", Path: "/path"}, {Scheme: "https", Host: "acme.com", Path: "/path"}}}, true},
{"fail parse emails", fields{
hasNameConstraints: true,
permittedEmailAddresses: []string{"example.com"},
}, args{emailAddresses: []string{`(notquoted)@example.com`}}, true},
{"fail match dns", fields{
hasNameConstraints: true,
permittedDNSDomains: []string{"example.com"},
}, args{dnsNames: []string{`www.example.com.`}}, true},
{"fail match email", fields{
hasNameConstraints: true,
excludedEmailAddresses: []string{`(notquoted)@example.com`},
}, args{emailAddresses: []string{`ok@example.com`}}, true},
{"fail match uri", fields{
hasNameConstraints: true,
permittedURIDomains: []string{"example.com"},
}, args{uris: []*url.URL{{Scheme: "urn", Opaque: "uuid:36efb1ae-6617-4b23-b799-874a37aaea1c"}}}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &Engine{
hasNameConstraints: tt.fields.hasNameConstraints,
permittedDNSDomains: tt.fields.permittedDNSDomains,
excludedDNSDomains: tt.fields.excludedDNSDomains,
permittedIPRanges: tt.fields.permittedIPRanges,
excludedIPRanges: tt.fields.excludedIPRanges,
permittedEmailAddresses: tt.fields.permittedEmailAddresses,
excludedEmailAddresses: tt.fields.excludedEmailAddresses,
permittedURIDomains: tt.fields.permittedURIDomains,
excludedURIDomains: tt.fields.excludedURIDomains,
}
if err := e.Validate(tt.args.dnsNames, tt.args.ipAddresses, tt.args.emailAddresses, tt.args.uris); (err != nil) != tt.wantErr {
t.Errorf("service.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestEngine_Validate_nil(t *testing.T) {
var e *Engine
if err := e.Validate([]string{"www.example.com"}, nil, nil, nil); err != nil {
t.Errorf("service.Validate() error = %v, wantErr false", err)
}
}
func TestEngine_ValidateCertificate(t *testing.T) {
type fields struct {
hasNameConstraints bool
permittedDNSDomains []string
excludedDNSDomains []string
permittedIPRanges []*net.IPNet
excludedIPRanges []*net.IPNet
permittedEmailAddresses []string
excludedEmailAddresses []string
permittedURIDomains []string
excludedURIDomains []string
}
type args struct {
cert *x509.Certificate
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{"ok", fields{hasNameConstraints: false}, args{&x509.Certificate{
DNSNames: []string{"example.com"},
IPAddresses: []net.IP{{127, 0, 0, 1}},
EmailAddresses: []string{"info@example.com"},
URIs: []*url.URL{{Scheme: "https", Host: "uuid.example.com", Path: "/dc4c76b5-5262-4551-a881-48094a604d63"}},
}}, false},
{"ok with constraints", fields{
hasNameConstraints: true,
permittedDNSDomains: []string{"example.com"},
permittedIPRanges: []*net.IPNet{
{IP: net.ParseIP("127.0.0.1").To4(), Mask: net.IPMask{255, 255, 255, 255}},
{IP: net.ParseIP("10.3.0.0").To4(), Mask: net.IPMask{255, 255, 0, 0}},
},
permittedEmailAddresses: []string{"example.com"},
permittedURIDomains: []string{".example.com"},
}, args{&x509.Certificate{
DNSNames: []string{"www.example.com"},
IPAddresses: []net.IP{{127, 0, 0, 1}, {10, 3, 1, 1}},
EmailAddresses: []string{"info@example.com"},
URIs: []*url.URL{{Scheme: "https", Host: "uuid.example.com", Path: "/dc4c76b5-5262-4551-a881-48094a604d63"}},
}}, false},
{"fail", fields{
hasNameConstraints: true,
permittedURIDomains: []string{".example.com"},
}, args{&x509.Certificate{
DNSNames: []string{"example.com"},
IPAddresses: []net.IP{{127, 0, 0, 1}},
EmailAddresses: []string{"info@example.com"},
URIs: []*url.URL{{Scheme: "https", Host: "uuid.example.org", Path: "/dc4c76b5-5262-4551-a881-48094a604d63"}},
}}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &Engine{
hasNameConstraints: tt.fields.hasNameConstraints,
permittedDNSDomains: tt.fields.permittedDNSDomains,
excludedDNSDomains: tt.fields.excludedDNSDomains,
permittedIPRanges: tt.fields.permittedIPRanges,
excludedIPRanges: tt.fields.excludedIPRanges,
permittedEmailAddresses: tt.fields.permittedEmailAddresses,
excludedEmailAddresses: tt.fields.excludedEmailAddresses,
permittedURIDomains: tt.fields.permittedURIDomains,
excludedURIDomains: tt.fields.excludedURIDomains,
}
if err := e.ValidateCertificate(tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("Engine.ValidateCertificate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -1,383 +0,0 @@
// Copyright (c) 2009 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package constraints
import (
"bytes"
"fmt"
"net"
"net/url"
"reflect"
"strings"
)
func checkNameConstraints(nameType, name string, parsedName, permitted, excluded any, match func(name, constraint any) (bool, error)) error {
excludedValue := reflect.ValueOf(excluded)
for i := 0; i < excludedValue.Len(); i++ {
constraint := excludedValue.Index(i).Interface()
match, err := match(parsedName, constraint)
if err != nil {
return ConstraintError{
Type: nameType,
Name: name,
Detail: err.Error(),
}
}
if match {
return ConstraintError{
Type: nameType,
Name: name,
Detail: fmt.Sprintf("%s %q is excluded by constraint %q", nameType, name, constraint),
}
}
}
var (
err error
ok = true
)
permittedValue := reflect.ValueOf(permitted)
for i := 0; i < permittedValue.Len(); i++ {
constraint := permittedValue.Index(i).Interface()
if ok, err = match(parsedName, constraint); err != nil {
return ConstraintError{
Type: nameType,
Name: name,
Detail: err.Error(),
}
}
if ok {
break
}
}
if !ok {
return ConstraintError{
Type: nameType,
Name: name,
Detail: fmt.Sprintf("%s %q is not permitted by any constraint", nameType, name),
}
}
return nil
}
func matchDomainConstraint(domain, constraint string) (bool, error) {
// The meaning of zero length constraints is not specified, but this
// code follows NSS and accepts them as matching everything.
if constraint == "" {
return true, nil
}
domainLabels, ok := domainToReverseLabels(domain)
if !ok {
return false, fmt.Errorf("internal error: cannot parse domain %q", domain)
}
// RFC 5280 says that a leading period in a domain name means that at least
// one label must be prepended, but only for URI and email constraints, not
// DNS constraints. The code also supports that behavior for DNS
// constraints.
mustHaveSubdomains := false
if constraint[0] == '.' {
mustHaveSubdomains = true
constraint = constraint[1:]
}
constraintLabels, ok := domainToReverseLabels(constraint)
if !ok {
return false, fmt.Errorf("internal error: cannot parse domain %q", constraint)
}
if len(domainLabels) < len(constraintLabels) ||
(mustHaveSubdomains && len(domainLabels) == len(constraintLabels)) {
return false, nil
}
for i, constraintLabel := range constraintLabels {
if !strings.EqualFold(constraintLabel, domainLabels[i]) {
return false, nil
}
}
return true, nil
}
func normalizeIP(ip net.IP) net.IP {
if ip4 := ip.To4(); ip4 != nil {
return ip4
}
return ip
}
func matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {
ip = normalizeIP(ip)
constraintIP := normalizeIP(constraint.IP)
if len(ip) != len(constraintIP) {
return false, nil
}
for i := range ip {
if mask := constraint.Mask[i]; ip[i]&mask != constraintIP[i]&mask {
return false, nil
}
}
return true, nil
}
func matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) {
// If the constraint contains an @, then it specifies an exact mailbox
// name.
if strings.Contains(constraint, "@") {
constraintMailbox, ok := parseRFC2821Mailbox(constraint)
if !ok {
return false, fmt.Errorf("internal error: cannot parse constraint %q", constraint)
}
return mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil
}
// Otherwise the constraint is like a DNS constraint of the domain part
// of the mailbox.
return matchDomainConstraint(mailbox.domain, constraint)
}
func matchURIConstraint(uri *url.URL, constraint string) (bool, error) {
// From RFC 5280, Section 4.2.1.10:
// “a uniformResourceIdentifier that does not include an authority
// component with a host name specified as a fully qualified domain
// name (e.g., if the URI either does not include an authority
// component or includes an authority component in which the host name
// is specified as an IP address), then the application MUST reject the
// certificate.”
host := uri.Host
if host == "" {
return false, fmt.Errorf("URI with empty host (%q) cannot be matched against constraints", uri.String())
}
if strings.Contains(host, ":") && !strings.HasSuffix(host, "]") {
var err error
host, _, err = net.SplitHostPort(uri.Host)
if err != nil {
return false, err
}
}
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") ||
net.ParseIP(host) != nil {
return false, fmt.Errorf("URI with IP (%q) cannot be matched against constraints", uri.String())
}
return matchDomainConstraint(host, constraint)
}
// domainToReverseLabels converts a textual domain name like foo.example.com to
// the list of labels in reverse order, e.g. ["com", "example", "foo"].
func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
for len(domain) > 0 {
if i := strings.LastIndexByte(domain, '.'); i == -1 {
reverseLabels = append(reverseLabels, domain)
domain = ""
} else {
reverseLabels = append(reverseLabels, domain[i+1:])
domain = domain[:i]
}
}
if len(reverseLabels) > 0 && reverseLabels[0] == "" {
// An empty label at the end indicates an absolute value.
return nil, false
}
for _, label := range reverseLabels {
if label == "" {
// Empty labels are otherwise invalid.
return nil, false
}
for _, c := range label {
if c < 33 || c > 126 {
// Invalid character.
return nil, false
}
}
}
return reverseLabels, true
}
// rfc2821Mailbox represents a “mailbox” (which is an email address to most
// people) by breaking it into the “local” (i.e. before the '@') and “domain”
// parts.
type rfc2821Mailbox struct {
local, domain string
}
// parseRFC2821Mailbox parses an email address into local and domain parts,
// based on the ABNF for a “Mailbox” from RFC 2821. According to RFC 5280,
// Section 4.2.1.6 that's correct for an rfc822Name from a certificate: “The
// format of an rfc822Name is a "Mailbox" as defined in RFC 2821, Section 4.1.2”.
func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
if in == "" {
return mailbox, false
}
localPartBytes := make([]byte, 0, len(in)/2)
if in[0] == '"' {
// Quoted-string = DQUOTE *qcontent DQUOTE
// non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127
// qcontent = qtext / quoted-pair
// qtext = non-whitespace-control /
// %d33 / %d35-91 / %d93-126
// quoted-pair = ("\" text) / obs-qp
// text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text
//
// (Names beginning with “obs-” are the obsolete syntax from RFC 2822,
// Section 4. Since it has been 16 years, we no longer accept that.)
in = in[1:]
QuotedString:
for {
if in == "" {
return mailbox, false
}
c := in[0]
in = in[1:]
switch {
case c == '"':
break QuotedString
case c == '\\':
// quoted-pair
if in == "" {
return mailbox, false
}
if in[0] == 11 ||
in[0] == 12 ||
(1 <= in[0] && in[0] <= 9) ||
(14 <= in[0] && in[0] <= 127) {
localPartBytes = append(localPartBytes, in[0])
in = in[1:]
} else {
return mailbox, false
}
case c == 11 ||
c == 12 ||
// Space (char 32) is not allowed based on the
// BNF, but RFC 3696 gives an example that
// assumes that it is. Several “verified”
// errata continue to argue about this point.
// We choose to accept it.
c == 32 ||
c == 33 ||
c == 127 ||
(1 <= c && c <= 8) ||
(14 <= c && c <= 31) ||
(35 <= c && c <= 91) ||
(93 <= c && c <= 126):
// qtext
localPartBytes = append(localPartBytes, c)
default:
return mailbox, false
}
}
} else {
// Atom ("." Atom)*
NextChar:
for len(in) > 0 {
// atext from RFC 2822, Section 3.2.4
c := in[0]
switch {
case c == '\\':
// Examples given in RFC 3696 suggest that
// escaped characters can appear outside of a
// quoted string. Several “verified” errata
// continue to argue the point. We choose to
// accept it.
in = in[1:]
if in == "" {
return mailbox, false
}
fallthrough
case ('0' <= c && c <= '9') ||
('a' <= c && c <= 'z') ||
('A' <= c && c <= 'Z') ||
c == '!' || c == '#' || c == '$' || c == '%' ||
c == '&' || c == '\'' || c == '*' || c == '+' ||
c == '-' || c == '/' || c == '=' || c == '?' ||
c == '^' || c == '_' || c == '`' || c == '{' ||
c == '|' || c == '}' || c == '~' || c == '.':
localPartBytes = append(localPartBytes, in[0])
in = in[1:]
default:
break NextChar
}
}
if len(localPartBytes) == 0 {
return mailbox, false
}
// From RFC 3696, Section 3:
// “period (".") may also appear, but may not be used to start
// or end the local part, nor may two or more consecutive
// periods appear.”
twoDots := []byte{'.', '.'}
if localPartBytes[0] == '.' ||
localPartBytes[len(localPartBytes)-1] == '.' ||
bytes.Contains(localPartBytes, twoDots) {
return mailbox, false
}
}
if in == "" || in[0] != '@' {
return mailbox, false
}
in = in[1:]
// The RFC species a format for domains, but that's known to be
// violated in practice so we accept that anything after an '@' is the
// domain part.
if _, ok := domainToReverseLabels(in); !ok {
return mailbox, false
}
mailbox.local = string(localPartBytes)
mailbox.domain = in
return mailbox, true
}

View file

@ -265,20 +265,8 @@ func (c *linkedCaClient) GetCertificateData(serial string) (*db.CertificateData,
ID: p.Id, Name: p.Name, Type: p.Type.String(), ID: p.Id, Name: p.Name, Type: p.Type.String(),
} }
} }
var raInfo *provisioner.RAInfo
if p := resp.RaProvisioner; p != nil && p.Provisioner != nil {
raInfo = &provisioner.RAInfo{
AuthorityID: p.AuthorityId,
ProvisionerID: p.Provisioner.Id,
ProvisionerType: p.Provisioner.Type.String(),
ProvisionerName: p.Provisioner.Name,
}
}
return &db.CertificateData{ return &db.CertificateData{
Provisioner: pd, Provisioner: pd,
RaInfo: raInfo,
}, nil }, nil
} }
@ -290,7 +278,6 @@ func (c *linkedCaClient) StoreCertificateChain(p provisioner.Interface, fullchai
PemCertificate: serializeCertificateChain(fullchain[0]), PemCertificate: serializeCertificateChain(fullchain[0]),
PemCertificateChain: serializeCertificateChain(fullchain[1:]...), PemCertificateChain: serializeCertificateChain(fullchain[1:]...),
Provisioner: createProvisionerIdentity(p), Provisioner: createProvisionerIdentity(p),
AttestationData: createAttestationData(p),
RaProvisioner: raProvisioner, RaProvisioner: raProvisioner,
EndpointId: endpointID, EndpointId: endpointID,
}) })
@ -381,19 +368,19 @@ func (c *linkedCaClient) IsSSHRevoked(serial string) (bool, error) {
return resp.Status != linkedca.RevocationStatus_ACTIVE, nil return resp.Status != linkedca.RevocationStatus_ACTIVE, nil
} }
func (c *linkedCaClient) CreateAuthorityPolicy(_ context.Context, _ *linkedca.Policy) error { func (c *linkedCaClient) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
return errors.New("not implemented yet") return errors.New("not implemented yet")
} }
func (c *linkedCaClient) GetAuthorityPolicy(context.Context) (*linkedca.Policy, error) { func (c *linkedCaClient) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
return nil, errors.New("not implemented yet") return nil, errors.New("not implemented yet")
} }
func (c *linkedCaClient) UpdateAuthorityPolicy(_ context.Context, _ *linkedca.Policy) error { func (c *linkedCaClient) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
return errors.New("not implemented yet") return errors.New("not implemented yet")
} }
func (c *linkedCaClient) DeleteAuthorityPolicy(context.Context) error { func (c *linkedCaClient) DeleteAuthorityPolicy(ctx context.Context) error {
return errors.New("not implemented yet") return errors.New("not implemented yet")
} }
@ -408,32 +395,24 @@ func createProvisionerIdentity(p provisioner.Interface) *linkedca.ProvisionerIde
} }
} }
func createRegistrationAuthorityProvisioner(p provisioner.Interface) (*linkedca.RegistrationAuthorityProvisioner, string) { type raProvisioner interface {
if rap, ok := p.(raProvisioner); ok { RAInfo() *provisioner.RAInfo
if info := rap.RAInfo(); info != nil {
typ := linkedca.Provisioner_Type_value[strings.ToUpper(info.ProvisionerType)]
return &linkedca.RegistrationAuthorityProvisioner{
AuthorityId: info.AuthorityID,
Provisioner: &linkedca.ProvisionerIdentity{
Id: info.ProvisionerID,
Type: linkedca.Provisioner_Type(typ),
Name: info.ProvisionerName,
},
}, info.EndpointID
}
}
return nil, ""
} }
func createAttestationData(p provisioner.Interface) *linkedca.AttestationData { func createRegistrationAuthorityProvisioner(p provisioner.Interface) (*linkedca.RegistrationAuthorityProvisioner, string) {
if ap, ok := p.(attProvisioner); ok { if rap, ok := p.(raProvisioner); ok {
if data := ap.AttestationData(); data != nil { info := rap.RAInfo()
return &linkedca.AttestationData{ typ := linkedca.Provisioner_Type_value[strings.ToUpper(info.ProvisionerType)]
PermanentIdentifier: data.PermanentIdentifier, return &linkedca.RegistrationAuthorityProvisioner{
} AuthorityId: info.AuthorityID,
} Provisioner: &linkedca.ProvisionerIdentity{
Id: info.ProvisionerID,
Type: linkedca.Provisioner_Type(typ),
Name: info.ProvisionerName,
},
}, info.EndpointID
} }
return nil return nil, ""
} }
func serializeCertificate(crt *x509.Certificate) string { func serializeCertificate(crt *x509.Certificate) string {
@ -482,7 +461,7 @@ func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error)
defer cancel() defer cancel()
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
//nolint:gosec // used in bootstrap protocol // nolint:gosec // used in bootstrap protocol
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check] InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
}))) })))
if err != nil { if err != nil {

View file

@ -5,7 +5,6 @@ import (
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"net/http"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
@ -86,22 +85,6 @@ func WithDatabase(d db.AuthDB) Option {
} }
} }
// WithQuietInit disables log output when the authority is initialized.
func WithQuietInit() Option {
return func(a *Authority) error {
a.quietInit = true
return nil
}
}
// WithWebhookClient sets the http.Client to be used for outbound requests.
func WithWebhookClient(c *http.Client) Option {
return func(a *Authority) error {
a.webhookClient = c
return nil
}
}
// WithGetIdentityFunc sets a custom function to retrieve the identity from // WithGetIdentityFunc sets a custom function to retrieve the identity from
// an external resource. // an external resource.
func WithGetIdentityFunc(fn func(ctx context.Context, p provisioner.Interface, email string) (*provisioner.Identity, error)) Option { func WithGetIdentityFunc(fn func(ctx context.Context, p provisioner.Interface, email string) (*provisioner.Identity, error)) Option {
@ -168,23 +151,16 @@ func WithKeyManager(k kms.KeyManager) Option {
// WithX509Signer defines the signer used to sign X509 certificates. // WithX509Signer defines the signer used to sign X509 certificates.
func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option { func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option {
return WithX509SignerChain([]*x509.Certificate{crt}, s)
}
// WithX509SignerChain defines the signer used to sign X509 certificates. This
// option is similar to WithX509Signer but it supports a chain of intermediates.
func WithX509SignerChain(issuerChain []*x509.Certificate, s crypto.Signer) Option {
return func(a *Authority) error { return func(a *Authority) error {
srv, err := cas.New(context.Background(), casapi.Options{ srv, err := cas.New(context.Background(), casapi.Options{
Type: casapi.SoftCAS, Type: casapi.SoftCAS,
Signer: s, Signer: s,
CertificateChain: issuerChain, CertificateChain: []*x509.Certificate{crt},
}) })
if err != nil { if err != nil {
return err return err
} }
a.x509CAService = srv a.x509CAService = srv
a.intermediateX509Certs = append(a.intermediateX509Certs, issuerChain...)
return nil return nil
} }
} }
@ -257,25 +233,6 @@ func WithX509FederatedCerts(certs ...*x509.Certificate) Option {
} }
} }
// WithX509IntermediateCerts is an option that allows to define the list of
// intermediate certificates that the CA will be using. This option will replace
// any intermediate certificate defined before.
//
// Note that these certificates will not be bundled with the certificates signed
// by the CA, because the CAS service will take care of that. They should match,
// but that's not guaranteed. These certificates will be mainly used for name
// constraint validation before a certificate is issued.
//
// This option should only be used on specific configurations, for example when
// WithX509SignerFunc is used, as we don't know the list of intermediates in
// advance.
func WithX509IntermediateCerts(intermediateCerts ...*x509.Certificate) Option {
return func(a *Authority) error {
a.intermediateX509Certs = intermediateCerts
return nil
}
}
// WithX509RootBundle is an option that allows to define the list of root // WithX509RootBundle is an option that allows to define the list of root
// certificates. This option will replace any root certificate defined before. // certificates. This option will replace any root certificate defined before.
func WithX509RootBundle(pemCerts []byte) Option { func WithX509RootBundle(pemCerts []byte) Option {

View file

@ -119,6 +119,7 @@ func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error {
} }
func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *linkedca.Admin, p *linkedca.Policy) error { func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *linkedca.Admin, p *linkedca.Policy) error {
// no policy and thus nothing to evaluate; return early // no policy and thus nothing to evaluate; return early
if p == nil { if p == nil {
return nil return nil
@ -137,6 +138,7 @@ func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *link
} }
func (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string, p *linkedca.Policy) error { func (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string, p *linkedca.Policy) error {
// no policy and thus nothing to evaluate; return early // no policy and thus nothing to evaluate; return early
if p == nil { if p == nil {
return nil return nil
@ -154,7 +156,8 @@ func (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string,
// checkPolicy checks if a new or updated policy configuration results in the user // checkPolicy checks if a new or updated policy configuration results in the user
// locking themselves or other admins out of the CA. // locking themselves or other admins out of the CA.
func (a *Authority) checkPolicy(_ context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error { func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error {
// convert the policy; return early if nil // convert the policy; return early if nil
policyOptions := authPolicy.LinkedToCertificates(p) policyOptions := authPolicy.LinkedToCertificates(p)
if policyOptions == nil { if policyOptions == nil {
@ -213,6 +216,7 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error {
) )
if a.config.AuthorityConfig.EnableAdmin { if a.config.AuthorityConfig.EnableAdmin {
// temporarily disable policy loading when LinkedCA is in use // temporarily disable policy loading when LinkedCA is in use
if _, ok := a.adminDB.(*linkedCaClient); ok { if _, ok := a.adminDB.(*linkedCaClient); ok {
return nil return nil
@ -248,7 +252,7 @@ func isAllowed(engine authPolicy.X509Policy, sans []string) error {
if isNamePolicyError && policyErr.Reason == policy.NotAllowed { if isNamePolicyError && policyErr.Reason == policy.NotAllowed {
return &PolicyError{ return &PolicyError{
Typ: AdminLockOut, Typ: AdminLockOut,
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), 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),
} }
} }
return &PolicyError{ return &PolicyError{

View file

@ -17,9 +17,9 @@ type Engine struct {
// New returns a new Engine using Options. // New returns a new Engine using Options.
func New(options *Options) (*Engine, error) { func New(options *Options) (*Engine, error) {
// if no options provided, return early // if no options provided, return early
if options == nil { if options == nil {
//nolint:nilnil // legacy
return nil, nil return nil, nil
} }
@ -56,6 +56,7 @@ func New(options *Options) (*Engine, error) {
// the X.509 policy (if available) and returns an error if one of the // the X.509 policy (if available) and returns an error if one of the
// names in the certificate is not allowed. // names in the certificate is not allowed.
func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error { func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {
// return early if there's no policy to evaluate // return early if there's no policy to evaluate
if e == nil || e.x509Policy == nil { if e == nil || e.x509Policy == nil {
return nil return nil
@ -68,6 +69,7 @@ func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {
// AreSANsAllowed evaluates the slice of SANs against the X.509 policy // AreSANsAllowed evaluates the slice of SANs against the X.509 policy
// (if available) and returns an error if one of the SANs is not allowed. // (if available) and returns an error if one of the SANs is not allowed.
func (e *Engine) AreSANsAllowed(sans []string) error { func (e *Engine) AreSANsAllowed(sans []string) error {
// return early if there's no policy to evaluate // return early if there's no policy to evaluate
if e == nil || e.x509Policy == nil { if e == nil || e.x509Policy == nil {
return nil return nil
@ -81,6 +83,7 @@ func (e *Engine) AreSANsAllowed(sans []string) error {
// user or host policy (if configured) and returns an error if one of the // user or host policy (if configured) and returns an error if one of the
// principals in the certificate is not allowed. // principals in the certificate is not allowed.
func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error { func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
// return early if there's no policy to evaluate // return early if there's no policy to evaluate
if e == nil || (e.sshHostPolicy == nil && e.sshUserPolicy == nil) { if e == nil || (e.sshHostPolicy == nil && e.sshUserPolicy == nil) {
return nil return nil

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