forked from TrueCloudLab/certificates
Merge branch 'master' into herman/acme-da-tpm
This commit is contained in:
commit
4cf25ede24
196 changed files with 8700 additions and 2755 deletions
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
# 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
4
.github/labeler.yml
vendored
|
@ -1,4 +0,0 @@
|
|||
needs triage:
|
||||
- '**' # index.php | src/main.php
|
||||
- '.*' # .gitignore
|
||||
- '.*/**' # .github/workflows/label.yml
|
26
.github/workflows/ci.yml
vendored
Normal file
26
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
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:
|
||||
os-dependencies: "libpcsclite-dev"
|
||||
run-gitleaks: true
|
||||
run-codeql: true
|
||||
secrets: inherit
|
9
.github/workflows/code-scan-cron.yml
vendored
Normal file
9
.github/workflows/code-scan-cron.yml
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
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
72
.github/workflows/codeql-analysis.yml
vendored
|
@ -1,72 +0,0 @@
|
|||
# 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
|
169
.github/workflows/release.yml
vendored
169
.github/workflows/release.yml
vendored
|
@ -7,63 +7,40 @@ on:
|
|||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Lint, Test, Build
|
||||
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
|
||||
ci:
|
||||
uses: smallstep/certificates/.github/workflows/ci.yml@master
|
||||
secrets: inherit
|
||||
|
||||
create_release:
|
||||
name: Create Release
|
||||
needs: test
|
||||
runs-on: ubuntu-20.04
|
||||
needs: ci
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_IMAGE: smallstep/step-ca
|
||||
outputs:
|
||||
debversion: ${{ steps.extract-tag.outputs.DEB_VERSION }}
|
||||
version: ${{ steps.extract-tag.outputs.VERSION }}
|
||||
is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
|
||||
docker_tags: ${{ env.DOCKER_TAGS }}
|
||||
steps:
|
||||
-
|
||||
name: Extract Tag Names
|
||||
id: extract-tag
|
||||
run: |
|
||||
DEB_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | sed 's/-/./')
|
||||
echo "::set-output name=DEB_VERSION::${DEB_VERSION}"
|
||||
-
|
||||
name: Is Pre-release
|
||||
- name: Is Pre-release
|
||||
id: is_prerelease
|
||||
run: |
|
||||
set +e
|
||||
echo ${{ github.ref }} | grep "\-rc.*"
|
||||
OUT=$?
|
||||
if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi
|
||||
echo "::set-output name=IS_PRERELEASE::${IS_PRERELEASE}"
|
||||
-
|
||||
name: Create Release
|
||||
echo "IS_PRERELEASE=${IS_PRERELEASE}" >> ${GITHUB_OUTPUT}
|
||||
- name: Extract Tag Names
|
||||
id: extract-tag
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
|
||||
echo "DOCKER_TAGS=${{ env.DOCKER_IMAGE }}:${VERSION}" >> ${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}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
|
@ -76,88 +53,48 @@ jobs:
|
|||
|
||||
goreleaser:
|
||||
name: Upload Assets To Github w/ goreleaser
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: create_release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
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
|
||||
check-latest: true
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v2
|
||||
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
|
||||
cosign-release: 'v1.13.1'
|
||||
- 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
|
||||
echo "RELEASE_DATE=${RELEASE_DATE}" >> ${GITHUB_ENV}
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
version: 'v1.7.0'
|
||||
version: 'latest'
|
||||
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 }}
|
||||
GITHUB_TOKEN: ${{ secrets.GORELEASER_PAT }}
|
||||
RELEASE_DATE: ${{ env.RELEASE_DATE }}
|
||||
COSIGN_EXPERIMENTAL: 1
|
||||
|
||||
build_upload_docker:
|
||||
name: Build & Upload Docker Images
|
||||
runs-on: ubuntu-20.04
|
||||
needs: test
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.19'
|
||||
-
|
||||
name: Install cosign
|
||||
uses: sigstore/cosign-installer@v1.1.0
|
||||
with:
|
||||
cosign-release: 'v1.1.0'
|
||||
-
|
||||
name: Write cosign key to disk
|
||||
id: write_key
|
||||
run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key"
|
||||
-
|
||||
name: Build
|
||||
id: build
|
||||
run: |
|
||||
PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin
|
||||
make docker-artifacts
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
||||
needs: create_release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm,linux/arm64
|
||||
tags: ${{ needs.create_release.outputs.docker_tags }}
|
||||
docker_image: smallstep/step-ca
|
||||
docker_file: docker/Dockerfile.step-ca
|
||||
secrets: inherit
|
||||
|
|
49
.github/workflows/test.yml
vendored
49
.github/workflows/test.yml
vendored
|
@ -1,49 +0,0 @@
|
|||
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)
|
23
.github/workflows/triage.yml
vendored
23
.github/workflows/triage.yml
vendored
|
@ -4,26 +4,13 @@ on:
|
|||
issues:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
|
||||
jobs:
|
||||
|
||||
label:
|
||||
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 }}
|
||||
triage:
|
||||
uses: smallstep/workflows/.github/workflows/triage.yml@main
|
||||
secrets: inherit
|
||||
|
|
18
.gitleaksignore
Normal file
18
.gitleaksignore
Normal file
|
@ -0,0 +1,18 @@
|
|||
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
|
|
@ -1,74 +0,0 @@
|
|||
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
|
125
.goreleaser.yml
125
.goreleaser.yml
|
@ -26,49 +26,7 @@ builds:
|
|||
flags:
|
||||
- -trimpath
|
||||
main: ./cmd/step-ca/main.go
|
||||
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
|
||||
binary: step-ca
|
||||
ldflags:
|
||||
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
|
||||
|
||||
|
@ -85,6 +43,38 @@ archives:
|
|||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
allow_different_binary_count: true
|
||||
|
||||
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
|
||||
#
|
||||
-
|
||||
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
|
||||
|
||||
source:
|
||||
enabled: true
|
||||
|
@ -97,8 +87,9 @@ checksum:
|
|||
|
||||
signs:
|
||||
- cmd: cosign
|
||||
stdin: '{{ .Env.COSIGN_PWD }}'
|
||||
args: ["sign-blob", "-key=/tmp/cosign.key", "-output=${signature}", "${artifact}"]
|
||||
signature: "${artifact}.sig"
|
||||
certificate: "${artifact}.pem"
|
||||
args: ["sign-blob", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"]
|
||||
artifacts: all
|
||||
|
||||
snapshot:
|
||||
|
@ -140,7 +131,7 @@ release:
|
|||
#### Linux
|
||||
|
||||
- 📦 [step-ca_linux_{{ .Version }}_amd64.tar.gz](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_linux_{{ .Version }}_amd64.tar.gz)
|
||||
- 📦 [step-ca_{{ .Env.DEB_VERSION }}_amd64.deb](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ .Env.DEB_VERSION }}_amd64.deb)
|
||||
- 📦 [step-ca_{{ .Version }}_amd64.deb](https://dl.step.sm/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ .Version }}_amd64.deb)
|
||||
|
||||
#### OSX Darwin
|
||||
|
||||
|
@ -163,9 +154,9 @@ release:
|
|||
Below is an example using `cosign` to verify a release artifact:
|
||||
|
||||
```
|
||||
cosign verify-blob \
|
||||
-key https://raw.githubusercontent.com/smallstep/certificates/master/cosign.pub \
|
||||
-signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig
|
||||
COSIGN_EXPERIMENTAL=1 cosign verify-blob \
|
||||
--certificate ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig.pem \
|
||||
--signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig \
|
||||
~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz
|
||||
```
|
||||
|
||||
|
@ -194,39 +185,3 @@ release:
|
|||
# - glob: ./path/to/file.txt
|
||||
# - glob: ./glob/**/to/**/file/**/*
|
||||
# - glob: ./glob/foo/to/bar/file/foobar/override_from_previous
|
||||
|
||||
scoop:
|
||||
# Template for the url which is determined by the given Token (github or gitlab)
|
||||
# Default for github is "https://github.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
# Default for gitlab is "https://gitlab.com/<repo_owner>/<repo_name>/uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}"
|
||||
# Default for gitea is "https://gitea.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
url_template: "http://github.com/smallstep/certificates/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
|
||||
|
||||
# Repository to push the app manifest to.
|
||||
bucket:
|
||||
owner: smallstep
|
||||
name: scoop-bucket
|
||||
|
||||
# Git author used to commit to the repository.
|
||||
# Defaults are shown.
|
||||
commit_author:
|
||||
name: goreleaserbot
|
||||
email: goreleaser@smallstep.com
|
||||
|
||||
# The project name and current git tag are used in the format string.
|
||||
commit_msg_template: "Scoop update for {{ .ProjectName }} version {{ .Tag }}"
|
||||
|
||||
# Your app's homepage.
|
||||
# Default is empty.
|
||||
homepage: "https://smallstep.com/docs/step-ca"
|
||||
|
||||
# Skip uploads for prerelease.
|
||||
skip_upload: auto
|
||||
|
||||
# Your app's description.
|
||||
# Default is empty.
|
||||
description: "A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management, so you can use TLS everywhere & SSO for SSH."
|
||||
|
||||
# Your app's license
|
||||
# Default is empty.
|
||||
license: "Apache-2.0"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env sh
|
||||
read -r firstline < .VERSION
|
||||
last_half="${firstline##*tag: }"
|
||||
if [[ ${last_half::1} == "v" ]]; then
|
||||
|
|
|
@ -18,6 +18,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||
## [Unreleased]
|
||||
### Added
|
||||
- Added support for ACME device-attest-01 challenge.
|
||||
- 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 managment.
|
||||
- Added experimental support for CRLs.
|
||||
|
||||
### Fixed
|
||||
- MySQL DSN parsing issues fixed with upgrade to [smallstep/nosql@v0.5.0](https://github.com/smallstep/nosql/releases/tag/v0.5.0).
|
||||
|
||||
## [0.22.1] - 2022-08-31
|
||||
### Fixed
|
||||
|
|
33
Makefile
33
Makefile
|
@ -28,8 +28,9 @@ ci: testcgo build
|
|||
#########################################
|
||||
|
||||
bootstra%:
|
||||
# Using a released version of golangci-lint to take into account custom replacements in their go.mod
|
||||
$Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.42.0
|
||||
$Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin latest
|
||||
$Q go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
$Q go install gotest.tools/gotestsum@latest
|
||||
|
||||
.PHONY: bootstra%
|
||||
|
||||
|
@ -78,8 +79,6 @@ $(info DEB_VERSION is $(DEB_VERSION))
|
|||
$(info PUSHTYPE is $(PUSHTYPE))
|
||||
endif
|
||||
|
||||
include make/docker.mk
|
||||
|
||||
#########################################
|
||||
# Build
|
||||
#########################################
|
||||
|
@ -132,17 +131,18 @@ generate:
|
|||
# Test
|
||||
#########################################
|
||||
test:
|
||||
$Q $(GOFLAGS) go test -short -coverprofile=coverage.out ./...
|
||||
$Q $(GOFLAGS) gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...
|
||||
|
||||
|
||||
testcgo:
|
||||
$Q go test -short -coverprofile=coverage.out ./...
|
||||
$Q gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...
|
||||
|
||||
.PHONY: test testcgo
|
||||
|
||||
integrate: integration
|
||||
|
||||
integration: bin/$(BINNAME)
|
||||
$Q $(GOFLAGS) go test -tags=integration ./integration/...
|
||||
$Q $(GOFLAGS) gotestsum -- -tags=integration ./integration/...
|
||||
|
||||
.PHONY: integrate integration
|
||||
|
||||
|
@ -151,15 +151,14 @@ integration: bin/$(BINNAME)
|
|||
#########################################
|
||||
|
||||
fmt:
|
||||
$Q gofmt -l -s -w $(SRC)
|
||||
$Q goimports -l -w $(SRC)
|
||||
|
||||
lint: SHELL:=/bin/bash
|
||||
lint:
|
||||
$Q golangci-lint run --timeout=30m
|
||||
$Q LOG_LEVEL=error golangci-lint run --config <(curl -s https://raw.githubusercontent.com/smallstep/workflows/master/.golangci.yml) --timeout=30m
|
||||
$Q govulncheck ./...
|
||||
|
||||
lintcgo:
|
||||
$Q LOG_LEVEL=error golangci-lint run --timeout=30m
|
||||
|
||||
.PHONY: fmt lint lintcgo
|
||||
.PHONY: fmt lint
|
||||
|
||||
#########################################
|
||||
# Install
|
||||
|
@ -231,11 +230,3 @@ debian: changelog
|
|||
distclean: clean
|
||||
|
||||
.PHONY: changelog debian distclean
|
||||
|
||||
#################################################
|
||||
# Targets for creating step artifacts
|
||||
#################################################
|
||||
|
||||
docker-artifacts: docker-$(PUSHTYPE)
|
||||
|
||||
.PHONY: docker-artifacts
|
||||
|
|
|
@ -33,7 +33,7 @@ func (a *Account) ToLog() (interface{}, error) {
|
|||
|
||||
// IsValid returns true if the Account is valid.
|
||||
func (a *Account) IsValid() bool {
|
||||
return Status(a.Status) == StatusValid
|
||||
return a.Status == StatusValid
|
||||
}
|
||||
|
||||
// KeyToID converts a JWK to a thumbprint.
|
||||
|
|
|
@ -46,14 +46,14 @@ func TestKeyToID(t *testing.T) {
|
|||
tc := run(t)
|
||||
if id, err := KeyToID(tc.jwk); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -131,12 +131,13 @@ func TestExternalAccountKey_BindTo(t *testing.T) {
|
|||
}
|
||||
if wantErr {
|
||||
assert.NotNil(t, err)
|
||||
assert.Type(t, &Error{}, err)
|
||||
ae, _ := err.(*Error)
|
||||
assert.Equals(t, ae.Type, tt.err.Type)
|
||||
assert.Equals(t, ae.Detail, tt.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tt.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
||||
var ae *Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.Equals(t, ae.Type, tt.err.Type)
|
||||
assert.Equals(t, ae.Detail, tt.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tt.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
||||
}
|
||||
} else {
|
||||
assert.Equals(t, eak.AccountID, acct.ID)
|
||||
assert.Equals(t, eak.HmacKey, []byte{})
|
||||
|
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
@ -97,8 +98,8 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
|
|||
httpStatus := http.StatusCreated
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
acmeErr, ok := err.(*acme.Error)
|
||||
if !ok || acmeErr.Status != http.StatusBadRequest {
|
||||
var acmeErr *acme.Error
|
||||
if !errors.As(err, &acmeErr) || acmeErr.Status != http.StatusBadRequest {
|
||||
// Something went wrong ...
|
||||
render.Error(w, err)
|
||||
return
|
||||
|
|
|
@ -197,11 +197,12 @@ func TestNewAccountRequest_Validate(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.nar.Validate(); err != nil {
|
||||
if assert.NotNil(t, err) {
|
||||
ae, ok := err.(*acme.Error)
|
||||
assert.True(t, ok)
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
var ae *acme.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
|
@ -268,11 +269,12 @@ func TestUpdateAccountRequest_Validate(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.uar.Validate(); err != nil {
|
||||
if assert.NotNil(t, err) {
|
||||
ae, ok := err.(*acme.Error)
|
||||
assert.True(t, ok)
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
var ae *acme.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
|
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
|
||||
|
@ -24,6 +25,7 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest)
|
|||
}
|
||||
|
||||
if !acmeProv.RequireEAB {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -51,7 +53,8 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest)
|
|||
db := acme.MustDatabaseFromContext(ctx)
|
||||
externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)
|
||||
if err != nil {
|
||||
if _, ok := err.(*acme.Error); ok {
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key")
|
||||
}
|
||||
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
|
||||
|
|
|
@ -860,13 +860,15 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
|||
if wantErr {
|
||||
assert.NotNil(t, err)
|
||||
assert.Type(t, &acme.Error{}, err)
|
||||
ae, _ := err.(*acme.Error)
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Status, tc.err.Status)
|
||||
assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
var ae *acme.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Status, tc.err.Status)
|
||||
assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
}
|
||||
} else {
|
||||
if got == nil {
|
||||
assert.Nil(t, tc.eak)
|
||||
|
|
|
@ -205,7 +205,7 @@ type Directory struct {
|
|||
NewOrder string `json:"newOrder"`
|
||||
RevokeCert string `json:"revokeCert"`
|
||||
KeyChange string `json:"keyChange"`
|
||||
Meta Meta `json:"meta"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// ToLog enables response logging for the Directory type.
|
||||
|
@ -228,18 +228,49 @@ func GetDirectory(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
render.JSON(w, &Directory{
|
||||
NewNonce: linker.GetLink(ctx, acme.NewNonceLinkType),
|
||||
NewAccount: linker.GetLink(ctx, acme.NewAccountLinkType),
|
||||
NewOrder: linker.GetLink(ctx, acme.NewOrderLinkType),
|
||||
RevokeCert: linker.GetLink(ctx, acme.RevokeCertLinkType),
|
||||
KeyChange: linker.GetLink(ctx, acme.KeyChangeLinkType),
|
||||
Meta: Meta{
|
||||
ExternalAccountRequired: acmeProv.RequireEAB,
|
||||
},
|
||||
Meta: createMetaObject(acmeProv),
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
// MAY be added at some point in the future but is not in any way a guarantee of such.
|
||||
func NotImplemented(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -18,10 +18,13 @@ import (
|
|||
"github.com/go-chi/chi"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
|
@ -129,7 +132,35 @@ func TestHandler_GetDirectory(t *testing.T) {
|
|||
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{
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
@ -751,3 +782,89 @@ 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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -518,9 +518,6 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"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(ctx, jwkContextKey, pub)
|
||||
return test{
|
||||
|
|
|
@ -179,11 +179,12 @@ func TestNewOrderRequest_Validate(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.nor.Validate(); err != nil {
|
||||
if assert.NotNil(t, err) {
|
||||
ae, ok := err.(*acme.Error)
|
||||
assert.True(t, ok)
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
var ae *acme.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
|
@ -253,11 +254,12 @@ func TestFinalizeRequestValidate(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.fr.Validate(); err != nil {
|
||||
if assert.NotNil(t, err) {
|
||||
ae, ok := err.(*acme.Error)
|
||||
assert.True(t, ok)
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
var ae *acme.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
|
@ -756,19 +758,22 @@ func TestHandler_newAuthorization(t *testing.T) {
|
|||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if name == "ok/permanent-identifier-enabled" {
|
||||
println(1)
|
||||
}
|
||||
tc := run(t)
|
||||
ctx := newBaseContext(context.Background(), tc.db)
|
||||
ctx = acme.NewProvisionerContext(ctx, tc.prov)
|
||||
if err := newAuthorization(ctx, tc.az); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var k *acme.Error
|
||||
if assert.True(t, errors.As(err, &k)) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,14 +130,14 @@ func TestAuthorization_UpdateStatus(t *testing.T) {
|
|||
tc := run(t)
|
||||
if err := tc.az.UpdateStatus(context.Background(), tc.db); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,18 @@ const (
|
|||
DEVICEATTEST01 ChallengeType = "device-attest-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.
|
||||
type Challenge struct {
|
||||
ID string `json:"-"`
|
||||
|
@ -97,6 +109,12 @@ 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 {
|
||||
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)
|
||||
resp, err := vc.Get(u.String())
|
||||
if err != nil {
|
||||
|
@ -166,10 +184,17 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON
|
|||
// [RFC5246] or higher when connecting to clients for validation.
|
||||
MinVersion: tls.VersionTLS12,
|
||||
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
|
||||
}
|
||||
|
||||
hostPort := net.JoinHostPort(ch.Value, "443")
|
||||
var hostPort string
|
||||
|
||||
// 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)
|
||||
conn, err := vc.TLSDial("tcp", hostPort, config)
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -188,14 +189,14 @@ func Test_storeError(t *testing.T) {
|
|||
tc := run(t)
|
||||
if err := storeError(context.Background(), tc.db, tc.ch, tc.markInvalid, err); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -243,14 +244,14 @@ func TestKeyAuthorization(t *testing.T) {
|
|||
tc := run(t)
|
||||
if ka, err := KeyAuthorization(tc.token, tc.jwk); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -370,6 +371,47 @@ func TestChallenge_Validate(t *testing.T) {
|
|||
},
|
||||
}
|
||||
},
|
||||
"ok/http-01-insecure": func(t *testing.T) test {
|
||||
t.Cleanup(func() {
|
||||
InsecurePortHTTP01 = 0
|
||||
})
|
||||
|
||||
ch := &Challenge{
|
||||
ID: "chID",
|
||||
Status: StatusPending,
|
||||
Type: "http-01",
|
||||
Token: "token",
|
||||
Value: "zap.internal",
|
||||
}
|
||||
|
||||
InsecurePortHTTP01 = 8080
|
||||
|
||||
return test{
|
||||
ch: ch,
|
||||
vc: &mockClient{
|
||||
get: func(url string) (*http.Response, error) {
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
db: &MockDB{
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equals(t, updch.ID, ch.ID)
|
||||
assert.Equals(t, updch.Token, ch.Token)
|
||||
assert.Equals(t, updch.Type, ch.Type)
|
||||
assert.Equals(t, updch.Status, ch.Status)
|
||||
assert.Equals(t, updch.Value, ch.Value)
|
||||
|
||||
err := NewError(ErrorConnectionType, "error doing http GET for url http://zap.internal:8080/.well-known/acme-challenge/%s: force", ch.Token)
|
||||
assert.HasPrefix(t, updch.Error.Err.Error(), err.Err.Error())
|
||||
assert.Equals(t, updch.Error.Type, err.Type)
|
||||
assert.Equals(t, updch.Error.Detail, err.Detail)
|
||||
assert.Equals(t, updch.Error.Status, err.Status)
|
||||
assert.Equals(t, updch.Error.Detail, err.Detail)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/dns-01": func(t *testing.T) test {
|
||||
ch := &Challenge{
|
||||
ID: "chID",
|
||||
|
@ -501,6 +543,72 @@ func TestChallenge_Validate(t *testing.T) {
|
|||
srv, tlsDial := newTestTLSALPNServer(cert)
|
||||
srv.Start()
|
||||
|
||||
return test{
|
||||
ch: ch,
|
||||
vc: &mockClient{
|
||||
tlsDial: tlsDial,
|
||||
},
|
||||
db: &MockDB{
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equals(t, updch.ID, ch.ID)
|
||||
assert.Equals(t, updch.Token, ch.Token)
|
||||
assert.Equals(t, updch.Status, ch.Status)
|
||||
assert.Equals(t, updch.Type, ch.Type)
|
||||
assert.Equals(t, updch.Value, ch.Value)
|
||||
assert.Equals(t, updch.Error, nil)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
srv: srv,
|
||||
jwk: jwk,
|
||||
}
|
||||
},
|
||||
"ok/tls-alpn-01-insecure": func(t *testing.T) test {
|
||||
t.Cleanup(func() {
|
||||
InsecurePortTLSALPN01 = 0
|
||||
})
|
||||
|
||||
ch := &Challenge{
|
||||
ID: "chID",
|
||||
Token: "token",
|
||||
Type: "tls-alpn-01",
|
||||
Status: StatusPending,
|
||||
Value: "zap.internal",
|
||||
}
|
||||
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
|
||||
assert.FatalError(t, err)
|
||||
expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
|
||||
|
||||
cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
|
||||
t.Fatalf("failed to listen on a port: %v", err)
|
||||
}
|
||||
}
|
||||
_, port, err := net.SplitHostPort(l.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to split host port: %v", err)
|
||||
}
|
||||
|
||||
// Use an insecure port
|
||||
InsecurePortTLSALPN01, err = strconv.Atoi(port)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to convert port to int: %v", err)
|
||||
}
|
||||
|
||||
srv, tlsDial := newTestTLSALPNServer(cert, func(srv *httptest.Server) {
|
||||
srv.Listener.Close()
|
||||
srv.Listener = l
|
||||
})
|
||||
srv.Start()
|
||||
|
||||
return test{
|
||||
ch: ch,
|
||||
vc: &mockClient{
|
||||
|
@ -533,14 +641,14 @@ func TestChallenge_Validate(t *testing.T) {
|
|||
ctx := NewClientContext(context.Background(), tc.vc)
|
||||
if err := tc.ch.Validate(ctx, tc.db, tc.jwk, nil); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -928,14 +1036,14 @@ func TestHTTP01Validate(t *testing.T) {
|
|||
ctx := NewClientContext(context.Background(), tc.vc)
|
||||
if err := http01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -1228,14 +1336,14 @@ func TestDNS01Validate(t *testing.T) {
|
|||
ctx := NewClientContext(context.Background(), tc.vc)
|
||||
if err := dns01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -1248,7 +1356,7 @@ func TestDNS01Validate(t *testing.T) {
|
|||
|
||||
type tlsDialer func(network, addr string, config *tls.Config) (conn *tls.Conn, err error)
|
||||
|
||||
func newTestTLSALPNServer(validationCert *tls.Certificate) (*httptest.Server, tlsDialer) {
|
||||
func newTestTLSALPNServer(validationCert *tls.Certificate, opts ...func(*httptest.Server)) (*httptest.Server, tlsDialer) {
|
||||
srv := httptest.NewUnstartedServer(http.NewServeMux())
|
||||
|
||||
srv.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){
|
||||
|
@ -1273,6 +1381,11 @@ func newTestTLSALPNServer(validationCert *tls.Certificate) (*httptest.Server, tl
|
|||
},
|
||||
}
|
||||
|
||||
// Apply options
|
||||
for _, fn := range opts {
|
||||
fn(srv)
|
||||
}
|
||||
|
||||
srv.Listener = tls.NewListener(srv.Listener, srv.TLS)
|
||||
//srv.Config.ErrorLog = log.New(ioutil.Discard, "", 0) // hush
|
||||
|
||||
|
@ -2298,14 +2411,14 @@ func TestTLSALPN01Validate(t *testing.T) {
|
|||
ctx := NewClientContext(context.Background(), tc.vc)
|
||||
if err := tlsalpn01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -2774,3 +2887,97 @@ func Test_doStepAttestationFormat(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_doStepAttestationFormat_noCAIntermediate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// This CA simulates a YubiKey v5.2.4, where the attestation intermediate in
|
||||
// the CA does not have the basic constraint extension. With the current
|
||||
// validation of the certificate the test case below returns an error. If
|
||||
// we change the validation to support this use case, the test case below
|
||||
// should change.
|
||||
//
|
||||
// See https://github.com/Yubico/yubikey-manager/issues/522
|
||||
ca, err := minica.New(minica.WithIntermediateTemplate(`{"subject": {{ toJson .Subject }}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
|
||||
|
||||
makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
|
||||
leaf, err := ca.Sign(&x509.Certificate{
|
||||
Subject: pkix.Name{CommonName: "attestation cert"},
|
||||
PublicKey: signer.Public(),
|
||||
ExtraExtensions: []pkix.Extension{
|
||||
{Id: oidYubicoSerialNumber, Value: serialNumber},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return leaf
|
||||
}
|
||||
|
||||
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serialNumber, err := asn1.Marshal(1234)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
leaf := makeLeaf(signer, serialNumber)
|
||||
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keyAuth, err := KeyAuthorization("token", jwk)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuth))
|
||||
sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cborSig, err := cbor.Marshal(sig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
prov Provisioner
|
||||
ch *Challenge
|
||||
jwk *jose.JSONWebKey
|
||||
att *AttestationObject
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *stepAttestationData
|
||||
wantErr bool
|
||||
}{
|
||||
{"fail no intermediate", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
|
||||
"alg": -7,
|
||||
"sig": cborSig,
|
||||
},
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := doStepAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("doStepAttestationFormat() error = %#v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("doStepAttestationFormat() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ func NewClient() Client {
|
|||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
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]
|
||||
},
|
||||
},
|
||||
|
|
|
@ -95,16 +95,16 @@ func TestDB_getDBAccount(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if dbacc, err := d.getDBAccount(context.Background(), accID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db}
|
||||
if retAccID, err := d.getAccountIDByKeyID(context.Background(), kid); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -248,16 +248,16 @@ func TestDB_GetAccount(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if acc, err := d.GetAccount(context.Background(), accID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -354,16 +354,16 @@ func TestDB_GetAccountByKeyID(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if acc, err := d.GetAccountByKeyID(context.Background(), kid); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -101,16 +101,16 @@ func TestDB_getDBAuthz(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if dbaz, err := d.getDBAuthz(context.Background(), azID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db}
|
||||
if az, err := d.GetAuthorization(context.Background(), azID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -745,16 +745,16 @@ func TestDB_GetAuthorizationsByAccountID(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if azs, err := d.GetAuthorizationsByAccountID(context.Background(), accountID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -138,5 +138,4 @@ func parseBundle(b []byte) ([]*x509.Certificate, error) {
|
|||
return nil, errors.New("error decoding PEM: unexpected data")
|
||||
}
|
||||
return bundle, nil
|
||||
|
||||
}
|
||||
|
|
|
@ -250,16 +250,16 @@ func TestDB_GetCertificate(t *testing.T) {
|
|||
d := DB{db: tc.db}
|
||||
cert, err := d.GetCertificate(context.Background(), certID)
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -444,16 +444,16 @@ func TestDB_GetCertificateBySerial(t *testing.T) {
|
|||
d := DB{db: tc.db}
|
||||
cert, err := d.GetCertificateBySerial(context.Background(), serial)
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -94,16 +94,16 @@ func TestDB_getDBChallenge(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if ch, err := d.getDBChallenge(context.Background(), chID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db}
|
||||
if ch, err := d.GetChallenge(context.Background(), chID, azID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -54,7 +54,6 @@ func (db *DB) getDBExternalAccountKey(ctx context.Context, id string) (*dbExtern
|
|||
|
||||
// CreateExternalAccountKey creates a new External Account Binding key with a name
|
||||
func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
|
||||
externalAccountKeyMutex.Lock()
|
||||
defer externalAccountKeyMutex.Unlock()
|
||||
|
||||
|
@ -210,6 +209,7 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI
|
|||
defer externalAccountKeyMutex.RUnlock()
|
||||
|
||||
if reference == "" {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -228,6 +228,7 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI
|
|||
}
|
||||
|
||||
func (db *DB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -371,7 +372,6 @@ func sliceIndex(slice []string, item string) int {
|
|||
// removeElement deletes the item if it exists in the
|
||||
// slice. It returns a new slice, keeping the old one intact.
|
||||
func removeElement(slice []string, item string) []string {
|
||||
|
||||
newSlice := make([]string, 0)
|
||||
index := sliceIndex(slice, item)
|
||||
if index < 0 {
|
||||
|
|
|
@ -93,16 +93,16 @@ func TestDB_getDBExternalAccountKey(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db}
|
||||
if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db}
|
||||
if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -580,16 +580,16 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
|
|||
cursor, limit := "", 0
|
||||
if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil {
|
||||
assert.Equals(t, "", nextCursor)
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.Equals(t, tc.err.Error(), err.Error())
|
||||
}
|
||||
|
@ -672,7 +672,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
|||
return errors.New("force default")
|
||||
}
|
||||
},
|
||||
MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) {
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
fmt.Println(string(bucket))
|
||||
switch string(bucket) {
|
||||
case string(externalAccountKeyIDsByReferenceTable):
|
||||
|
@ -882,16 +882,16 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.Equals(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -146,16 +146,16 @@ func TestDB_DeleteNonce(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.DeleteNonce(context.Background(), acme.Nonce(nonceID)); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -102,16 +102,16 @@ func TestDB_getDBOrder(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if dbo, err := d.getDBOrder(context.Background(), orderID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db}
|
||||
if o, err := d.GetOrder(context.Background(), orderID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1003,16 +1003,16 @@ func TestDB_updateAddOrderIDs(t *testing.T) {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *acme.Error:
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -75,6 +75,8 @@ func (ap ProblemType) String() string {
|
|||
return "accountDoesNotExist"
|
||||
case ErrorAlreadyRevokedType:
|
||||
return "alreadyRevoked"
|
||||
case ErrorBadAttestationStatementType:
|
||||
return "badAttestationStatement"
|
||||
case ErrorBadCSRType:
|
||||
return "badCSR"
|
||||
case ErrorBadNonceType:
|
||||
|
@ -310,10 +312,11 @@ func NewErrorISE(msg string, args ...interface{}) *Error {
|
|||
|
||||
// WrapError attempts to wrap the internal error.
|
||||
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
||||
switch e := err.(type) {
|
||||
case nil:
|
||||
var e *Error
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case *Error:
|
||||
case errors.As(err, &e):
|
||||
if e.Err == nil {
|
||||
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
||||
} else {
|
||||
|
|
|
@ -194,6 +194,14 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
|
|||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
|
@ -324,7 +332,6 @@ func numberOfIdentifierType(typ IdentifierType, ids []Identifier) int {
|
|||
// 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.
|
||||
func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) {
|
||||
|
||||
// for clarity only; we're operating on the same object by pointer
|
||||
canonicalized = csr
|
||||
|
||||
|
|
|
@ -247,14 +247,14 @@ func TestOrder_UpdateStatus(t *testing.T) {
|
|||
tc := run(t)
|
||||
if err := tc.o.UpdateStatus(context.Background(), tc.db); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -812,14 +812,14 @@ func TestOrder_Finalize(t *testing.T) {
|
|||
tc := run(t)
|
||||
if err := tc.o.Finalize(context.Background(), tc.db, tc.csr, tc.ca, tc.prov); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
|
@ -1474,14 +1474,14 @@ func TestOrder_sans(t *testing.T) {
|
|||
t.Errorf("Order.sans() = %v, want error; got none", got)
|
||||
return
|
||||
}
|
||||
switch k := err.(type) {
|
||||
case *Error:
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tt.err.Type)
|
||||
assert.Equals(t, k.Detail, tt.err.Detail)
|
||||
assert.Equals(t, k.Status, tt.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tt.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tt.err.Detail)
|
||||
default:
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
return
|
||||
|
|
|
@ -3,7 +3,7 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/dsa" //nolint
|
||||
"crypto/dsa" //nolint:staticcheck // support legacy algorithms
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
|
@ -40,6 +40,7 @@ type Authority interface {
|
|||
Root(shasum string) (*x509.Certificate, error)
|
||||
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*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)
|
||||
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
|
||||
LoadProvisionerByName(string) (provisioner.Interface, error)
|
||||
|
@ -49,6 +50,7 @@ type Authority interface {
|
|||
GetRoots() ([]*x509.Certificate, error)
|
||||
GetFederation() ([]*x509.Certificate, error)
|
||||
Version() authority.Version
|
||||
GetCertificateRevocationList() ([]byte, error)
|
||||
}
|
||||
|
||||
// mustAuthority will be replaced on unit tests.
|
||||
|
@ -267,6 +269,7 @@ func Route(r Router) {
|
|||
r.MethodFunc("POST", "/renew", Renew)
|
||||
r.MethodFunc("POST", "/rekey", Rekey)
|
||||
r.MethodFunc("POST", "/revoke", Revoke)
|
||||
r.MethodFunc("GET", "/crl", CRL)
|
||||
r.MethodFunc("GET", "/provisioners", Provisioners)
|
||||
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
|
||||
r.MethodFunc("GET", "/roots", Roots)
|
||||
|
|
|
@ -192,6 +192,7 @@ type mockAuthority struct {
|
|||
sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
renew func(cert *x509.Certificate) ([]*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)
|
||||
loadProvisionerByName func(name string) (provisioner.Interface, error)
|
||||
getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
|
||||
|
@ -199,6 +200,7 @@ type mockAuthority struct {
|
|||
getEncryptedKey func(kid string) (string, error)
|
||||
getRoots 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)
|
||||
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)
|
||||
|
@ -212,6 +214,14 @@ type mockAuthority struct {
|
|||
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.
|
||||
func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||
if m.authorize != nil {
|
||||
|
@ -255,6 +265,13 @@ func (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, erro
|
|||
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) {
|
||||
if m.rekey != nil {
|
||||
return m.rekey(oldcert, pk)
|
||||
|
@ -772,6 +789,45 @@ func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) (
|
|||
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) {
|
||||
type fields struct {
|
||||
Authority Authority
|
||||
|
|
32
api/crl.go
Normal file
32
api/crl.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
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 {
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "X509 CRL",
|
||||
Bytes: crlBytes,
|
||||
})
|
||||
w.Header().Add("Content-Type", "application/x-pem-file")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.pem\"")
|
||||
w.Write(pemBytes)
|
||||
} else {
|
||||
w.Header().Add("Content-Type", "application/pkix-crl")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"")
|
||||
w.Write(crlBytes)
|
||||
}
|
||||
}
|
|
@ -38,14 +38,10 @@ func Error(rw http.ResponseWriter, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
e, ok := err.(StackTracedError)
|
||||
if !ok {
|
||||
e, ok = errors.Cause(err).(StackTracedError)
|
||||
}
|
||||
|
||||
if ok {
|
||||
var st StackTracedError
|
||||
if !errors.As(err, &st) {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"stack-trace": fmt.Sprintf("%+v", e.StackTrace()),
|
||||
"stack-trace": fmt.Sprintf("%+v", st.StackTrace()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
if tt.wantErr {
|
||||
e, ok := err.(*errs.Error)
|
||||
if ok {
|
||||
var e *errs.Error
|
||||
if errors.As(err, &e) {
|
||||
if code := e.StatusCode(); code != 400 {
|
||||
t.Errorf("error.StatusCode() = %v, wants 400", code)
|
||||
}
|
||||
|
@ -102,14 +102,15 @@ func TestProtoJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
if tt.wantErr {
|
||||
switch err.(type) {
|
||||
case badProtoJSONError:
|
||||
var (
|
||||
ee *errs.Error
|
||||
bpe badProtoJSONError
|
||||
)
|
||||
switch {
|
||||
case errors.As(err, &bpe):
|
||||
assert.Contains(t, err.Error(), "syntax error")
|
||||
case *errs.Error:
|
||||
var ee *errs.Error
|
||||
if errors.As(err, &ee) {
|
||||
assert.Equal(t, http.StatusBadRequest, ee.Status)
|
||||
}
|
||||
case errors.As(err, &ee):
|
||||
assert.Equal(t, http.StatusBadRequest, ee.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ package render
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
@ -77,8 +78,9 @@ type RenderableError interface {
|
|||
func Error(w http.ResponseWriter, err error) {
|
||||
log.Error(w, err)
|
||||
|
||||
if e, ok := err.(RenderableError); ok {
|
||||
e.Render(w)
|
||||
var r RenderableError
|
||||
if errors.As(err, &r) {
|
||||
r.Render(w)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -105,17 +107,18 @@ func statusCodeFromError(err error) (code int) {
|
|||
}
|
||||
|
||||
for err != nil {
|
||||
if sc, ok := err.(StatusCodedError); ok {
|
||||
var sc StatusCodedError
|
||||
if errors.As(err, &sc) {
|
||||
code = sc.StatusCode()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
cause, ok := err.(causer)
|
||||
if !ok {
|
||||
var c causer
|
||||
if !errors.As(err, &c) {
|
||||
break
|
||||
}
|
||||
err = cause.Cause()
|
||||
err = c.Cause()
|
||||
}
|
||||
|
||||
return
|
||||
|
|
24
api/renew.go
24
api/renew.go
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
|
@ -17,14 +18,22 @@ const (
|
|||
// Renew uses the information of certificate in the TLS connection to create a
|
||||
// new one.
|
||||
func Renew(w http.ResponseWriter, r *http.Request) {
|
||||
cert, err := getPeerCertificate(r)
|
||||
ctx := r.Context()
|
||||
|
||||
// Get the leaf certificate from the peer or the token.
|
||||
cert, token, err := getPeerCertificate(r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
a := mustAuthority(r.Context())
|
||||
certChain, err := a.Renew(cert)
|
||||
// The token can be used by RAs to renew a certificate.
|
||||
if token != "" {
|
||||
ctx = authority.NewTokenContext(ctx, token)
|
||||
}
|
||||
|
||||
a := mustAuthority(ctx)
|
||||
certChain, err := a.RenewContext(ctx, cert, nil)
|
||||
if err != nil {
|
||||
render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
|
||||
return
|
||||
|
@ -44,15 +53,16 @@ func Renew(w http.ResponseWriter, r *http.Request) {
|
|||
}, http.StatusCreated)
|
||||
}
|
||||
|
||||
func getPeerCertificate(r *http.Request) (*x509.Certificate, error) {
|
||||
func getPeerCertificate(r *http.Request) (*x509.Certificate, string, error) {
|
||||
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 parts := strings.SplitN(s, bearerScheme+" ", 2); len(parts) == 2 {
|
||||
ctx := r.Context()
|
||||
return mustAuthority(ctx).AuthorizeRenewToken(ctx, parts[1])
|
||||
peer, err := 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")
|
||||
}
|
||||
|
|
|
@ -62,12 +62,12 @@ func TestRevokeRequestValidate(t *testing.T) {
|
|||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.rr.Validate(); err != nil {
|
||||
switch v := err.(type) {
|
||||
case *errs.Error:
|
||||
assert.HasPrefix(t, v.Error(), tc.err.Error())
|
||||
assert.Equals(t, v.StatusCode(), tc.err.Status)
|
||||
default:
|
||||
t.Errorf("unexpected error type: %T", v)
|
||||
var ee *errs.Error
|
||||
if errors.As(err, &ee) {
|
||||
assert.HasPrefix(t, ee.Error(), tc.err.Error())
|
||||
assert.Equals(t, ee.StatusCode(), tc.err.Status)
|
||||
} else {
|
||||
t.Errorf("unexpected error type: %T", err)
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
|
|
|
@ -84,7 +84,6 @@ func (h *acmeAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *
|
|||
}
|
||||
|
||||
func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey {
|
||||
|
||||
if k == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -229,11 +229,13 @@ func TestCreateAdminRequest_Validate(t *testing.T) {
|
|||
|
||||
if err != nil {
|
||||
assert.Type(t, &admin.Error{}, err)
|
||||
adminErr, _ := err.(*admin.Error)
|
||||
assert.Equals(t, tt.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, tt.err.Status, adminErr.Status)
|
||||
assert.Equals(t, tt.err.Message, adminErr.Message)
|
||||
var adminErr *admin.Error
|
||||
if assert.True(t, errors.As(err, &adminErr)) {
|
||||
assert.Equals(t, tt.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, tt.err.Status, adminErr.Status)
|
||||
assert.Equals(t, tt.err.Message, adminErr.Message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -278,11 +280,13 @@ func TestUpdateAdminRequest_Validate(t *testing.T) {
|
|||
|
||||
if err != nil {
|
||||
assert.Type(t, &admin.Error{}, err)
|
||||
adminErr, _ := err.(*admin.Error)
|
||||
assert.Equals(t, tt.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, tt.err.Status, adminErr.Status)
|
||||
assert.Equals(t, tt.err.Message, adminErr.Message)
|
||||
var ae *admin.Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.Equals(t, tt.err.Type, ae.Type)
|
||||
assert.Equals(t, tt.err.Detail, ae.Detail)
|
||||
assert.Equals(t, tt.err.Status, ae.Status)
|
||||
assert.Equals(t, tt.err.Message, ae.Message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,41 +4,47 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"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 {
|
||||
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.
|
||||
func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) {
|
||||
func Route(r api.Router, options ...RouterOption) {
|
||||
router := &router{}
|
||||
for _, fn := range options {
|
||||
fn(router)
|
||||
}
|
||||
|
||||
authnz := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return extractAuthorizeTokenAdmin(requireAPIEnabled(next))
|
||||
}
|
||||
|
@ -67,6 +73,10 @@ func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder Polic
|
|||
return authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next)))))
|
||||
}
|
||||
|
||||
webhookMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return authnz(loadProvisionerByName(next))
|
||||
}
|
||||
|
||||
// Provisioners
|
||||
r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner))
|
||||
r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners))
|
||||
|
@ -82,36 +92,42 @@ func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder Polic
|
|||
r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin))
|
||||
|
||||
// ACME responder
|
||||
if acmeResponder != nil {
|
||||
if router.acmeResponder != nil {
|
||||
// ACME External Account Binding Keys
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.CreateExternalAccountKey))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(acmeResponder.DeleteExternalAccountKey))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys))
|
||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.CreateExternalAccountKey))
|
||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(router.acmeResponder.DeleteExternalAccountKey))
|
||||
}
|
||||
|
||||
// Policy responder
|
||||
if policyResponder != nil {
|
||||
if router.policyResponder != nil {
|
||||
// Policy - Authority
|
||||
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(policyResponder.GetAuthorityPolicy))
|
||||
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(policyResponder.CreateAuthorityPolicy))
|
||||
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(policyResponder.UpdateAuthorityPolicy))
|
||||
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(policyResponder.DeleteAuthorityPolicy))
|
||||
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(router.policyResponder.GetAuthorityPolicy))
|
||||
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(router.policyResponder.CreateAuthorityPolicy))
|
||||
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(router.policyResponder.UpdateAuthorityPolicy))
|
||||
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(router.policyResponder.DeleteAuthorityPolicy))
|
||||
|
||||
// Policy - Provisioner
|
||||
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.GetProvisionerPolicy))
|
||||
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.CreateProvisionerPolicy))
|
||||
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.UpdateProvisionerPolicy))
|
||||
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.DeleteProvisionerPolicy))
|
||||
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.GetProvisionerPolicy))
|
||||
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.CreateProvisionerPolicy))
|
||||
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.UpdateProvisionerPolicy))
|
||||
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.DeleteProvisionerPolicy))
|
||||
|
||||
// Policy - ACME Account
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy))
|
||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ func requireAPIEnabled(next http.HandlerFunc) http.HandlerFunc {
|
|||
// extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token.
|
||||
func extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tok := r.Header.Get("Authorization")
|
||||
if tok == "" {
|
||||
render.Error(w, admin.NewError(admin.ErrorUnauthorizedType,
|
||||
|
|
|
@ -50,7 +50,8 @@ func (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *ht
|
|||
|
||||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(r.Context())
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
@ -74,7 +75,8 @@ func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r
|
|||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
@ -125,7 +127,8 @@ func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r
|
|||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
@ -175,7 +178,8 @@ func (par *policyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r
|
|||
auth := mustAuthority(ctx)
|
||||
authorityPolicy, err := auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
if ae, ok := err.(*admin.Error); ok && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
|
||||
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
@ -468,7 +472,6 @@ func isBadRequest(err error) bool {
|
|||
}
|
||||
|
||||
func validatePolicy(p *linkedca.Policy) error {
|
||||
|
||||
// convert the policy; return early if nil
|
||||
options := policy.LinkedToCertificates(p)
|
||||
if options == nil {
|
||||
|
|
235
authority/admin/api/webhook.go
Normal file
235
authority/admin/api/webhook.go
Normal file
|
@ -0,0 +1,235 @@
|
|||
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:
|
||||
default:
|
||||
return admin.NewError(admin.ErrorBadRequestType, "webhook kind is invalid")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
668
authority/admin/api/webhook_test.go
Normal file
668
authority/admin/api/webhook_test.go
Normal file
|
@ -0,0 +1,668 @@
|
|||
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/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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -111,14 +111,14 @@ func (db *DB) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) {
|
|||
for _, entry := range dbEntries {
|
||||
adm, err := db.unmarshalAdmin(entry.Value, string(entry.Key))
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
if k.IsType(admin.ErrorDeletedType) || k.IsType(admin.ErrorAuthorityMismatchType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if ae.IsType(admin.ErrorDeletedType) || ae.IsType(admin.ErrorAuthorityMismatchType) {
|
||||
continue
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,16 +68,16 @@ func TestDB_getDBAdminBytes(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if b, err := d.getDBAdminBytes(context.Background(), adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if dba, err := d.getDBAdmin(context.Background(), adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||
if dba, err := d.unmarshalDBAdmin(tc.in, adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||
if adm, err := d.unmarshalAdmin(tc.in, adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if adm, err := d.GetAdmin(context.Background(), adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.DeleteAdmin(context.Background(), adminID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.UpdateAdmin(context.Background(), tc.adm); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.CreateAdmin(context.Background(), tc.adm); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if admins, err := d.GetAdmins(context.Background()); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string)
|
|||
|
||||
func (db *DB) unmarshalDBAuthorityPolicy(data []byte) (*dbAuthorityPolicy, error) {
|
||||
if len(data) == 0 {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
var dba = new(dbAuthorityPolicy)
|
||||
|
@ -102,6 +103,7 @@ func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*db
|
|||
return nil, err
|
||||
}
|
||||
if dbap == nil {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
if dbap.AuthorityID != authorityID {
|
||||
|
@ -112,7 +114,6 @@ func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*db
|
|||
}
|
||||
|
||||
func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||
|
||||
dbap := &dbAuthorityPolicy{
|
||||
ID: db.authorityID,
|
||||
AuthorityID: db.authorityID,
|
||||
|
@ -228,7 +229,6 @@ func dbToLinked(p *dbPolicy) *linkedca.Policy {
|
|||
}
|
||||
|
||||
func linkedToDB(p *linkedca.Policy) *dbPolicy {
|
||||
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -72,16 +72,16 @@ func TestDB_getDBAuthorityPolicyBytes(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if b, err := d.getDBAuthorityPolicyBytes(tc.ctx, tc.authorityID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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)
|
||||
switch {
|
||||
case err != nil:
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||
if err := d.CreateAuthorityPolicy(tc.ctx, tc.policy); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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}
|
||||
got, err := d.GetAuthorityPolicy(tc.ctx)
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||
if err := d.UpdateAuthorityPolicy(tc.ctx, tc.policy); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
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) {
|
||||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||
if err := d.DeleteAuthorityPolicy(tc.ctx); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -24,6 +24,24 @@ type dbProvisioner struct {
|
|||
SSHTemplate *linkedca.Template `json:"sshTemplate"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
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 {
|
||||
|
@ -48,6 +66,7 @@ func (dbp *dbProvisioner) convert2linkedca() (*linkedca.Provisioner, error) {
|
|||
SshTemplate: dbp.SSHTemplate,
|
||||
CreatedAt: timestamppb.New(dbp.CreatedAt),
|
||||
DeletedAt: timestamppb.New(dbp.DeletedAt),
|
||||
Webhooks: dbWebhooksToLinkedca(dbp.Webhooks),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -122,14 +141,14 @@ func (db *DB) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, err
|
|||
for _, entry := range dbEntries {
|
||||
prov, err := db.unmarshalProvisioner(entry.Value, string(entry.Key))
|
||||
if err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
if k.IsType(admin.ErrorDeletedType) || k.IsType(admin.ErrorAuthorityMismatchType) {
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if ae.IsType(admin.ErrorDeletedType) || ae.IsType(admin.ErrorAuthorityMismatchType) {
|
||||
continue
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
@ -164,6 +183,7 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
|
|||
X509Template: prov.X509Template,
|
||||
SSHTemplate: prov.SshTemplate,
|
||||
CreatedAt: clock.Now(),
|
||||
Webhooks: linkedcaWebhooksToDB(prov.Webhooks),
|
||||
}
|
||||
|
||||
if err := db.save(ctx, prov.Id, dbp, nil, "provisioner", provisionersTable); err != nil {
|
||||
|
@ -193,6 +213,7 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
|
|||
}
|
||||
nu.X509Template = prov.X509Template
|
||||
nu.SSHTemplate = prov.SshTemplate
|
||||
nu.Webhooks = linkedcaWebhooksToDB(prov.Webhooks)
|
||||
|
||||
return db.save(ctx, prov.Id, nu, old, "provisioner", provisionersTable)
|
||||
}
|
||||
|
@ -209,3 +230,70 @@ func (db *DB) DeleteProvisioner(ctx context.Context, id string) error {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -67,16 +67,16 @@ func TestDB_getDBProvisionerBytes(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if b, err := d.getDBProvisionerBytes(context.Background(), provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -137,6 +137,7 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
|||
}
|
||||
},
|
||||
"fail/deleted": func(t *testing.T) test {
|
||||
|
||||
now := clock.Now()
|
||||
dbp := &dbProvisioner{
|
||||
ID: provID,
|
||||
|
@ -189,16 +190,16 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if dbp, err := d.getDBProvisioner(context.Background(), provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -210,6 +211,7 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
|||
assert.Equals(t, dbp.Name, tc.dbp.Name)
|
||||
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
|
||||
assert.Fatal(t, dbp.DeletedAt.IsZero())
|
||||
assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -275,16 +277,16 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||
if dbp, err := d.unmarshalDBProvisioner(tc.in, provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -300,6 +302,7 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) {
|
|||
assert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate)
|
||||
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
|
||||
assert.Fatal(t, dbp.DeletedAt.IsZero())
|
||||
assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -353,6 +356,15 @@ func defaultDBP(t *testing.T) *dbProvisioner {
|
|||
Data: []byte("zap"),
|
||||
},
|
||||
CreatedAt: clock.Now(),
|
||||
Webhooks: []dbWebhook{
|
||||
{
|
||||
Name: "metadata",
|
||||
URL: "https://inventory.smallstep.com",
|
||||
Kind: linkedca.Webhook_ENRICHING.String(),
|
||||
Secret: "secret",
|
||||
BearerToken: "token",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -397,16 +409,16 @@ func TestDB_unmarshalProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||
if prov, err := d.unmarshalProvisioner(tc.in, provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -419,6 +431,7 @@ func TestDB_unmarshalProvisioner(t *testing.T) {
|
|||
assert.Equals(t, prov.Claims, tc.dbp.Claims)
|
||||
assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
|
||||
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
|
||||
assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -535,16 +548,16 @@ func TestDB_GetProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if prov, err := d.GetProvisioner(context.Background(), provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -557,6 +570,7 @@ func TestDB_GetProvisioner(t *testing.T) {
|
|||
assert.Equals(t, prov.Claims, tc.dbp.Claims)
|
||||
assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
|
||||
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
|
||||
assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -629,6 +643,7 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
|
||||
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
|
||||
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.After(time.Now().Add(-time.Minute)))
|
||||
|
@ -668,6 +683,7 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
|
||||
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
|
||||
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.After(time.Now().Add(-time.Minute)))
|
||||
|
@ -683,16 +699,16 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.DeleteProvisioner(context.Background(), provID); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -819,6 +835,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
assert.Equals(t, provs[0].Claims, fooProv.Claims)
|
||||
assert.Equals(t, provs[0].X509Template, fooProv.X509Template)
|
||||
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())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -831,6 +848,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
assert.Equals(t, provs[1].Claims, zapProv.Claims)
|
||||
assert.Equals(t, provs[1].X509Template, zapProv.X509Template)
|
||||
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())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -844,16 +862,16 @@ func TestDB_GetProvisioners(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if provs, err := d.GetProvisioners(context.Background()); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -895,6 +913,7 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -932,6 +951,7 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -952,16 +972,16 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.CreateProvisioner(context.Background(), tc.prov); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1080,6 +1100,7 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1141,6 +1162,12 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
prov.Webhooks = []*linkedca.Webhook{
|
||||
{
|
||||
Name: "users",
|
||||
Url: "https://example.com/users",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(dbp)
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1168,6 +1195,7 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||
|
||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||
assert.FatalError(t, err)
|
||||
|
@ -1188,16 +1216,16 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||
if err := d.UpdateProvisioner(context.Background(), tc.prov); err != nil {
|
||||
switch k := err.(type) {
|
||||
case *admin.Error:
|
||||
var ae *admin.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.adminErr) {
|
||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1206,3 +1234,164 @@ 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,16 +156,17 @@ func NewErrorISE(msg string, args ...interface{}) *Error {
|
|||
|
||||
// WrapError attempts to wrap the internal error.
|
||||
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
||||
switch e := err.(type) {
|
||||
case nil:
|
||||
var ee *Error
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case *Error:
|
||||
if e.Err == nil {
|
||||
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
||||
case errors.As(err, &ee):
|
||||
if ee.Err == nil {
|
||||
ee.Err = errors.Errorf(msg+"; "+ee.Detail, args...)
|
||||
} else {
|
||||
e.Err = errors.Wrapf(e.Err, msg, args...)
|
||||
ee.Err = errors.Wrapf(ee.Err, msg, args...)
|
||||
}
|
||||
return e
|
||||
return ee
|
||||
default:
|
||||
return newError(typ, errors.Wrapf(err, msg, args...))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -24,6 +26,7 @@ import (
|
|||
adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql"
|
||||
"github.com/smallstep/certificates/authority/administrator"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/internal/constraints"
|
||||
"github.com/smallstep/certificates/authority/policy"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/cas"
|
||||
|
@ -44,16 +47,18 @@ type Authority struct {
|
|||
adminDB admin.DB
|
||||
templates *templates.Templates
|
||||
linkedCAToken string
|
||||
webhookClient *http.Client
|
||||
|
||||
// X509 CA
|
||||
password []byte
|
||||
issuerPassword []byte
|
||||
x509CAService cas.CertificateAuthorityService
|
||||
rootX509Certs []*x509.Certificate
|
||||
rootX509CertPool *x509.CertPool
|
||||
federatedX509Certs []*x509.Certificate
|
||||
certificates *sync.Map
|
||||
x509Enforcers []provisioner.CertificateEnforcer
|
||||
password []byte
|
||||
issuerPassword []byte
|
||||
x509CAService cas.CertificateAuthorityService
|
||||
rootX509Certs []*x509.Certificate
|
||||
rootX509CertPool *x509.CertPool
|
||||
federatedX509Certs []*x509.Certificate
|
||||
intermediateX509Certs []*x509.Certificate
|
||||
certificates *sync.Map
|
||||
x509Enforcers []provisioner.CertificateEnforcer
|
||||
|
||||
// SCEP CA
|
||||
scepService *scep.Service
|
||||
|
@ -68,7 +73,12 @@ type Authority struct {
|
|||
sshCAUserFederatedCerts []ssh.PublicKey
|
||||
sshCAHostFederatedCerts []ssh.PublicKey
|
||||
|
||||
// Do not re-initialize
|
||||
// CRL vars
|
||||
crlTicker *time.Ticker
|
||||
crlStopper chan struct{}
|
||||
crlMutex sync.Mutex
|
||||
|
||||
// If true, do not re-initialize
|
||||
initOnce bool
|
||||
startTime time.Time
|
||||
|
||||
|
@ -80,13 +90,17 @@ type Authority struct {
|
|||
authorizeRenewFunc provisioner.AuthorizeRenewFunc
|
||||
authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc
|
||||
|
||||
// Policy engines
|
||||
policyEngine *policy.Engine
|
||||
// Constraints and Policy engines
|
||||
constraintsEngine *constraints.Engine
|
||||
policyEngine *policy.Engine
|
||||
|
||||
adminMutex sync.RWMutex
|
||||
|
||||
// Do Not initialize the authority
|
||||
// If true, do not initialize the authority
|
||||
skipInit bool
|
||||
|
||||
// If true, do not output initialization logs
|
||||
quietInit bool
|
||||
}
|
||||
|
||||
// Info contains information about the authority.
|
||||
|
@ -368,11 +382,17 @@ func (a *Authority) init() error {
|
|||
}
|
||||
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.IntermediateKey,
|
||||
Password: []byte(a.password),
|
||||
Password: a.password,
|
||||
})
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
|
@ -434,7 +454,7 @@ func (a *Authority) init() error {
|
|||
if a.config.SSH.HostKey != "" {
|
||||
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.SSH.HostKey,
|
||||
Password: []byte(a.sshHostPassword),
|
||||
Password: a.sshHostPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -460,7 +480,7 @@ func (a *Authority) init() error {
|
|||
if a.config.SSH.UserKey != "" {
|
||||
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.SSH.UserKey,
|
||||
Password: []byte(a.sshUserPassword),
|
||||
Password: a.sshUserPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -545,7 +565,7 @@ func (a *Authority) init() error {
|
|||
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...)
|
||||
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||
SigningKey: a.config.IntermediateKey,
|
||||
Password: []byte(a.password),
|
||||
Password: a.password,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -554,7 +574,7 @@ func (a *Authority) init() error {
|
|||
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
|
||||
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
|
||||
DecryptionKey: a.config.IntermediateKey,
|
||||
Password: []byte(a.password),
|
||||
Password: a.password,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -588,20 +608,74 @@ func (a *Authority) init() error {
|
|||
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")
|
||||
// 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 admin
|
||||
// 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: prov.Id,
|
||||
Subject: "step",
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -610,6 +684,21 @@ func (a *Authority) init() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Load X509 constraints engine.
|
||||
//
|
||||
// This is currently only available in CA mode.
|
||||
if size := len(a.intermediateX509Certs); size > 0 {
|
||||
last := a.intermediateX509Certs[size-1]
|
||||
constraintCerts := make([]*x509.Certificate, 0, size+1)
|
||||
constraintCerts = append(constraintCerts, a.intermediateX509Certs...)
|
||||
for _, root := range a.rootX509Certs {
|
||||
if bytes.Equal(last.RawIssuer, root.RawSubject) && bytes.Equal(last.AuthorityKeyId, root.SubjectKeyId) {
|
||||
constraintCerts = append(constraintCerts, root)
|
||||
}
|
||||
}
|
||||
a.constraintsEngine = constraints.New(constraintCerts...)
|
||||
}
|
||||
|
||||
// Load x509 and SSH Policy Engines
|
||||
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||
return err
|
||||
|
@ -627,6 +716,18 @@ func (a *Authority) init() error {
|
|||
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.
|
||||
a.startTime = time.Now().Truncate(time.Second)
|
||||
// Set flag indicating that initialization has been completed, and should
|
||||
|
@ -636,6 +737,14 @@ func (a *Authority) init() error {
|
|||
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.
|
||||
func (a *Authority) GetID() string {
|
||||
const zeroUUID = "00000000-0000-0000-0000-000000000000"
|
||||
|
@ -685,6 +794,11 @@ func (a *Authority) IsAdminAPIEnabled() bool {
|
|||
|
||||
// Shutdown safely shuts down any clients, databases, etc. held by the Authority.
|
||||
func (a *Authority) Shutdown() error {
|
||||
if a.crlTicker != nil {
|
||||
a.crlTicker.Stop()
|
||||
close(a.crlStopper)
|
||||
}
|
||||
|
||||
if err := a.keyManager.Close(); err != nil {
|
||||
log.Printf("error closing the key manager: %v", err)
|
||||
}
|
||||
|
@ -693,6 +807,11 @@ func (a *Authority) Shutdown() error {
|
|||
|
||||
// CloseForReload closes internal services, to allow a safe reload.
|
||||
func (a *Authority) CloseForReload() {
|
||||
if a.crlTicker != nil {
|
||||
a.crlTicker.Stop()
|
||||
close(a.crlStopper)
|
||||
}
|
||||
|
||||
if err := a.keyManager.Close(); err != nil {
|
||||
log.Printf("error closing the key manager: %v", err)
|
||||
}
|
||||
|
@ -733,11 +852,49 @@ func (a *Authority) requiresSCEPService() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// GetSCEPService returns the configured SCEP Service
|
||||
// TODO: this function is intended to exist temporarily
|
||||
// in order to make SCEP work more easily. It can be
|
||||
// made more correct by using the right interfaces/abstractions
|
||||
// after it works as expected.
|
||||
// GetSCEPService returns the configured SCEP Service.
|
||||
//
|
||||
// TODO: this function is intended to exist temporarily in order to make SCEP
|
||||
// work more easily. It can be made more correct by using the right
|
||||
// interfaces/abstractions after it works as expected.
|
||||
func (a *Authority) GetSCEPService() *scep.Service {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
|
@ -285,7 +286,7 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error {
|
|||
// extra extension cannot be found, authorize the renewal by default.
|
||||
//
|
||||
// TODO(mariano): should we authorize by default?
|
||||
func (a *Authority) authorizeRenew(cert *x509.Certificate) error {
|
||||
func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
||||
serial := cert.SerialNumber.String()
|
||||
var opts = []interface{}{errs.WithKeyVal("serialNumber", serial)}
|
||||
|
||||
|
@ -307,7 +308,7 @@ func (a *Authority) authorizeRenew(cert *x509.Certificate) error {
|
|||
return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...)
|
||||
}
|
||||
}
|
||||
if err := p.AuthorizeRenew(context.Background(), cert); err != nil {
|
||||
if err := p.AuthorizeRenew(ctx, cert); err != nil {
|
||||
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
|
||||
}
|
||||
return nil
|
||||
|
@ -416,16 +417,16 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.
|
|||
Subject: leaf.Subject.CommonName,
|
||||
Time: time.Now().UTC(),
|
||||
}, time.Minute); err != nil {
|
||||
switch err {
|
||||
case jose.ErrInvalidIssuer:
|
||||
switch {
|
||||
case errors.Is(err, jose.ErrInvalidIssuer):
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid issuer claim (iss)"))
|
||||
case jose.ErrInvalidSubject:
|
||||
case errors.Is(err, jose.ErrInvalidSubject):
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid subject claim (sub)"))
|
||||
case jose.ErrNotValidYet:
|
||||
case errors.Is(err, jose.ErrNotValidYet):
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token not valid yet (nbf)"))
|
||||
case jose.ErrExpired:
|
||||
case errors.Is(err, jose.ErrExpired):
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token is expired (exp)"))
|
||||
case jose.ErrIssuedInTheFuture:
|
||||
case errors.Is(err, jose.ErrIssuedInTheFuture):
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token issued in the future (iat)"))
|
||||
default:
|
||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token"))
|
||||
|
@ -433,7 +434,7 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.
|
|||
}
|
||||
|
||||
audiences := a.config.GetAudiences().Renew
|
||||
if !matchesAudience(claims.Audience, audiences) {
|
||||
if !matchesAudience(claims.Audience, audiences) && !isRAProvisioner(p) {
|
||||
return nil, errs.InternalServerErr(jose.ErrInvalidAudience, errs.WithMessage("error validating renew token: invalid audience claim (aud)"))
|
||||
}
|
||||
|
||||
|
|
|
@ -313,8 +313,8 @@ func TestAuthority_authorizeToken(t *testing.T) {
|
|||
p, err := tc.auth.authorizeToken(context.Background(), tc.token)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
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 assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
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)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, 9, len(got)) // number of provisioner.SignOptions returned
|
||||
assert.Equals(t, 10, len(got)) // number of provisioner.SignOptions returned
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -743,13 +743,13 @@ func TestAuthority_Authorize(t *testing.T) {
|
|||
if err != nil {
|
||||
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
|
||||
assert.Nil(t, got)
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
|
||||
ctxErr, ok := err.(*errs.Error)
|
||||
assert.Fatal(t, ok, "error is not of type *errs.Error")
|
||||
var ctxErr *errs.Error
|
||||
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
|
||||
assert.Equals(t, ctxErr.Details["token"], tc.token)
|
||||
}
|
||||
} else {
|
||||
|
@ -876,16 +876,16 @@ func TestAuthority_authorizeRenew(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
tc := genTestCase(t)
|
||||
|
||||
err := tc.auth.authorizeRenew(tc.cert)
|
||||
err := tc.auth.authorizeRenew(context.Background(), tc.cert)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
|
||||
ctxErr, ok := err.(*errs.Error)
|
||||
assert.Fatal(t, ok, "error is not of type *errs.Error")
|
||||
var ctxErr *errs.Error
|
||||
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
|
||||
assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String())
|
||||
}
|
||||
} else {
|
||||
|
@ -1027,14 +1027,14 @@ func TestAuthority_authorizeSSHSign(t *testing.T) {
|
|||
got, err := tc.auth.authorizeSSHSign(context.Background(), tc.token)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Len(t, 9, got) // number of provisioner.SignOptions returned
|
||||
assert.Len(t, 10, 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)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
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 assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
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)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -1459,6 +1459,37 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) {
|
|||
})
|
||||
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{
|
||||
Audience: []string{"https://example.com/1.0/renew"},
|
||||
Subject: "test.example.com",
|
||||
|
@ -1627,6 +1658,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) {
|
|||
{"ok", a1, args{ctx, t1}, c1, false},
|
||||
{"ok expired cert", a1, args{ctx, t2}, c2, 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 reuse", a1, args{ctx, t1}, nil, true},
|
||||
{"fail token signature", a1, args{ctx, badSigner}, nil, true},
|
||||
|
|
|
@ -35,8 +35,13 @@ var (
|
|||
// DefaultEnableSSHCA enable SSH CA features per provisioner or globally
|
||||
// for all provisioners.
|
||||
DefaultEnableSSHCA = false
|
||||
// GlobalProvisionerClaims default claims for the Authority. Can be overridden
|
||||
// by provisioner specific claims.
|
||||
// DefaultCRLCacheDuration is the default cache duration for the CRL.
|
||||
DefaultCRLCacheDuration = &provisioner.Duration{Duration: 24 * time.Hour}
|
||||
// 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{
|
||||
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
|
||||
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||
|
@ -72,7 +77,60 @@ type Config struct {
|
|||
Password string `json:"password,omitempty"`
|
||||
Templates *templates.Templates `json:"templates,omitempty"`
|
||||
CommonName string `json:"commonName,omitempty"`
|
||||
CRL *CRLConfig `json:"crl,omitempty"`
|
||||
SkipValidation bool `json:"-"`
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -163,6 +221,10 @@ func LoadConfiguration(filename string) (*Config, error) {
|
|||
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()
|
||||
|
||||
return &c, nil
|
||||
|
@ -183,6 +245,9 @@ func (c *Config) Init() {
|
|||
if c.CommonName == "" {
|
||||
c.CommonName = "Step Online CA"
|
||||
}
|
||||
if c.CRL != nil && c.CRL.Enabled && c.CRL.CacheDuration == nil {
|
||||
c.CRL.CacheDuration = DefaultCRLCacheDuration
|
||||
}
|
||||
c.AuthorityConfig.init()
|
||||
}
|
||||
|
||||
|
@ -199,6 +264,30 @@ func (c *Config) Save(filename string) error {
|
|||
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.
|
||||
func (c *Config) Validate() error {
|
||||
switch {
|
||||
|
@ -269,6 +358,11 @@ func (c *Config) Validate() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Validate crl config: nil is ok
|
||||
if err := c.CRL.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.AuthorityConfig.Validate(c.GetAudiences())
|
||||
}
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@ func (t *TLSOptions) TLSConfig() *tls.Config {
|
|||
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{
|
||||
CipherSuites: t.CipherSuites.Value(),
|
||||
MinVersion: t.MinVersion.Value(),
|
||||
|
|
135
authority/internal/constraints/constraints.go
Normal file
135
authority/internal/constraints/constraints.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
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)
|
||||
}
|
334
authority/internal/constraints/constraints_test.go
Normal file
334
authority/internal/constraints/constraints_test.go
Normal file
|
@ -0,0 +1,334 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
383
authority/internal/constraints/verify.go
Normal file
383
authority/internal/constraints/verify.go
Normal file
|
@ -0,0 +1,383 @@
|
|||
// 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
|
||||
}
|
|
@ -278,6 +278,7 @@ func (c *linkedCaClient) StoreCertificateChain(p provisioner.Interface, fullchai
|
|||
PemCertificate: serializeCertificateChain(fullchain[0]),
|
||||
PemCertificateChain: serializeCertificateChain(fullchain[1:]...),
|
||||
Provisioner: createProvisionerIdentity(p),
|
||||
AttestationData: createAttestationData(p),
|
||||
RaProvisioner: raProvisioner,
|
||||
EndpointId: endpointID,
|
||||
})
|
||||
|
@ -395,26 +396,34 @@ func createProvisionerIdentity(p provisioner.Interface) *linkedca.ProvisionerIde
|
|||
}
|
||||
}
|
||||
|
||||
type raProvisioner interface {
|
||||
RAInfo() *provisioner.RAInfo
|
||||
}
|
||||
|
||||
func createRegistrationAuthorityProvisioner(p provisioner.Interface) (*linkedca.RegistrationAuthorityProvisioner, string) {
|
||||
if rap, ok := p.(raProvisioner); ok {
|
||||
info := rap.RAInfo()
|
||||
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
|
||||
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 {
|
||||
if ap, ok := p.(attProvisioner); ok {
|
||||
if data := ap.AttestationData(); data != nil {
|
||||
return &linkedca.AttestationData{
|
||||
PermanentIdentifier: data.PermanentIdentifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func serializeCertificate(crt *x509.Certificate) string {
|
||||
if crt == nil {
|
||||
return ""
|
||||
|
@ -461,7 +470,7 @@ func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error)
|
|||
defer cancel()
|
||||
|
||||
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]
|
||||
})))
|
||||
if err != nil {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
@ -85,6 +86,22 @@ 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
|
||||
// an external resource.
|
||||
func WithGetIdentityFunc(fn func(ctx context.Context, p provisioner.Interface, email string) (*provisioner.Identity, error)) Option {
|
||||
|
@ -151,16 +168,23 @@ func WithKeyManager(k kms.KeyManager) Option {
|
|||
|
||||
// WithX509Signer defines the signer used to sign X509 certificates.
|
||||
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 {
|
||||
srv, err := cas.New(context.Background(), casapi.Options{
|
||||
Type: casapi.SoftCAS,
|
||||
Signer: s,
|
||||
CertificateChain: []*x509.Certificate{crt},
|
||||
CertificateChain: issuerChain,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.x509CAService = srv
|
||||
a.intermediateX509Certs = append(a.intermediateX509Certs, issuerChain...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -233,6 +257,25 @@ 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
|
||||
// certificates. This option will replace any root certificate defined before.
|
||||
func WithX509RootBundle(pemCerts []byte) Option {
|
||||
|
|
|
@ -119,7 +119,6 @@ func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *linkedca.Admin, p *linkedca.Policy) error {
|
||||
|
||||
// no policy and thus nothing to evaluate; return early
|
||||
if p == nil {
|
||||
return nil
|
||||
|
@ -138,7 +137,6 @@ func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *link
|
|||
}
|
||||
|
||||
func (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string, p *linkedca.Policy) error {
|
||||
|
||||
// no policy and thus nothing to evaluate; return early
|
||||
if p == nil {
|
||||
return nil
|
||||
|
@ -157,7 +155,6 @@ func (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string,
|
|||
// checkPolicy checks if a new or updated policy configuration results in the user
|
||||
// locking themselves or other admins out of the CA.
|
||||
func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error {
|
||||
|
||||
// convert the policy; return early if nil
|
||||
policyOptions := authPolicy.LinkedToCertificates(p)
|
||||
if policyOptions == nil {
|
||||
|
@ -216,7 +213,6 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error {
|
|||
)
|
||||
|
||||
if a.config.AuthorityConfig.EnableAdmin {
|
||||
|
||||
// temporarily disable policy loading when LinkedCA is in use
|
||||
if _, ok := a.adminDB.(*linkedCaClient); ok {
|
||||
return nil
|
||||
|
|
|
@ -17,9 +17,9 @@ type Engine struct {
|
|||
|
||||
// New returns a new Engine using Options.
|
||||
func New(options *Options) (*Engine, error) {
|
||||
|
||||
// if no options provided, return early
|
||||
if options == nil {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,6 @@ func New(options *Options) (*Engine, error) {
|
|||
// the X.509 policy (if available) and returns an error if one of the
|
||||
// names in the certificate is not allowed.
|
||||
func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {
|
||||
|
||||
// return early if there's no policy to evaluate
|
||||
if e == nil || e.x509Policy == nil {
|
||||
return nil
|
||||
|
@ -69,7 +68,6 @@ func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {
|
|||
// 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.
|
||||
func (e *Engine) AreSANsAllowed(sans []string) error {
|
||||
|
||||
// return early if there's no policy to evaluate
|
||||
if e == nil || e.x509Policy == nil {
|
||||
return nil
|
||||
|
@ -83,7 +81,6 @@ func (e *Engine) AreSANsAllowed(sans []string) error {
|
|||
// user or host policy (if configured) and returns an error if one of the
|
||||
// principals in the certificate is not allowed.
|
||||
func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
|
||||
|
||||
// return early if there's no policy to evaluate
|
||||
if e == nil || (e.sshHostPolicy == nil && e.sshUserPolicy == nil) {
|
||||
return nil
|
||||
|
|
|
@ -19,7 +19,6 @@ type HostPolicy policy.SSHNamePolicyEngine
|
|||
|
||||
// NewX509PolicyEngine creates a new x509 name policy engine
|
||||
func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, error) {
|
||||
|
||||
// return early if no policy engine options to configure
|
||||
if policyOptions == nil {
|
||||
return nil, nil
|
||||
|
@ -92,7 +91,6 @@ func NewSSHHostPolicyEngine(policyOptions SSHPolicyOptionsInterface) (HostPolicy
|
|||
|
||||
// newSSHPolicyEngine creates a new SSH name policy engine
|
||||
func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) {
|
||||
|
||||
// return early if no policy engine options to configure
|
||||
if policyOptions == nil {
|
||||
return nil, nil
|
||||
|
@ -143,7 +141,6 @@ func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEn
|
|||
}
|
||||
|
||||
func LinkedToCertificates(p *linkedca.Policy) *Options {
|
||||
|
||||
// return early
|
||||
if p == nil {
|
||||
return nil
|
||||
|
|
|
@ -185,11 +185,11 @@ func TestAuthority_checkPolicy(t *testing.T) {
|
|||
} else {
|
||||
assert.IsType(t, &PolicyError{}, err)
|
||||
|
||||
pe, ok := err.(*PolicyError)
|
||||
assert.True(t, ok)
|
||||
|
||||
assert.Equal(t, tc.err.Typ, pe.Typ)
|
||||
assert.Equal(t, tc.err.Error(), pe.Error())
|
||||
var pe *PolicyError
|
||||
if assert.True(t, errors.As(err, &pe)) {
|
||||
assert.Equal(t, tc.err.Typ, pe.Typ)
|
||||
assert.Equal(t, tc.err.Error(), pe.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1179,10 +1179,11 @@ func TestAuthority_RemoveAuthorityPolicy(t *testing.T) {
|
|||
}
|
||||
err := a.RemoveAuthorityPolicy(tt.args.ctx)
|
||||
if err != nil {
|
||||
pe, ok := err.(*PolicyError)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||
var pe *PolicyError
|
||||
if assert.True(t, errors.As(err, &pe)) {
|
||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
|
@ -1250,10 +1251,11 @@ func TestAuthority_GetAuthorityPolicy(t *testing.T) {
|
|||
}
|
||||
got, err := a.GetAuthorityPolicy(tt.args.ctx)
|
||||
if err != nil {
|
||||
pe, ok := err.(*PolicyError)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||
var pe *PolicyError
|
||||
if assert.True(t, errors.As(err, &pe)) {
|
||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
|
@ -1429,10 +1431,11 @@ func TestAuthority_CreateAuthorityPolicy(t *testing.T) {
|
|||
}
|
||||
got, err := a.CreateAuthorityPolicy(tt.args.ctx, tt.args.adm, tt.args.p)
|
||||
if err != nil {
|
||||
pe, ok := err.(*PolicyError)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||
var pe *PolicyError
|
||||
if assert.True(t, errors.As(err, &pe)) {
|
||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
|
@ -1611,10 +1614,11 @@ func TestAuthority_UpdateAuthorityPolicy(t *testing.T) {
|
|||
}
|
||||
got, err := a.UpdateAuthorityPolicy(tt.args.ctx, tt.args.adm, tt.args.p)
|
||||
if err != nil {
|
||||
pe, ok := err.(*PolicyError)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||
var pe *PolicyError
|
||||
if assert.True(t, errors.As(err, &pe)) {
|
||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
|
|
|
@ -10,12 +10,13 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
// ACMEChallenge represents the supported acme challenges.
|
||||
type ACMEChallenge string
|
||||
|
||||
// nolint:revive // better names
|
||||
//nolint:stylecheck,revive // better names
|
||||
const (
|
||||
// HTTP_01 is the http-01 ACME challenge.
|
||||
HTTP_01 ACMEChallenge = "http-01"
|
||||
|
@ -83,6 +84,17 @@ type ACME struct {
|
|||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ForceCN bool `json:"forceCN,omitempty"`
|
||||
// TermsOfService contains a URL pointing to the ACME server's
|
||||
// terms of service. Defaults to empty.
|
||||
TermsOfService string `json:"termsOfService,omitempty"`
|
||||
// Website contains an URL pointing to more information about
|
||||
// the ACME server. Defaults to empty.
|
||||
Website string `json:"website,omitempty"`
|
||||
// CaaIdentities is an array of hostnames that the ACME server
|
||||
// identifies itself with. These hostnames can be used by ACME
|
||||
// clients to determine the correct issuer domain name to use
|
||||
// when configuring CAA records. Defaults to empty array.
|
||||
CaaIdentities []string `json:"caaIdentities,omitempty"`
|
||||
// RequireEAB makes the provisioner require ACME EAB to be provided
|
||||
// by clients when creating a new Account. If set to true, the provided
|
||||
// EAB will be verified. If set to false and an EAB is provided, it is
|
||||
|
@ -217,7 +229,6 @@ type ACMEIdentifier struct {
|
|||
// AuthorizeOrderIdentifier verifies the provisioner is allowed to issue a
|
||||
// certificate for an ACME Order Identifier.
|
||||
func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIdentifier) error {
|
||||
|
||||
x509Policy := p.ctl.getPolicy().getX509()
|
||||
|
||||
// identifier is allowed if no policy is configured
|
||||
|
@ -253,22 +264,22 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(nil, linkedca.Webhook_X509),
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// AuthorizeRevoke is called just before the certificate is to be revoked by
|
||||
// the CA. It can be used to authorize revocation of a certificate. It
|
||||
// currently is a no-op.
|
||||
// TODO(hs): add configuration option that toggles revocation? Or change function signature to make it more useful?
|
||||
// Or move certain logic out of the Revoke API to here? Would likely involve some more stuff in the ctx.
|
||||
// the CA. It can be used to authorize revocation of a certificate. With the
|
||||
// ACME protocol, revocation authorization is specified and performed as part
|
||||
// of the client/server interaction, so this is a no-op.
|
||||
func (p *ACME) AuthorizeRevoke(ctx context.Context, token string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthorizeRenew returns an error if the renewal is disabled.
|
||||
// NOTE: This method does not actually validate the certificate or check it's
|
||||
// NOTE: This method does not actually validate the certificate or check its
|
||||
// revocation status. Just confirms that the provisioner that created the
|
||||
// certificate was configured to allow renewals.
|
||||
func (p *ACME) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
||||
|
|
82
authority/provisioner/acme_118_test.go
Normal file
82
authority/provisioner/acme_118_test.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package provisioner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestACME_GetAttestationRoots(t *testing.T) {
|
||||
appleCA, err := os.ReadFile("testdata/certs/apple-att-ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
yubicoCA, err := os.ReadFile("testdata/certs/yubico-piv-ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
pool.AppendCertsFromPEM(appleCA)
|
||||
pool.AppendCertsFromPEM(yubicoCA)
|
||||
|
||||
type fields struct {
|
||||
Type string
|
||||
Name string
|
||||
AttestationRoots []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want *x509.CertPool
|
||||
want1 bool
|
||||
}{
|
||||
{"ok", fields{"ACME", "acme", bytes.Join([][]byte{appleCA, yubicoCA}, []byte("\n"))}, pool, true},
|
||||
{"nil", fields{"ACME", "acme", nil}, nil, false},
|
||||
{"empty", fields{"ACME", "acme", []byte{}}, nil, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &ACME{
|
||||
Type: tt.fields.Type,
|
||||
Name: tt.fields.Name,
|
||||
AttestationRoots: tt.fields.AttestationRoots,
|
||||
}
|
||||
if err := p.Init(Config{
|
||||
Claims: globalProvisionerClaims,
|
||||
Audiences: testAudiences,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, got1 := p.GetAttestationRoots()
|
||||
switch {
|
||||
case tt.want == nil && got == nil:
|
||||
break
|
||||
case tt.want == nil && got != nil, tt.want != nil && got == nil:
|
||||
t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want)
|
||||
default:
|
||||
//nolint:staticcheck // this file only runs in go1.18
|
||||
gotSubjects := got.Subjects()
|
||||
//nolint:staticcheck // this file only runs in go1.18
|
||||
wantSubjects := tt.want.Subjects()
|
||||
if len(gotSubjects) != len(wantSubjects) {
|
||||
t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want)
|
||||
} else {
|
||||
for i, gotSub := range gotSubjects {
|
||||
if !bytes.Equal(gotSub, wantSubjects[i]) {
|
||||
t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("ACME.GetAttestationRoots() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
66
authority/provisioner/acme_119_test.go
Normal file
66
authority/provisioner/acme_119_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
//go:build !go1.18
|
||||
// +build !go1.18
|
||||
|
||||
package provisioner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestACME_GetAttestationRoots(t *testing.T) {
|
||||
appleCA, err := os.ReadFile("testdata/certs/apple-att-ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
yubicoCA, err := os.ReadFile("testdata/certs/yubico-piv-ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
pool.AppendCertsFromPEM(appleCA)
|
||||
pool.AppendCertsFromPEM(yubicoCA)
|
||||
|
||||
type fields struct {
|
||||
Type string
|
||||
Name string
|
||||
AttestationRoots []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want *x509.CertPool
|
||||
want1 bool
|
||||
}{
|
||||
{"ok", fields{"ACME", "acme", bytes.Join([][]byte{appleCA, yubicoCA}, []byte("\n"))}, pool, true},
|
||||
{"nil", fields{"ACME", "acme", nil}, nil, false},
|
||||
{"empty", fields{"ACME", "acme", []byte{}}, nil, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &ACME{
|
||||
Type: tt.fields.Type,
|
||||
Name: tt.fields.Name,
|
||||
AttestationRoots: tt.fields.AttestationRoots,
|
||||
}
|
||||
if err := p.Init(Config{
|
||||
Claims: globalProvisionerClaims,
|
||||
Audiences: testAudiences,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, got1 := p.GetAttestationRoots()
|
||||
if tt.want == nil && got != nil {
|
||||
t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want)
|
||||
} else if !tt.want.Equal(got) {
|
||||
t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("ACME.GetAttestationRoots() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
//go:build !go1.18
|
||||
// +build !go1.18
|
||||
|
||||
package provisioner
|
||||
|
||||
import (
|
||||
|
@ -266,7 +269,7 @@ func TestACME_AuthorizeSign(t *testing.T) {
|
|||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) && assert.NotNil(t, opts) {
|
||||
assert.Equals(t, 7, len(opts)) // number of SignOptions returned
|
||||
assert.Equals(t, 8, len(opts)) // number of SignOptions returned
|
||||
for _, o := range opts {
|
||||
switch v := o.(type) {
|
||||
case *ACME:
|
||||
|
@ -285,6 +288,8 @@ func TestACME_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
@ -371,58 +376,3 @@ func TestACME_IsAttestationFormatEnabled(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACME_GetAttestationRoots(t *testing.T) {
|
||||
appleCA, err := os.ReadFile("testdata/certs/apple-att-ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
yubicoCA, err := os.ReadFile("testdata/certs/yubico-piv-ca.crt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
pool.AppendCertsFromPEM(appleCA)
|
||||
pool.AppendCertsFromPEM(yubicoCA)
|
||||
|
||||
type fields struct {
|
||||
Type string
|
||||
Name string
|
||||
AttestationRoots []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want *x509.CertPool
|
||||
want1 bool
|
||||
}{
|
||||
{"ok", fields{"ACME", "acme", bytes.Join([][]byte{appleCA, yubicoCA}, []byte("\n"))}, pool, true},
|
||||
{"nil", fields{"ACME", "acme", nil}, nil, false},
|
||||
{"empty", fields{"ACME", "acme", []byte{}}, nil, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &ACME{
|
||||
Type: tt.fields.Type,
|
||||
Name: tt.fields.Name,
|
||||
AttestationRoots: tt.fields.AttestationRoots,
|
||||
}
|
||||
if err := p.Init(Config{
|
||||
Claims: globalProvisionerClaims,
|
||||
Audiences: testAudiences,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, got1 := p.GetAttestationRoots()
|
||||
if tt.want == nil && got != nil {
|
||||
t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want)
|
||||
} else if !tt.want.Equal(got) {
|
||||
t.Errorf("ACME.GetAttestationRoots() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("ACME.GetAttestationRoots() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -35,20 +36,17 @@ const awsIdentityURL = "http://169.254.169.254/latest/dynamic/instance-identity/
|
|||
const awsSignatureURL = "http://169.254.169.254/latest/dynamic/instance-identity/signature"
|
||||
|
||||
// awsAPITokenURL is the url used to get the IMDSv2 API token
|
||||
// nolint:gosec // no credentials here
|
||||
const awsAPITokenURL = "http://169.254.169.254/latest/api/token"
|
||||
const awsAPITokenURL = "http://169.254.169.254/latest/api/token" //nolint:gosec // no credentials here
|
||||
|
||||
// awsAPITokenTTL is the default TTL to use when requesting IMDSv2 API tokens
|
||||
// -- we keep this short-lived since we get a new token with every call to readURL()
|
||||
const awsAPITokenTTL = "30"
|
||||
|
||||
// awsMetadataTokenHeader is the header that must be passed with every IMDSv2 request
|
||||
// nolint:gosec // no credentials here
|
||||
const awsMetadataTokenHeader = "X-aws-ec2-metadata-token"
|
||||
const awsMetadataTokenHeader = "X-aws-ec2-metadata-token" //nolint:gosec // no credentials here
|
||||
|
||||
// awsMetadataTokenTTLHeader is the header used to indicate the token TTL requested
|
||||
// nolint:gosec // no credentials here
|
||||
const awsMetadataTokenTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds"
|
||||
const awsMetadataTokenTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds" //nolint:gosec // no credentials here
|
||||
|
||||
// awsCertificate is the certificate used to validate the instance identity
|
||||
// signature.
|
||||
|
@ -487,6 +485,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
commonNameValidator(payload.Claims.Subject),
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
@ -768,5 +767,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
|
|
@ -522,8 +522,8 @@ func TestAWS_authorizeToken(t *testing.T) {
|
|||
tc := tt(t)
|
||||
if claims, err := tc.p.authorizeToken(tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -642,11 +642,11 @@ func TestAWS_AuthorizeSign(t *testing.T) {
|
|||
code int
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", p1, args{t1, "foo.local"}, 8, http.StatusOK, false},
|
||||
{"ok", p2, args{t2, "instance-id"}, 12, http.StatusOK, false},
|
||||
{"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 12, http.StatusOK, false},
|
||||
{"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 12, http.StatusOK, false},
|
||||
{"ok", p1, args{t4, "instance-id"}, 8, http.StatusOK, false},
|
||||
{"ok", p1, args{t1, "foo.local"}, 9, http.StatusOK, false},
|
||||
{"ok", p2, args{t2, "instance-id"}, 13, http.StatusOK, false},
|
||||
{"ok", p2, args{t2Hostname, "ip-127-0-0-1.us-west-1.compute.internal"}, 13, http.StatusOK, false},
|
||||
{"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 13, http.StatusOK, false},
|
||||
{"ok", p1, args{t4, "instance-id"}, 9, http.StatusOK, false},
|
||||
{"fail account", p3, args{token: t3}, 0, http.StatusUnauthorized, true},
|
||||
{"fail token", p1, args{token: "token"}, 0, http.StatusUnauthorized, true},
|
||||
{"fail subject", p1, args{token: failSubject}, 0, http.StatusUnauthorized, true},
|
||||
|
@ -669,8 +669,8 @@ func TestAWS_AuthorizeSign(t *testing.T) {
|
|||
t.Errorf("AWS.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
case err != nil:
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
default:
|
||||
assert.Equals(t, tt.wantLen, len(got))
|
||||
|
@ -701,6 +701,8 @@ func TestAWS_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, []string(v), []string{"ip-127-0-0-1.us-west-1.compute.internal"})
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
@ -748,7 +750,7 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) {
|
|||
pub := key.Public().Key
|
||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
assert.FatalError(t, err)
|
||||
// nolint:gosec // tests minimum size of the key
|
||||
//nolint:gosec // tests minimum size of the key
|
||||
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
|
@ -807,8 +809,8 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.Nil(t, got)
|
||||
} else if assert.NotNil(t, got) {
|
||||
|
@ -864,8 +866,8 @@ func TestAWS_AuthorizeRenew(t *testing.T) {
|
|||
if err := tt.aws.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("AWS.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
} else if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -24,8 +25,7 @@ import (
|
|||
// azureOIDCBaseURL is the base discovery url for Microsoft Azure tokens.
|
||||
const azureOIDCBaseURL = "https://login.microsoftonline.com"
|
||||
|
||||
// azureIdentityTokenURL is the URL to get the identity token for an instance.
|
||||
// nolint:gosec // no credentials here
|
||||
//nolint:gosec // azureIdentityTokenURL is the URL to get the identity token for an instance.
|
||||
const azureIdentityTokenURL = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F"
|
||||
|
||||
// azureDefaultAudience is the default audience used.
|
||||
|
@ -364,6 +364,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
@ -432,6 +433,8 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -336,8 +336,8 @@ func TestAzure_authorizeToken(t *testing.T) {
|
|||
tc := tt(t)
|
||||
if claims, name, group, subscriptionID, objectID, err := tc.p.authorizeToken(tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -474,11 +474,11 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
|||
code int
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", p1, args{t1}, 7, http.StatusOK, false},
|
||||
{"ok", p2, args{t2}, 12, http.StatusOK, false},
|
||||
{"ok", p1, args{t11}, 7, http.StatusOK, false},
|
||||
{"ok", p5, args{t5}, 7, http.StatusOK, false},
|
||||
{"ok", p7, args{t7}, 7, http.StatusOK, false},
|
||||
{"ok", p1, args{t1}, 8, http.StatusOK, false},
|
||||
{"ok", p2, args{t2}, 13, http.StatusOK, false},
|
||||
{"ok", p1, args{t11}, 8, http.StatusOK, false},
|
||||
{"ok", p5, args{t5}, 8, http.StatusOK, false},
|
||||
{"ok", p7, args{t7}, 8, http.StatusOK, false},
|
||||
{"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true},
|
||||
{"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true},
|
||||
{"fail subscription", p6, args{t6}, 0, http.StatusUnauthorized, true},
|
||||
|
@ -498,8 +498,8 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
|||
t.Errorf("Azure.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
case err != nil:
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
default:
|
||||
assert.Equals(t, tt.wantLen, len(got))
|
||||
|
@ -530,6 +530,8 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, []string(v), []string{"virtualMachine"})
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
@ -576,8 +578,8 @@ func TestAzure_AuthorizeRenew(t *testing.T) {
|
|||
if err := tt.azure.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Azure.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
} else if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
}
|
||||
})
|
||||
|
@ -624,7 +626,7 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) {
|
|||
pub := key.Public().Key
|
||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
assert.FatalError(t, err)
|
||||
// nolint:gosec // tests minimum size of the key
|
||||
//nolint:gosec // tests minimum size of the key
|
||||
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
|
@ -673,8 +675,8 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.Nil(t, got)
|
||||
} else if assert.NotNil(t, got) {
|
||||
|
|
|
@ -38,7 +38,8 @@ type Claimer struct {
|
|||
// NewClaimer initializes a new claimer with the given claims.
|
||||
func NewClaimer(claims *Claims, global Claims) (*Claimer, error) {
|
||||
c := &Claimer{global: global, claims: claims}
|
||||
return c, c.Validate()
|
||||
err := c.Validate()
|
||||
return c, err
|
||||
}
|
||||
|
||||
// Claims returns the merge of the inner and global claims.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"crypto/sha1" // nolint:gosec // not used for cryptographic security
|
||||
"crypto/sha1" //nolint:gosec // not used for cryptographic security
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/binary"
|
||||
|
@ -319,7 +319,7 @@ func loadProvisioner(m *sync.Map, key string) (Interface, bool) {
|
|||
// provisionerSum returns the SHA1 of the provisioners ID. From this we will
|
||||
// create the unique and sorted id.
|
||||
func provisionerSum(p Interface) []byte {
|
||||
// nolint:gosec // not used for cryptographic security
|
||||
//nolint:gosec // not used for cryptographic security
|
||||
sum := sha1.Sum([]byte(p.GetID()))
|
||||
return sum[:]
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"go.step.sm/linkedca"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
|
@ -23,6 +24,8 @@ type Controller struct {
|
|||
AuthorizeRenewFunc AuthorizeRenewFunc
|
||||
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
|
||||
policy *policyEngine
|
||||
webhookClient *http.Client
|
||||
webhooks []*Webhook
|
||||
}
|
||||
|
||||
// NewController initializes a new provisioner controller.
|
||||
|
@ -43,6 +46,8 @@ func NewController(p Interface, claims *Claims, config Config, options *Options)
|
|||
AuthorizeRenewFunc: config.AuthorizeRenewFunc,
|
||||
AuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc,
|
||||
policy: policy,
|
||||
webhookClient: config.WebhookClient,
|
||||
webhooks: options.GetWebhooks(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -72,6 +77,19 @@ func (c *Controller) AuthorizeSSHRenew(ctx context.Context, cert *ssh.Certificat
|
|||
return DefaultAuthorizeSSHRenew(ctx, c, cert)
|
||||
}
|
||||
|
||||
func (c *Controller) newWebhookController(templateData WebhookSetter, certType linkedca.Webhook_CertType) *WebhookController {
|
||||
client := c.webhookClient
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return &WebhookController{
|
||||
TemplateData: templateData,
|
||||
client: client,
|
||||
webhooks: c.webhooks,
|
||||
certType: certType,
|
||||
}
|
||||
}
|
||||
|
||||
// Identity is the type representing an externally supplied identity that is used
|
||||
// by provisioners to populate certificate fields.
|
||||
type Identity struct {
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/smallstep/certificates/authority/policy"
|
||||
|
@ -445,3 +447,18 @@ func TestDefaultAuthorizeSSHRenew(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newWebhookController(t *testing.T) {
|
||||
c := &Controller{}
|
||||
data := x509util.TemplateData{"foo": "bar"}
|
||||
ctl := c.newWebhookController(data, linkedca.Webhook_X509)
|
||||
if !reflect.DeepEqual(ctl.TemplateData, data) {
|
||||
t.Error("Failed to set templateData")
|
||||
}
|
||||
if ctl.certType != linkedca.Webhook_X509 {
|
||||
t.Error("Failed to set certType")
|
||||
}
|
||||
if ctl.client == nil {
|
||||
t.Error("Failed to set client")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -102,7 +103,6 @@ func (p *GCP) GetID() string {
|
|||
return p.ID
|
||||
}
|
||||
return p.GetIDForToken()
|
||||
|
||||
}
|
||||
|
||||
// GetIDForToken returns an identifier that will be used to load the provisioner
|
||||
|
@ -273,6 +273,7 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
@ -438,5 +439,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
|
|
@ -391,8 +391,8 @@ func TestGCP_authorizeToken(t *testing.T) {
|
|||
tc := tt(t)
|
||||
if claims, err := tc.p.authorizeToken(tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -516,9 +516,9 @@ func TestGCP_AuthorizeSign(t *testing.T) {
|
|||
code int
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", p1, args{t1}, 7, http.StatusOK, false},
|
||||
{"ok", p2, args{t2}, 12, http.StatusOK, false},
|
||||
{"ok", p3, args{t3}, 7, http.StatusOK, false},
|
||||
{"ok", p1, args{t1}, 8, http.StatusOK, false},
|
||||
{"ok", p2, args{t2}, 13, http.StatusOK, false},
|
||||
{"ok", p3, args{t3}, 8, http.StatusOK, false},
|
||||
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
|
||||
{"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true},
|
||||
{"fail iss", p1, args{failIss}, 0, http.StatusUnauthorized, true},
|
||||
|
@ -541,8 +541,8 @@ func TestGCP_AuthorizeSign(t *testing.T) {
|
|||
t.Errorf("GCP.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
case err != nil:
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
default:
|
||||
assert.Equals(t, tt.wantLen, len(got))
|
||||
|
@ -573,6 +573,8 @@ func TestGCP_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, []string(v), []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"})
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
@ -623,7 +625,7 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
|
|||
pub := key.Public().Key
|
||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
assert.FatalError(t, err)
|
||||
// nolint:gosec // tests minimum size of the key
|
||||
//nolint:gosec // tests minimum size of the key
|
||||
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
|
@ -682,8 +684,8 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.Nil(t, got)
|
||||
} else if assert.NotNil(t, got) {
|
||||
|
@ -739,8 +741,8 @@ func TestGCP_AuthorizeRenew(t *testing.T) {
|
|||
if err := tt.prov.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("GCP.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
} else if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -194,6 +195,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
|||
defaultSANsValidator(claims.SANs),
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -278,6 +280,8 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -185,8 +185,8 @@ func TestJWK_authorizeToken(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got, err := tt.prov.authorizeToken(tt.args.token, testAudiences.Sign); err != nil {
|
||||
if assert.NotNil(t, tt.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
||||
}
|
||||
|
@ -225,8 +225,8 @@ func TestJWK_AuthorizeRevoke(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.prov.AuthorizeRevoke(context.Background(), tt.args.token); err != nil {
|
||||
if assert.NotNil(t, tt.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
||||
}
|
||||
|
@ -290,14 +290,14 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
|||
ctx := NewContextWithMethod(context.Background(), SignMethod)
|
||||
if got, err := tt.prov.AuthorizeSign(ctx, tt.args.token); err != nil {
|
||||
if assert.NotNil(t, tt.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, got) {
|
||||
assert.Equals(t, 9, len(got))
|
||||
assert.Equals(t, 10, len(got))
|
||||
for _, o := range got {
|
||||
switch v := o.(type) {
|
||||
case *JWK:
|
||||
|
@ -319,6 +319,7 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, []string(v), tt.sans)
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
@ -366,8 +367,8 @@ func TestJWK_AuthorizeRenew(t *testing.T) {
|
|||
if err := tt.prov.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("JWK.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
} else if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
}
|
||||
})
|
||||
|
@ -411,7 +412,7 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) {
|
|||
pub := key.Public().Key
|
||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
assert.FatalError(t, err)
|
||||
// nolint:gosec // tests minimum size of the key
|
||||
//nolint:gosec // tests minimum size of the key
|
||||
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
|
@ -461,8 +462,8 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.Nil(t, got)
|
||||
} else if assert.NotNil(t, got) {
|
||||
|
@ -626,8 +627,8 @@ func TestJWK_AuthorizeSSHRevoke(t *testing.T) {
|
|||
tc := tt(t)
|
||||
if err := tc.p.AuthorizeSSHRevoke(context.Background(), tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -93,7 +94,6 @@ func (p *K8sSA) GetEncryptedKey() (string, string, bool) {
|
|||
|
||||
// Init initializes and validates the fields of a K8sSA type.
|
||||
func (p *K8sSA) Init(config Config) (err error) {
|
||||
|
||||
switch {
|
||||
case p.Type == "":
|
||||
return errors.New("provisioner type cannot be empty")
|
||||
|
@ -243,6 +243,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -288,6 +289,8 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -118,8 +118,8 @@ func TestK8sSA_authorizeToken(t *testing.T) {
|
|||
tc := tt(t)
|
||||
if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -167,8 +167,8 @@ func TestK8sSA_AuthorizeRevoke(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
tc := tt(t)
|
||||
if err := tc.p.AuthorizeRevoke(context.Background(), tc.token); err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
|
@ -223,8 +223,8 @@ func TestK8sSA_AuthorizeRenew(t *testing.T) {
|
|||
t.Run(name, func(t *testing.T) {
|
||||
tc := tt(t)
|
||||
if err := tc.p.AuthorizeRenew(context.Background(), tc.cert); err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
|
@ -272,8 +272,8 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
|
|||
tc := tt(t)
|
||||
if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
|
@ -297,11 +297,13 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
}
|
||||
assert.Equals(t, 7, len(opts))
|
||||
assert.Equals(t, 8, len(opts))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -360,15 +362,15 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
|
|||
tc := tt(t)
|
||||
if opts, err := tc.p.AuthorizeSSHSign(context.Background(), tc.token); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
if assert.NotNil(t, opts) {
|
||||
assert.Len(t, 8, opts)
|
||||
assert.Len(t, 9, opts)
|
||||
for _, o := range opts {
|
||||
switch v := o.(type) {
|
||||
case Interface:
|
||||
|
@ -384,6 +386,8 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
|
|||
case *sshNamePolicyValidator:
|
||||
assert.Equals(t, nil, v.userPolicyEngine)
|
||||
assert.Equals(t, nil, v.hostPolicyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
|
|
@ -85,14 +85,14 @@ func (ks *keyStore) reload() {
|
|||
// 0 it will randomly rotate between 0-12 hours, but every time we call to Get
|
||||
// it will automatically rotate.
|
||||
func (ks *keyStore) nextReloadDuration(age time.Duration) time.Duration {
|
||||
n := rand.Int63n(int64(ks.jitter)) // nolint:gosec // not used for cryptographic security
|
||||
n := rand.Int63n(int64(ks.jitter)) //nolint:gosec // not used for cryptographic security
|
||||
age -= time.Duration(n)
|
||||
return abs(age)
|
||||
}
|
||||
|
||||
func getKeysFromJWKsURI(uri string) (jose.JSONWebKeySet, time.Duration, error) {
|
||||
var keys jose.JSONWebKeySet
|
||||
resp, err := http.Get(uri) // nolint:gosec // openid-configuration jwks_uri
|
||||
resp, err := http.Get(uri) //nolint:gosec // openid-configuration jwks_uri
|
||||
if err != nil {
|
||||
return keys, 0, errors.Wrapf(err, "failed to connect to %s", uri)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x25519"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
|
@ -164,6 +165,7 @@ func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -262,6 +264,8 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||
// Call webhooks
|
||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ func (p *noop) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
|
|||
}
|
||||
|
||||
func (p *noop) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
|
||||
//nolint:nilnil // fine for noop
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -356,6 +357,8 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
|||
defaultPublicKeyValidator{},
|
||||
newValidityValidator(o.ctl.Claimer.MinTLSCertDuration(), o.ctl.Claimer.MaxTLSCertDuration()),
|
||||
newX509NamePolicyValidator(o.ctl.getPolicy().getX509()),
|
||||
// webhooks
|
||||
o.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -460,6 +463,8 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
|
|||
&sshCertDefaultValidator{},
|
||||
// Ensure that all principal names are allowed
|
||||
newSSHNamePolicyValidator(o.ctl.getPolicy().getSSHHost(), o.ctl.getPolicy().getSSHUser()),
|
||||
// Call webhooks
|
||||
o.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||
), nil
|
||||
}
|
||||
|
||||
|
@ -479,7 +484,7 @@ func (o *OIDC) AuthorizeSSHRevoke(ctx context.Context, token string) error {
|
|||
}
|
||||
|
||||
func getAndDecode(uri string, v interface{}) error {
|
||||
resp, err := http.Get(uri) // nolint:gosec // openid-configuration uri
|
||||
resp, err := http.Get(uri) //nolint:gosec // openid-configuration uri
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to connect to %s", uri)
|
||||
}
|
||||
|
|
|
@ -247,8 +247,8 @@ func TestOIDC_authorizeToken(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.Nil(t, got)
|
||||
} else {
|
||||
|
@ -318,12 +318,12 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.Nil(t, got)
|
||||
} else if assert.NotNil(t, got) {
|
||||
assert.Equals(t, 7, len(got))
|
||||
assert.Equals(t, 8, len(got))
|
||||
for _, o := range got {
|
||||
switch v := o.(type) {
|
||||
case *OIDC:
|
||||
|
@ -343,6 +343,8 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
|
|||
assert.Equals(t, string(v), "name@smallstep.com")
|
||||
case *x509NamePolicyValidator:
|
||||
assert.Equals(t, nil, v.policyEngine)
|
||||
case *WebhookController:
|
||||
assert.Len(t, 0, v.webhooks)
|
||||
default:
|
||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||
}
|
||||
|
@ -406,8 +408,8 @@ func TestOIDC_AuthorizeRevoke(t *testing.T) {
|
|||
t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
} else if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
}
|
||||
})
|
||||
|
@ -452,8 +454,8 @@ func TestOIDC_AuthorizeRenew(t *testing.T) {
|
|||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("OIDC.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
} else if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
}
|
||||
})
|
||||
|
@ -540,7 +542,7 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
|
|||
pub := key.Public().Key
|
||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
assert.FatalError(t, err)
|
||||
// nolint:gosec // tests minimum size of the key
|
||||
//nolint:gosec // tests minimum size of the key
|
||||
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
|
@ -614,8 +616,8 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
assert.Nil(t, got)
|
||||
} else if assert.NotNil(t, got) {
|
||||
|
@ -682,8 +684,8 @@ func TestOIDC_AuthorizeSSHRevoke(t *testing.T) {
|
|||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("OIDC.AuthorizeSSHRevoke() error = %v, wantErr %v", err, tt.wantErr)
|
||||
} else if err != nil {
|
||||
sc, ok := err.(render.StatusCodedError)
|
||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
||||
var sc render.StatusCodedError
|
||||
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -29,6 +29,9 @@ func (fn certificateOptionsFunc) Options(so SignOptions) []x509util.Option {
|
|||
type Options struct {
|
||||
X509 *X509Options `json:"x509,omitempty"`
|
||||
SSH *SSHOptions `json:"ssh,omitempty"`
|
||||
|
||||
// Webhooks is a list of webhooks that can augment template data
|
||||
Webhooks []*Webhook `json:"webhooks,omitempty"`
|
||||
}
|
||||
|
||||
// GetX509Options returns the X.509 options.
|
||||
|
@ -47,6 +50,14 @@ func (o *Options) GetSSHOptions() *SSHOptions {
|
|||
return o.SSH
|
||||
}
|
||||
|
||||
// GetWebhooks returns the webhooks options.
|
||||
func (o *Options) GetWebhooks() []*Webhook {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.Webhooks
|
||||
}
|
||||
|
||||
// X509Options contains specific options for X.509 certificates.
|
||||
type X509Options struct {
|
||||
// Template contains a X.509 certificate template. It can be a JSON template
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue