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
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
ci:
|
||||||
name: Lint, Test, Build
|
uses: smallstep/certificates/.github/workflows/ci.yml@master
|
||||||
runs-on: ubuntu-20.04
|
secrets: inherit
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go: [ '1.18', '1.19' ]
|
|
||||||
outputs:
|
|
||||||
is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
-
|
|
||||||
name: Setup Go
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go }}
|
|
||||||
-
|
|
||||||
name: Install Deps
|
|
||||||
id: install-deps
|
|
||||||
run: sudo apt-get -y install libpcsclite-dev
|
|
||||||
-
|
|
||||||
name: golangci-lint
|
|
||||||
uses: golangci/golangci-lint-action@v2
|
|
||||||
with:
|
|
||||||
version: ${{ secrets.GOLANGCI_LINT_VERSION }}
|
|
||||||
args: --timeout=30m
|
|
||||||
-
|
|
||||||
name: Test, Build
|
|
||||||
id: lint_test_build
|
|
||||||
run: V=1 make ci
|
|
||||||
|
|
||||||
create_release:
|
create_release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
needs: test
|
needs: ci
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DOCKER_IMAGE: smallstep/step-ca
|
||||||
outputs:
|
outputs:
|
||||||
debversion: ${{ steps.extract-tag.outputs.DEB_VERSION }}
|
version: ${{ steps.extract-tag.outputs.VERSION }}
|
||||||
is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
|
is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
|
||||||
|
docker_tags: ${{ env.DOCKER_TAGS }}
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Is Pre-release
|
||||||
name: Extract Tag Names
|
|
||||||
id: extract-tag
|
|
||||||
run: |
|
|
||||||
DEB_VERSION=$(echo ${GITHUB_REF#refs/tags/v} | sed 's/-/./')
|
|
||||||
echo "::set-output name=DEB_VERSION::${DEB_VERSION}"
|
|
||||||
-
|
|
||||||
name: Is Pre-release
|
|
||||||
id: is_prerelease
|
id: is_prerelease
|
||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
echo ${{ github.ref }} | grep "\-rc.*"
|
echo ${{ github.ref }} | grep "\-rc.*"
|
||||||
OUT=$?
|
OUT=$?
|
||||||
if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi
|
if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi
|
||||||
echo "::set-output name=IS_PRERELEASE::${IS_PRERELEASE}"
|
echo "IS_PRERELEASE=${IS_PRERELEASE}" >> ${GITHUB_OUTPUT}
|
||||||
-
|
- name: Extract Tag Names
|
||||||
name: Create Release
|
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
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
env:
|
env:
|
||||||
|
@ -76,88 +53,48 @@ jobs:
|
||||||
|
|
||||||
goreleaser:
|
goreleaser:
|
||||||
name: Upload Assets To Github w/ goreleaser
|
name: Upload Assets To Github w/ goreleaser
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
needs: create_release
|
needs: create_release
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Checkout
|
||||||
name: Checkout
|
uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- name: Set up Go
|
||||||
with:
|
uses: actions/setup-go@v3
|
||||||
fetch-depth: 0
|
|
||||||
-
|
|
||||||
name: Set up Go
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
with:
|
||||||
go-version: 1.19
|
go-version: 1.19
|
||||||
-
|
check-latest: true
|
||||||
name: APT Install
|
- name: Install cosign
|
||||||
id: aptInstall
|
uses: sigstore/cosign-installer@v2
|
||||||
run: sudo apt-get -y install build-essential debhelper fakeroot
|
|
||||||
-
|
|
||||||
name: Build Debian package
|
|
||||||
id: make_debian
|
|
||||||
run: |
|
|
||||||
PATH=$PATH:/usr/local/go/bin:/home/admin/go/bin
|
|
||||||
make debian
|
|
||||||
# need to restore the git state otherwise goreleaser fails due to dirty state
|
|
||||||
git restore debian/changelog
|
|
||||||
git clean -fd
|
|
||||||
-
|
|
||||||
name: Install cosign
|
|
||||||
uses: sigstore/cosign-installer@v1.1.0
|
|
||||||
with:
|
with:
|
||||||
cosign-release: 'v1.1.0'
|
cosign-release: 'v1.13.1'
|
||||||
-
|
- name: Get Release Date
|
||||||
name: Write cosign key to disk
|
|
||||||
id: write_key
|
|
||||||
run: echo "${{ secrets.COSIGN_KEY }}" > "/tmp/cosign.key"
|
|
||||||
-
|
|
||||||
name: Get Release Date
|
|
||||||
id: release_date
|
id: release_date
|
||||||
run: |
|
run: |
|
||||||
RELEASE_DATE=$(date +"%y-%m-%d")
|
RELEASE_DATE=$(date +"%y-%m-%d")
|
||||||
echo "::set-output name=RELEASE_DATE::${RELEASE_DATE}"
|
echo "RELEASE_DATE=${RELEASE_DATE}" >> ${GITHUB_ENV}
|
||||||
-
|
- name: Run GoReleaser
|
||||||
name: Run GoReleaser
|
uses: goreleaser/goreleaser-action@v3
|
||||||
uses: goreleaser/goreleaser-action@5a54d7e660bda43b405e8463261b3d25631ffe86 # v2.7.0
|
|
||||||
with:
|
with:
|
||||||
version: 'v1.7.0'
|
version: 'latest'
|
||||||
args: release --rm-dist
|
args: release --rm-dist
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
GITHUB_TOKEN: ${{ secrets.GORELEASER_PAT }}
|
||||||
COSIGN_PWD: ${{ secrets.COSIGN_PWD }}
|
RELEASE_DATE: ${{ env.RELEASE_DATE }}
|
||||||
DEB_VERSION: ${{ needs.create_release.outputs.debversion }}
|
COSIGN_EXPERIMENTAL: 1
|
||||||
RELEASE_DATE: ${{ steps.release_date.outputs.RELEASE_DATE }}
|
|
||||||
|
|
||||||
build_upload_docker:
|
build_upload_docker:
|
||||||
name: Build & Upload Docker Images
|
name: Build & Upload Docker Images
|
||||||
runs-on: ubuntu-20.04
|
needs: create_release
|
||||||
needs: test
|
permissions:
|
||||||
steps:
|
id-token: write
|
||||||
-
|
contents: write
|
||||||
name: Checkout
|
uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main
|
||||||
uses: actions/checkout@v2
|
with:
|
||||||
-
|
platforms: linux/amd64,linux/386,linux/arm,linux/arm64
|
||||||
name: Setup Go
|
tags: ${{ needs.create_release.outputs.docker_tags }}
|
||||||
uses: actions/setup-go@v2
|
docker_image: smallstep/step-ca
|
||||||
with:
|
docker_file: docker/Dockerfile.step-ca
|
||||||
go-version: '1.19'
|
secrets: inherit
|
||||||
-
|
|
||||||
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 }}
|
|
||||||
|
|
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:
|
issues:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
|
- reopened
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
|
- reopened
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
triage:
|
||||||
label:
|
uses: smallstep/workflows/.github/workflows/triage.yml@main
|
||||||
name: Label PR
|
secrets: inherit
|
||||||
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 }}
|
|
||||||
|
|
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:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
main: ./cmd/step-ca/main.go
|
main: ./cmd/step-ca/main.go
|
||||||
binary: bin/step-ca
|
binary: step-ca
|
||||||
ldflags:
|
|
||||||
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
|
|
||||||
-
|
|
||||||
id: step-cloudkms-init
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
targets:
|
|
||||||
- darwin_amd64
|
|
||||||
- darwin_arm64
|
|
||||||
- freebsd_amd64
|
|
||||||
- linux_386
|
|
||||||
- linux_amd64
|
|
||||||
- linux_arm64
|
|
||||||
- linux_arm_5
|
|
||||||
- linux_arm_6
|
|
||||||
- linux_arm_7
|
|
||||||
- windows_amd64
|
|
||||||
flags:
|
|
||||||
- -trimpath
|
|
||||||
main: ./cmd/step-cloudkms-init/main.go
|
|
||||||
binary: bin/step-cloudkms-init
|
|
||||||
ldflags:
|
|
||||||
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
|
|
||||||
-
|
|
||||||
id: step-awskms-init
|
|
||||||
env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
targets:
|
|
||||||
- darwin_amd64
|
|
||||||
- darwin_arm64
|
|
||||||
- freebsd_amd64
|
|
||||||
- linux_386
|
|
||||||
- linux_amd64
|
|
||||||
- linux_arm64
|
|
||||||
- linux_arm_5
|
|
||||||
- linux_arm_6
|
|
||||||
- linux_arm_7
|
|
||||||
- windows_amd64
|
|
||||||
flags:
|
|
||||||
- -trimpath
|
|
||||||
main: ./cmd/step-awskms-init/main.go
|
|
||||||
binary: bin/step-awskms-init
|
|
||||||
ldflags:
|
ldflags:
|
||||||
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
|
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
|
||||||
|
|
||||||
|
@ -85,6 +43,38 @@ archives:
|
||||||
files:
|
files:
|
||||||
- README.md
|
- README.md
|
||||||
- LICENSE
|
- 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:
|
source:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
@ -97,8 +87,9 @@ checksum:
|
||||||
|
|
||||||
signs:
|
signs:
|
||||||
- cmd: cosign
|
- cmd: cosign
|
||||||
stdin: '{{ .Env.COSIGN_PWD }}'
|
signature: "${artifact}.sig"
|
||||||
args: ["sign-blob", "-key=/tmp/cosign.key", "-output=${signature}", "${artifact}"]
|
certificate: "${artifact}.pem"
|
||||||
|
args: ["sign-blob", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"]
|
||||||
artifacts: all
|
artifacts: all
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
|
@ -140,7 +131,7 @@ release:
|
||||||
#### Linux
|
#### 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_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
|
#### OSX Darwin
|
||||||
|
|
||||||
|
@ -163,9 +154,9 @@ release:
|
||||||
Below is an example using `cosign` to verify a release artifact:
|
Below is an example using `cosign` to verify a release artifact:
|
||||||
|
|
||||||
```
|
```
|
||||||
cosign verify-blob \
|
COSIGN_EXPERIMENTAL=1 cosign verify-blob \
|
||||||
-key https://raw.githubusercontent.com/smallstep/certificates/master/cosign.pub \
|
--certificate ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig.pem \
|
||||||
-signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig
|
--signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig \
|
||||||
~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz
|
~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -194,39 +185,3 @@ release:
|
||||||
# - glob: ./path/to/file.txt
|
# - glob: ./path/to/file.txt
|
||||||
# - glob: ./glob/**/to/**/file/**/*
|
# - glob: ./glob/**/to/**/file/**/*
|
||||||
# - glob: ./glob/foo/to/bar/file/foobar/override_from_previous
|
# - glob: ./glob/foo/to/bar/file/foobar/override_from_previous
|
||||||
|
|
||||||
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
|
read -r firstline < .VERSION
|
||||||
last_half="${firstline##*tag: }"
|
last_half="${firstline##*tag: }"
|
||||||
if [[ ${last_half::1} == "v" ]]; then
|
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]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- Added support for ACME device-attest-01 challenge.
|
- 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
|
## [0.22.1] - 2022-08-31
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
33
Makefile
33
Makefile
|
@ -28,8 +28,9 @@ ci: testcgo build
|
||||||
#########################################
|
#########################################
|
||||||
|
|
||||||
bootstra%:
|
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 $$(go env GOPATH)/bin latest
|
||||||
$Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.42.0
|
$Q go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
$Q go install gotest.tools/gotestsum@latest
|
||||||
|
|
||||||
.PHONY: bootstra%
|
.PHONY: bootstra%
|
||||||
|
|
||||||
|
@ -78,8 +79,6 @@ $(info DEB_VERSION is $(DEB_VERSION))
|
||||||
$(info PUSHTYPE is $(PUSHTYPE))
|
$(info PUSHTYPE is $(PUSHTYPE))
|
||||||
endif
|
endif
|
||||||
|
|
||||||
include make/docker.mk
|
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
# Build
|
# Build
|
||||||
#########################################
|
#########################################
|
||||||
|
@ -132,17 +131,18 @@ generate:
|
||||||
# Test
|
# Test
|
||||||
#########################################
|
#########################################
|
||||||
test:
|
test:
|
||||||
$Q $(GOFLAGS) go test -short -coverprofile=coverage.out ./...
|
$Q $(GOFLAGS) gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...
|
||||||
|
|
||||||
|
|
||||||
testcgo:
|
testcgo:
|
||||||
$Q go test -short -coverprofile=coverage.out ./...
|
$Q gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...
|
||||||
|
|
||||||
.PHONY: test testcgo
|
.PHONY: test testcgo
|
||||||
|
|
||||||
integrate: integration
|
integrate: integration
|
||||||
|
|
||||||
integration: bin/$(BINNAME)
|
integration: bin/$(BINNAME)
|
||||||
$Q $(GOFLAGS) go test -tags=integration ./integration/...
|
$Q $(GOFLAGS) gotestsum -- -tags=integration ./integration/...
|
||||||
|
|
||||||
.PHONY: integrate integration
|
.PHONY: integrate integration
|
||||||
|
|
||||||
|
@ -151,15 +151,14 @@ integration: bin/$(BINNAME)
|
||||||
#########################################
|
#########################################
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
$Q gofmt -l -s -w $(SRC)
|
$Q goimports -l -w $(SRC)
|
||||||
|
|
||||||
|
lint: SHELL:=/bin/bash
|
||||||
lint:
|
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:
|
.PHONY: fmt lint
|
||||||
$Q LOG_LEVEL=error golangci-lint run --timeout=30m
|
|
||||||
|
|
||||||
.PHONY: fmt lint lintcgo
|
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
# Install
|
# Install
|
||||||
|
@ -231,11 +230,3 @@ debian: changelog
|
||||||
distclean: clean
|
distclean: clean
|
||||||
|
|
||||||
.PHONY: changelog debian distclean
|
.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.
|
// IsValid returns true if the Account is valid.
|
||||||
func (a *Account) IsValid() bool {
|
func (a *Account) IsValid() bool {
|
||||||
return Status(a.Status) == StatusValid
|
return a.Status == StatusValid
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyToID converts a JWK to a thumbprint.
|
// KeyToID converts a JWK to a thumbprint.
|
||||||
|
|
|
@ -46,14 +46,14 @@ func TestKeyToID(t *testing.T) {
|
||||||
tc := run(t)
|
tc := run(t)
|
||||||
if id, err := KeyToID(tc.jwk); err != nil {
|
if id, err := KeyToID(tc.jwk); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
assert.FatalError(t, errors.New("unexpected error type"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,12 +131,13 @@ func TestExternalAccountKey_BindTo(t *testing.T) {
|
||||||
}
|
}
|
||||||
if wantErr {
|
if wantErr {
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
assert.Type(t, &Error{}, err)
|
var ae *Error
|
||||||
ae, _ := err.(*Error)
|
if assert.True(t, errors.As(err, &ae)) {
|
||||||
assert.Equals(t, ae.Type, tt.err.Type)
|
assert.Equals(t, ae.Type, tt.err.Type)
|
||||||
assert.Equals(t, ae.Detail, tt.err.Detail)
|
assert.Equals(t, ae.Detail, tt.err.Detail)
|
||||||
assert.Equals(t, ae.Identifier, tt.err.Identifier)
|
assert.Equals(t, ae.Identifier, tt.err.Identifier)
|
||||||
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
assert.Equals(t, eak.AccountID, acct.ID)
|
assert.Equals(t, eak.AccountID, acct.ID)
|
||||||
assert.Equals(t, eak.HmacKey, []byte{})
|
assert.Equals(t, eak.HmacKey, []byte{})
|
||||||
|
|
|
@ -2,6 +2,7 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
@ -97,8 +98,8 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
|
||||||
httpStatus := http.StatusCreated
|
httpStatus := http.StatusCreated
|
||||||
acc, err := accountFromContext(ctx)
|
acc, err := accountFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
acmeErr, ok := err.(*acme.Error)
|
var acmeErr *acme.Error
|
||||||
if !ok || acmeErr.Status != http.StatusBadRequest {
|
if !errors.As(err, &acmeErr) || acmeErr.Status != http.StatusBadRequest {
|
||||||
// Something went wrong ...
|
// Something went wrong ...
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -197,11 +197,12 @@ func TestNewAccountRequest_Validate(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
if err := tc.nar.Validate(); err != nil {
|
if err := tc.nar.Validate(); err != nil {
|
||||||
if assert.NotNil(t, err) {
|
if assert.NotNil(t, err) {
|
||||||
ae, ok := err.(*acme.Error)
|
var ae *acme.Error
|
||||||
assert.True(t, ok)
|
if assert.True(t, errors.As(err, &ae)) {
|
||||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||||
assert.Equals(t, ae.Type, tc.err.Type)
|
assert.Equals(t, ae.Type, tc.err.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, tc.err)
|
assert.Nil(t, tc.err)
|
||||||
|
@ -268,11 +269,12 @@ func TestUpdateAccountRequest_Validate(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
if err := tc.uar.Validate(); err != nil {
|
if err := tc.uar.Validate(); err != nil {
|
||||||
if assert.NotNil(t, err) {
|
if assert.NotNil(t, err) {
|
||||||
ae, ok := err.(*acme.Error)
|
var ae *acme.Error
|
||||||
assert.True(t, ok)
|
if assert.True(t, errors.As(err, &ae)) {
|
||||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||||
assert.Equals(t, ae.Type, tc.err.Type)
|
assert.Equals(t, ae.Type, tc.err.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, tc.err)
|
assert.Nil(t, tc.err)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package api
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
|
|
||||||
|
@ -24,6 +25,7 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !acmeProv.RequireEAB {
|
if !acmeProv.RequireEAB {
|
||||||
|
//nolint:nilnil // legacy
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +53,8 @@ func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest)
|
||||||
db := acme.MustDatabaseFromContext(ctx)
|
db := acme.MustDatabaseFromContext(ctx)
|
||||||
externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)
|
externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key")
|
||||||
}
|
}
|
||||||
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
|
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
|
||||||
|
|
|
@ -860,13 +860,15 @@ func TestHandler_validateExternalAccountBinding(t *testing.T) {
|
||||||
if wantErr {
|
if wantErr {
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
assert.Type(t, &acme.Error{}, err)
|
assert.Type(t, &acme.Error{}, err)
|
||||||
ae, _ := err.(*acme.Error)
|
var ae *acme.Error
|
||||||
assert.Equals(t, ae.Type, tc.err.Type)
|
if assert.True(t, errors.As(err, &ae)) {
|
||||||
assert.Equals(t, ae.Status, tc.err.Status)
|
assert.Equals(t, ae.Type, tc.err.Type)
|
||||||
assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, ae.Status, tc.err.Status)
|
||||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
assert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
assert.Equals(t, ae.Identifier, tc.err.Identifier)
|
||||||
|
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if got == nil {
|
if got == nil {
|
||||||
assert.Nil(t, tc.eak)
|
assert.Nil(t, tc.eak)
|
||||||
|
|
|
@ -205,7 +205,7 @@ type Directory struct {
|
||||||
NewOrder string `json:"newOrder"`
|
NewOrder string `json:"newOrder"`
|
||||||
RevokeCert string `json:"revokeCert"`
|
RevokeCert string `json:"revokeCert"`
|
||||||
KeyChange string `json:"keyChange"`
|
KeyChange string `json:"keyChange"`
|
||||||
Meta Meta `json:"meta"`
|
Meta *Meta `json:"meta,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToLog enables response logging for the Directory type.
|
// ToLog enables response logging for the Directory type.
|
||||||
|
@ -228,18 +228,49 @@ func GetDirectory(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
linker := acme.MustLinkerFromContext(ctx)
|
linker := acme.MustLinkerFromContext(ctx)
|
||||||
|
|
||||||
render.JSON(w, &Directory{
|
render.JSON(w, &Directory{
|
||||||
NewNonce: linker.GetLink(ctx, acme.NewNonceLinkType),
|
NewNonce: linker.GetLink(ctx, acme.NewNonceLinkType),
|
||||||
NewAccount: linker.GetLink(ctx, acme.NewAccountLinkType),
|
NewAccount: linker.GetLink(ctx, acme.NewAccountLinkType),
|
||||||
NewOrder: linker.GetLink(ctx, acme.NewOrderLinkType),
|
NewOrder: linker.GetLink(ctx, acme.NewOrderLinkType),
|
||||||
RevokeCert: linker.GetLink(ctx, acme.RevokeCertLinkType),
|
RevokeCert: linker.GetLink(ctx, acme.RevokeCertLinkType),
|
||||||
KeyChange: linker.GetLink(ctx, acme.KeyChangeLinkType),
|
KeyChange: linker.GetLink(ctx, acme.KeyChangeLinkType),
|
||||||
Meta: Meta{
|
Meta: createMetaObject(acmeProv),
|
||||||
ExternalAccountRequired: acmeProv.RequireEAB,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createMetaObject creates a Meta object if the ACME provisioner
|
||||||
|
// has one or more properties that are written in the ACME directory output.
|
||||||
|
// It returns nil if none of the properties are set.
|
||||||
|
func createMetaObject(p *provisioner.ACME) *Meta {
|
||||||
|
if shouldAddMetaObject(p) {
|
||||||
|
return &Meta{
|
||||||
|
TermsOfService: p.TermsOfService,
|
||||||
|
Website: p.Website,
|
||||||
|
CaaIdentities: p.CaaIdentities,
|
||||||
|
ExternalAccountRequired: p.RequireEAB,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldAddMetaObject returns whether or not the ACME provisioner
|
||||||
|
// has properties configured that must be added to the ACME directory object.
|
||||||
|
func shouldAddMetaObject(p *provisioner.ACME) bool {
|
||||||
|
switch {
|
||||||
|
case p.TermsOfService != "":
|
||||||
|
return true
|
||||||
|
case p.Website != "":
|
||||||
|
return true
|
||||||
|
case len(p.CaaIdentities) > 0:
|
||||||
|
return true
|
||||||
|
case p.RequireEAB:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NotImplemented returns a 501 and is generally a placeholder for functionality which
|
// NotImplemented returns a 501 and is generally a placeholder for functionality which
|
||||||
// MAY be added at some point in the future but is not in any way a guarantee of such.
|
// MAY be added at some point in the future but is not in any way a guarantee of such.
|
||||||
func NotImplemented(w http.ResponseWriter, r *http.Request) {
|
func NotImplemented(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -18,10 +18,13 @@ import (
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
|
||||||
"github.com/smallstep/certificates/acme"
|
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/pemutil"
|
"go.step.sm/crypto/pemutil"
|
||||||
|
|
||||||
|
"github.com/smallstep/assert"
|
||||||
|
"github.com/smallstep/certificates/acme"
|
||||||
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockClient struct {
|
type mockClient struct {
|
||||||
|
@ -129,7 +132,35 @@ func TestHandler_GetDirectory(t *testing.T) {
|
||||||
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
|
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
|
||||||
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
|
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
|
||||||
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
|
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
|
||||||
Meta: Meta{
|
Meta: &Meta{
|
||||||
|
ExternalAccountRequired: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return test{
|
||||||
|
ctx: ctx,
|
||||||
|
dir: expDir,
|
||||||
|
statusCode: 200,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/full-meta": func(t *testing.T) test {
|
||||||
|
prov := newACMEProv(t)
|
||||||
|
prov.TermsOfService = "https://terms.ca.local/"
|
||||||
|
prov.Website = "https://ca.local/"
|
||||||
|
prov.CaaIdentities = []string{"ca.local"}
|
||||||
|
prov.RequireEAB = true
|
||||||
|
provName := url.PathEscape(prov.GetName())
|
||||||
|
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||||
|
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||||
|
expDir := Directory{
|
||||||
|
NewNonce: fmt.Sprintf("%s/acme/%s/new-nonce", baseURL.String(), provName),
|
||||||
|
NewAccount: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName),
|
||||||
|
NewOrder: fmt.Sprintf("%s/acme/%s/new-order", baseURL.String(), provName),
|
||||||
|
RevokeCert: fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL.String(), provName),
|
||||||
|
KeyChange: fmt.Sprintf("%s/acme/%s/key-change", baseURL.String(), provName),
|
||||||
|
Meta: &Meta{
|
||||||
|
TermsOfService: "https://terms.ca.local/",
|
||||||
|
Website: "https://ca.local/",
|
||||||
|
CaaIdentities: []string{"ca.local"},
|
||||||
ExternalAccountRequired: true,
|
ExternalAccountRequired: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
"ok/empty-algorithm-in-jwk": func(t *testing.T) test {
|
||||||
_pub := *pub
|
|
||||||
clone := &_pub
|
|
||||||
clone.Algorithm = ""
|
|
||||||
ctx := context.WithValue(context.Background(), jwsContextKey, parsedJWS)
|
ctx := context.WithValue(context.Background(), jwsContextKey, parsedJWS)
|
||||||
ctx = context.WithValue(ctx, jwkContextKey, pub)
|
ctx = context.WithValue(ctx, jwkContextKey, pub)
|
||||||
return test{
|
return test{
|
||||||
|
|
|
@ -179,11 +179,12 @@ func TestNewOrderRequest_Validate(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
if err := tc.nor.Validate(); err != nil {
|
if err := tc.nor.Validate(); err != nil {
|
||||||
if assert.NotNil(t, err) {
|
if assert.NotNil(t, err) {
|
||||||
ae, ok := err.(*acme.Error)
|
var ae *acme.Error
|
||||||
assert.True(t, ok)
|
if assert.True(t, errors.As(err, &ae)) {
|
||||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||||
assert.Equals(t, ae.Type, tc.err.Type)
|
assert.Equals(t, ae.Type, tc.err.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
|
@ -253,11 +254,12 @@ func TestFinalizeRequestValidate(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
if err := tc.fr.Validate(); err != nil {
|
if err := tc.fr.Validate(); err != nil {
|
||||||
if assert.NotNil(t, err) {
|
if assert.NotNil(t, err) {
|
||||||
ae, ok := err.(*acme.Error)
|
var ae *acme.Error
|
||||||
assert.True(t, ok)
|
if assert.True(t, errors.As(err, &ae)) {
|
||||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||||
assert.Equals(t, ae.Type, tc.err.Type)
|
assert.Equals(t, ae.Type, tc.err.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
|
@ -756,19 +758,22 @@ func TestHandler_newAuthorization(t *testing.T) {
|
||||||
}
|
}
|
||||||
for name, run := range tests {
|
for name, run := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
if name == "ok/permanent-identifier-enabled" {
|
||||||
|
println(1)
|
||||||
|
}
|
||||||
tc := run(t)
|
tc := run(t)
|
||||||
ctx := newBaseContext(context.Background(), tc.db)
|
ctx := newBaseContext(context.Background(), tc.db)
|
||||||
ctx = acme.NewProvisionerContext(ctx, tc.prov)
|
ctx = acme.NewProvisionerContext(ctx, tc.prov)
|
||||||
if err := newAuthorization(ctx, tc.az); err != nil {
|
if err := newAuthorization(ctx, tc.az); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *acme.Error
|
||||||
case *acme.Error:
|
if assert.True(t, errors.As(err, &k)) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
assert.FatalError(t, errors.New("unexpected error type"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,14 +130,14 @@ func TestAuthorization_UpdateStatus(t *testing.T) {
|
||||||
tc := run(t)
|
tc := run(t)
|
||||||
if err := tc.az.UpdateStatus(context.Background(), tc.db); err != nil {
|
if err := tc.az.UpdateStatus(context.Background(), tc.db); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
assert.FatalError(t, errors.New("unexpected error type"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,18 @@ const (
|
||||||
DEVICEATTEST01 ChallengeType = "device-attest-01"
|
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.
|
// Challenge represents an ACME response Challenge type.
|
||||||
type Challenge struct {
|
type Challenge struct {
|
||||||
ID string `json:"-"`
|
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 {
|
func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error {
|
||||||
u := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)}
|
u := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)}
|
||||||
|
|
||||||
|
// Append insecure port if set.
|
||||||
|
// Only used for testing purposes.
|
||||||
|
if InsecurePortHTTP01 != 0 {
|
||||||
|
u.Host += ":" + strconv.Itoa(InsecurePortHTTP01)
|
||||||
|
}
|
||||||
|
|
||||||
vc := MustClientFromContext(ctx)
|
vc := MustClientFromContext(ctx)
|
||||||
resp, err := vc.Get(u.String())
|
resp, err := vc.Get(u.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -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.
|
// [RFC5246] or higher when connecting to clients for validation.
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
ServerName: serverName(ch),
|
ServerName: serverName(ch),
|
||||||
InsecureSkipVerify: true, // nolint:gosec // we expect a self-signed challenge certificate
|
InsecureSkipVerify: true, //nolint:gosec // we expect a self-signed challenge certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
vc := MustClientFromContext(ctx)
|
||||||
conn, err := vc.TLSDial("tcp", hostPort, config)
|
conn, err := vc.TLSDial("tcp", hostPort, config)
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -188,14 +189,14 @@ func Test_storeError(t *testing.T) {
|
||||||
tc := run(t)
|
tc := run(t)
|
||||||
if err := storeError(context.Background(), tc.db, tc.ch, tc.markInvalid, err); err != nil {
|
if err := storeError(context.Background(), tc.db, tc.ch, tc.markInvalid, err); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
assert.FatalError(t, errors.New("unexpected error type"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,14 +244,14 @@ func TestKeyAuthorization(t *testing.T) {
|
||||||
tc := run(t)
|
tc := run(t)
|
||||||
if ka, err := KeyAuthorization(tc.token, tc.jwk); err != nil {
|
if ka, err := KeyAuthorization(tc.token, tc.jwk); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
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 {
|
"fail/dns-01": func(t *testing.T) test {
|
||||||
ch := &Challenge{
|
ch := &Challenge{
|
||||||
ID: "chID",
|
ID: "chID",
|
||||||
|
@ -501,6 +543,72 @@ func TestChallenge_Validate(t *testing.T) {
|
||||||
srv, tlsDial := newTestTLSALPNServer(cert)
|
srv, tlsDial := newTestTLSALPNServer(cert)
|
||||||
srv.Start()
|
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{
|
return test{
|
||||||
ch: ch,
|
ch: ch,
|
||||||
vc: &mockClient{
|
vc: &mockClient{
|
||||||
|
@ -533,14 +641,14 @@ func TestChallenge_Validate(t *testing.T) {
|
||||||
ctx := NewClientContext(context.Background(), tc.vc)
|
ctx := NewClientContext(context.Background(), tc.vc)
|
||||||
if err := tc.ch.Validate(ctx, tc.db, tc.jwk, nil); err != nil {
|
if err := tc.ch.Validate(ctx, tc.db, tc.jwk, nil); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
assert.FatalError(t, errors.New("unexpected error type"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -928,14 +1036,14 @@ func TestHTTP01Validate(t *testing.T) {
|
||||||
ctx := NewClientContext(context.Background(), tc.vc)
|
ctx := NewClientContext(context.Background(), tc.vc)
|
||||||
if err := http01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
|
if err := http01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
assert.FatalError(t, errors.New("unexpected error type"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1228,14 +1336,14 @@ func TestDNS01Validate(t *testing.T) {
|
||||||
ctx := NewClientContext(context.Background(), tc.vc)
|
ctx := NewClientContext(context.Background(), tc.vc)
|
||||||
if err := dns01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
|
if err := dns01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
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)
|
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 := httptest.NewUnstartedServer(http.NewServeMux())
|
||||||
|
|
||||||
srv.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){
|
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.Listener = tls.NewListener(srv.Listener, srv.TLS)
|
||||||
//srv.Config.ErrorLog = log.New(ioutil.Discard, "", 0) // hush
|
//srv.Config.ErrorLog = log.New(ioutil.Discard, "", 0) // hush
|
||||||
|
|
||||||
|
@ -2298,14 +2411,14 @@ func TestTLSALPN01Validate(t *testing.T) {
|
||||||
ctx := NewClientContext(context.Background(), tc.vc)
|
ctx := NewClientContext(context.Background(), tc.vc)
|
||||||
if err := tlsalpn01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
|
if err := tlsalpn01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
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,
|
Timeout: 30 * time.Second,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
// nolint:gosec // used on tls-alpn-01 challenge
|
//nolint:gosec // used on tls-alpn-01 challenge
|
||||||
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
|
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -95,16 +95,16 @@ func TestDB_getDBAccount(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if dbacc, err := d.getDBAccount(context.Background(), accID); err != nil {
|
if dbacc, err := d.getDBAccount(context.Background(), accID); err != nil {
|
||||||
switch k := err.(type) {
|
var acmeErr *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &acmeErr) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -174,16 +174,16 @@ func TestDB_getAccountIDByKeyID(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if retAccID, err := d.getAccountIDByKeyID(context.Background(), kid); err != nil {
|
if retAccID, err := d.getAccountIDByKeyID(context.Background(), kid); err != nil {
|
||||||
switch k := err.(type) {
|
var acmeErr *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &acmeErr) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -248,16 +248,16 @@ func TestDB_GetAccount(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if acc, err := d.GetAccount(context.Background(), accID); err != nil {
|
if acc, err := d.GetAccount(context.Background(), accID); err != nil {
|
||||||
switch k := err.(type) {
|
var acmeErr *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &acmeErr) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -354,16 +354,16 @@ func TestDB_GetAccountByKeyID(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if acc, err := d.GetAccountByKeyID(context.Background(), kid); err != nil {
|
if acc, err := d.GetAccountByKeyID(context.Background(), kid); err != nil {
|
||||||
switch k := err.(type) {
|
var acmeErr *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &acmeErr) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,16 +101,16 @@ func TestDB_getDBAuthz(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if dbaz, err := d.getDBAuthz(context.Background(), azID); err != nil {
|
if dbaz, err := d.getDBAuthz(context.Background(), azID); err != nil {
|
||||||
switch k := err.(type) {
|
var acmeErr *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &acmeErr) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -295,16 +295,16 @@ func TestDB_GetAuthorization(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if az, err := d.GetAuthorization(context.Background(), azID); err != nil {
|
if az, err := d.GetAuthorization(context.Background(), azID); err != nil {
|
||||||
switch k := err.(type) {
|
var acmeErr *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &acmeErr) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -745,16 +745,16 @@ func TestDB_GetAuthorizationsByAccountID(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if azs, err := d.GetAuthorizationsByAccountID(context.Background(), accountID); err != nil {
|
if azs, err := d.GetAuthorizationsByAccountID(context.Background(), accountID); err != nil {
|
||||||
switch k := err.(type) {
|
var acmeErr *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &acmeErr) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,5 +138,4 @@ func parseBundle(b []byte) ([]*x509.Certificate, error) {
|
||||||
return nil, errors.New("error decoding PEM: unexpected data")
|
return nil, errors.New("error decoding PEM: unexpected data")
|
||||||
}
|
}
|
||||||
return bundle, nil
|
return bundle, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -250,16 +250,16 @@ func TestDB_GetCertificate(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
cert, err := d.GetCertificate(context.Background(), certID)
|
cert, err := d.GetCertificate(context.Background(), certID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch k := err.(type) {
|
var acmeErr *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &acmeErr) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -444,16 +444,16 @@ func TestDB_GetCertificateBySerial(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
cert, err := d.GetCertificateBySerial(context.Background(), serial)
|
cert, err := d.GetCertificateBySerial(context.Background(), serial)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,16 +94,16 @@ func TestDB_getDBChallenge(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if ch, err := d.getDBChallenge(context.Background(), chID); err != nil {
|
if ch, err := d.getDBChallenge(context.Background(), chID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -286,16 +286,16 @@ func TestDB_GetChallenge(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if ch, err := d.GetChallenge(context.Background(), chID, azID); err != nil {
|
if ch, err := d.GetChallenge(context.Background(), chID, azID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// CreateExternalAccountKey creates a new External Account Binding key with a name
|
||||||
func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||||
|
|
||||||
externalAccountKeyMutex.Lock()
|
externalAccountKeyMutex.Lock()
|
||||||
defer externalAccountKeyMutex.Unlock()
|
defer externalAccountKeyMutex.Unlock()
|
||||||
|
|
||||||
|
@ -210,6 +209,7 @@ func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerI
|
||||||
defer externalAccountKeyMutex.RUnlock()
|
defer externalAccountKeyMutex.RUnlock()
|
||||||
|
|
||||||
if reference == "" {
|
if reference == "" {
|
||||||
|
//nolint:nilnil // legacy
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
func (db *DB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {
|
||||||
|
//nolint:nilnil // legacy
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,7 +372,6 @@ func sliceIndex(slice []string, item string) int {
|
||||||
// removeElement deletes the item if it exists in the
|
// removeElement deletes the item if it exists in the
|
||||||
// slice. It returns a new slice, keeping the old one intact.
|
// slice. It returns a new slice, keeping the old one intact.
|
||||||
func removeElement(slice []string, item string) []string {
|
func removeElement(slice []string, item string) []string {
|
||||||
|
|
||||||
newSlice := make([]string, 0)
|
newSlice := make([]string, 0)
|
||||||
index := sliceIndex(slice, item)
|
index := sliceIndex(slice, item)
|
||||||
if index < 0 {
|
if index < 0 {
|
||||||
|
|
|
@ -93,16 +93,16 @@ func TestDB_getDBExternalAccountKey(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil {
|
if dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -210,16 +210,16 @@ func TestDB_GetExternalAccountKey(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil {
|
if eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -374,16 +374,16 @@ func TestDB_GetExternalAccountKeyByReference(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil {
|
if eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -580,16 +580,16 @@ func TestDB_GetExternalAccountKeys(t *testing.T) {
|
||||||
cursor, limit := "", 0
|
cursor, limit := "", 0
|
||||||
if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil {
|
if eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil {
|
||||||
assert.Equals(t, "", nextCursor)
|
assert.Equals(t, "", nextCursor)
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.Equals(t, tc.err.Error(), err.Error())
|
assert.Equals(t, tc.err.Error(), err.Error())
|
||||||
}
|
}
|
||||||
|
@ -672,7 +672,7 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
||||||
return errors.New("force default")
|
return errors.New("force default")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MCmpAndSwap: func(bucket, key, old, new []byte) ([]byte, bool, error) {
|
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||||
fmt.Println(string(bucket))
|
fmt.Println(string(bucket))
|
||||||
switch string(bucket) {
|
switch string(bucket) {
|
||||||
case string(externalAccountKeyIDsByReferenceTable):
|
case string(externalAccountKeyIDsByReferenceTable):
|
||||||
|
@ -882,16 +882,16 @@ func TestDB_DeleteExternalAccountKey(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil {
|
if err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.Equals(t, err.Error(), tc.err.Error())
|
assert.Equals(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,16 +146,16 @@ func TestDB_DeleteNonce(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if err := d.DeleteNonce(context.Background(), acme.Nonce(nonceID)); err != nil {
|
if err := d.DeleteNonce(context.Background(), acme.Nonce(nonceID)); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,16 +102,16 @@ func TestDB_getDBOrder(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if dbo, err := d.getDBOrder(context.Background(), orderID); err != nil {
|
if dbo, err := d.getDBOrder(context.Background(), orderID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -206,16 +206,16 @@ func TestDB_GetOrder(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if o, err := d.GetOrder(context.Background(), orderID); err != nil {
|
if o, err := d.GetOrder(context.Background(), orderID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -1003,16 +1003,16 @@ func TestDB_updateAddOrderIDs(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch k := err.(type) {
|
var ae *acme.Error
|
||||||
case *acme.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.acmeErr) {
|
if assert.NotNil(t, tc.acmeErr) {
|
||||||
assert.Equals(t, k.Type, tc.acmeErr.Type)
|
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.acmeErr.Status)
|
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.acmeErr.Detail)
|
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,8 @@ func (ap ProblemType) String() string {
|
||||||
return "accountDoesNotExist"
|
return "accountDoesNotExist"
|
||||||
case ErrorAlreadyRevokedType:
|
case ErrorAlreadyRevokedType:
|
||||||
return "alreadyRevoked"
|
return "alreadyRevoked"
|
||||||
|
case ErrorBadAttestationStatementType:
|
||||||
|
return "badAttestationStatement"
|
||||||
case ErrorBadCSRType:
|
case ErrorBadCSRType:
|
||||||
return "badCSR"
|
return "badCSR"
|
||||||
case ErrorBadNonceType:
|
case ErrorBadNonceType:
|
||||||
|
@ -310,10 +312,11 @@ func NewErrorISE(msg string, args ...interface{}) *Error {
|
||||||
|
|
||||||
// WrapError attempts to wrap the internal error.
|
// WrapError attempts to wrap the internal error.
|
||||||
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
||||||
switch e := err.(type) {
|
var e *Error
|
||||||
case nil:
|
switch {
|
||||||
|
case err == nil:
|
||||||
return nil
|
return nil
|
||||||
case *Error:
|
case errors.As(err, &e):
|
||||||
if e.Err == nil {
|
if e.Err == nil {
|
||||||
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -194,6 +194,14 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner")
|
return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner")
|
||||||
}
|
}
|
||||||
|
// Unlike most of the provisioners, ACME's AuthorizeSign method doesn't
|
||||||
|
// define the templates, and the template data used in WebHooks is not
|
||||||
|
// available.
|
||||||
|
for _, signOp := range signOps {
|
||||||
|
if wc, ok := signOp.(*provisioner.WebhookController); ok {
|
||||||
|
wc.TemplateData = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
templateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate)
|
templateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -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
|
// addresses or DNS names slice, depending on whether it can be parsed as an IP
|
||||||
// or not. This might result in an additional SAN in the final certificate.
|
// or not. This might result in an additional SAN in the final certificate.
|
||||||
func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) {
|
func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) {
|
||||||
|
|
||||||
// for clarity only; we're operating on the same object by pointer
|
// for clarity only; we're operating on the same object by pointer
|
||||||
canonicalized = csr
|
canonicalized = csr
|
||||||
|
|
||||||
|
|
|
@ -247,14 +247,14 @@ func TestOrder_UpdateStatus(t *testing.T) {
|
||||||
tc := run(t)
|
tc := run(t)
|
||||||
if err := tc.o.UpdateStatus(context.Background(), tc.db); err != nil {
|
if err := tc.o.UpdateStatus(context.Background(), tc.db); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
assert.FatalError(t, errors.New("unexpected error type"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -812,14 +812,14 @@ func TestOrder_Finalize(t *testing.T) {
|
||||||
tc := run(t)
|
tc := run(t)
|
||||||
if err := tc.o.Finalize(context.Background(), tc.db, tc.csr, tc.ca, tc.prov); err != nil {
|
if err := tc.o.Finalize(context.Background(), tc.db, tc.csr, tc.ca, tc.prov); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tc.err.Type)
|
assert.Equals(t, k.Type, tc.err.Type)
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
assert.Equals(t, k.Status, tc.err.Status)
|
assert.Equals(t, k.Status, tc.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
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)
|
t.Errorf("Order.sans() = %v, want error; got none", got)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch k := err.(type) {
|
var k *Error
|
||||||
case *Error:
|
if errors.As(err, &k) {
|
||||||
assert.Equals(t, k.Type, tt.err.Type)
|
assert.Equals(t, k.Type, tt.err.Type)
|
||||||
assert.Equals(t, k.Detail, tt.err.Detail)
|
assert.Equals(t, k.Detail, tt.err.Detail)
|
||||||
assert.Equals(t, k.Status, tt.err.Status)
|
assert.Equals(t, k.Status, tt.err.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tt.err.Err.Error())
|
assert.Equals(t, k.Err.Error(), tt.err.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tt.err.Detail)
|
assert.Equals(t, k.Detail, tt.err.Detail)
|
||||||
default:
|
} else {
|
||||||
assert.FatalError(t, errors.New("unexpected error type"))
|
assert.FatalError(t, errors.New("unexpected error type"))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
|
@ -3,7 +3,7 @@ package api
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/dsa" //nolint
|
"crypto/dsa" //nolint:staticcheck // support legacy algorithms
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
@ -40,6 +40,7 @@ type Authority interface {
|
||||||
Root(shasum string) (*x509.Certificate, error)
|
Root(shasum string) (*x509.Certificate, error)
|
||||||
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||||
Renew(peer *x509.Certificate) ([]*x509.Certificate, error)
|
Renew(peer *x509.Certificate) ([]*x509.Certificate, error)
|
||||||
|
RenewContext(ctx context.Context, peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||||
Rekey(peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
Rekey(peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||||
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
|
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
|
||||||
LoadProvisionerByName(string) (provisioner.Interface, error)
|
LoadProvisionerByName(string) (provisioner.Interface, error)
|
||||||
|
@ -49,6 +50,7 @@ type Authority interface {
|
||||||
GetRoots() ([]*x509.Certificate, error)
|
GetRoots() ([]*x509.Certificate, error)
|
||||||
GetFederation() ([]*x509.Certificate, error)
|
GetFederation() ([]*x509.Certificate, error)
|
||||||
Version() authority.Version
|
Version() authority.Version
|
||||||
|
GetCertificateRevocationList() ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustAuthority will be replaced on unit tests.
|
// mustAuthority will be replaced on unit tests.
|
||||||
|
@ -267,6 +269,7 @@ func Route(r Router) {
|
||||||
r.MethodFunc("POST", "/renew", Renew)
|
r.MethodFunc("POST", "/renew", Renew)
|
||||||
r.MethodFunc("POST", "/rekey", Rekey)
|
r.MethodFunc("POST", "/rekey", Rekey)
|
||||||
r.MethodFunc("POST", "/revoke", Revoke)
|
r.MethodFunc("POST", "/revoke", Revoke)
|
||||||
|
r.MethodFunc("GET", "/crl", CRL)
|
||||||
r.MethodFunc("GET", "/provisioners", Provisioners)
|
r.MethodFunc("GET", "/provisioners", Provisioners)
|
||||||
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
|
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
|
||||||
r.MethodFunc("GET", "/roots", Roots)
|
r.MethodFunc("GET", "/roots", Roots)
|
||||||
|
|
|
@ -192,6 +192,7 @@ type mockAuthority struct {
|
||||||
sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
sign func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||||
renew func(cert *x509.Certificate) ([]*x509.Certificate, error)
|
renew func(cert *x509.Certificate) ([]*x509.Certificate, error)
|
||||||
rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
rekey func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||||
|
renewContext func(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||||
loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error)
|
loadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error)
|
||||||
loadProvisionerByName func(name string) (provisioner.Interface, error)
|
loadProvisionerByName func(name string) (provisioner.Interface, error)
|
||||||
getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
|
getProvisioners func(nextCursor string, limit int) (provisioner.List, string, error)
|
||||||
|
@ -199,6 +200,7 @@ type mockAuthority struct {
|
||||||
getEncryptedKey func(kid string) (string, error)
|
getEncryptedKey func(kid string) (string, error)
|
||||||
getRoots func() ([]*x509.Certificate, error)
|
getRoots func() ([]*x509.Certificate, error)
|
||||||
getFederation func() ([]*x509.Certificate, error)
|
getFederation func() ([]*x509.Certificate, error)
|
||||||
|
getCRL func() ([]byte, error)
|
||||||
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
||||||
signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
|
signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||||
renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
|
renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||||
|
@ -212,6 +214,14 @@ type mockAuthority struct {
|
||||||
version func() authority.Version
|
version func() authority.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAuthority) GetCertificateRevocationList() ([]byte, error) {
|
||||||
|
if m.getCRL != nil {
|
||||||
|
return m.getCRL()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.ret1.([]byte), m.err
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove once Authorize is deprecated.
|
// TODO: remove once Authorize is deprecated.
|
||||||
func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||||
if m.authorize != nil {
|
if m.authorize != nil {
|
||||||
|
@ -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
|
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAuthority) RenewContext(ctx context.Context, oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
|
||||||
|
if m.renewContext != nil {
|
||||||
|
return m.renewContext(ctx, oldcert, pk)
|
||||||
|
}
|
||||||
|
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
|
func (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
|
||||||
if m.rekey != nil {
|
if m.rekey != nil {
|
||||||
return m.rekey(oldcert, pk)
|
return m.rekey(oldcert, pk)
|
||||||
|
@ -772,6 +789,45 @@ func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) (
|
||||||
return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err
|
return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_CRLGeneration(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
statusCode int
|
||||||
|
expected []byte
|
||||||
|
}{
|
||||||
|
{"empty", nil, http.StatusOK, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/crl", nil)
|
||||||
|
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockMustAuthority(t, &mockAuthority{ret1: tt.expected, err: tt.err})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
CRL(w, req)
|
||||||
|
res := w.Result()
|
||||||
|
|
||||||
|
if res.StatusCode != tt.statusCode {
|
||||||
|
t.Errorf("caHandler.CRL StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("caHandler.Root unexpected error = %v", err)
|
||||||
|
}
|
||||||
|
if tt.statusCode == 200 {
|
||||||
|
if !bytes.Equal(bytes.TrimSpace(body), tt.expected) {
|
||||||
|
t.Errorf("caHandler.Root CRL = %s, wants %s", body, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_caHandler_Route(t *testing.T) {
|
func Test_caHandler_Route(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Authority Authority
|
Authority Authority
|
||||||
|
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
e, ok := err.(StackTracedError)
|
var st StackTracedError
|
||||||
if !ok {
|
if !errors.As(err, &st) {
|
||||||
e, ok = errors.Cause(err).(StackTracedError)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
rl.WithFields(map[string]interface{}{
|
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 {
|
if tt.wantErr {
|
||||||
e, ok := err.(*errs.Error)
|
var e *errs.Error
|
||||||
if ok {
|
if errors.As(err, &e) {
|
||||||
if code := e.StatusCode(); code != 400 {
|
if code := e.StatusCode(); code != 400 {
|
||||||
t.Errorf("error.StatusCode() = %v, wants 400", code)
|
t.Errorf("error.StatusCode() = %v, wants 400", code)
|
||||||
}
|
}
|
||||||
|
@ -102,14 +102,15 @@ func TestProtoJSON(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
switch err.(type) {
|
var (
|
||||||
case badProtoJSONError:
|
ee *errs.Error
|
||||||
|
bpe badProtoJSONError
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &bpe):
|
||||||
assert.Contains(t, err.Error(), "syntax error")
|
assert.Contains(t, err.Error(), "syntax error")
|
||||||
case *errs.Error:
|
case errors.As(err, &ee):
|
||||||
var ee *errs.Error
|
assert.Equal(t, http.StatusBadRequest, ee.Status)
|
||||||
if errors.As(err, &ee) {
|
|
||||||
assert.Equal(t, http.StatusBadRequest, ee.Status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ package render
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
@ -77,8 +78,9 @@ type RenderableError interface {
|
||||||
func Error(w http.ResponseWriter, err error) {
|
func Error(w http.ResponseWriter, err error) {
|
||||||
log.Error(w, err)
|
log.Error(w, err)
|
||||||
|
|
||||||
if e, ok := err.(RenderableError); ok {
|
var r RenderableError
|
||||||
e.Render(w)
|
if errors.As(err, &r) {
|
||||||
|
r.Render(w)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -105,17 +107,18 @@ func statusCodeFromError(err error) (code int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for err != nil {
|
for err != nil {
|
||||||
if sc, ok := err.(StatusCodedError); ok {
|
var sc StatusCodedError
|
||||||
|
if errors.As(err, &sc) {
|
||||||
code = sc.StatusCode()
|
code = sc.StatusCode()
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
cause, ok := err.(causer)
|
var c causer
|
||||||
if !ok {
|
if !errors.As(err, &c) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
err = cause.Cause()
|
err = c.Cause()
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
24
api/renew.go
24
api/renew.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/api/render"
|
"github.com/smallstep/certificates/api/render"
|
||||||
|
"github.com/smallstep/certificates/authority"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,14 +18,22 @@ const (
|
||||||
// Renew uses the information of certificate in the TLS connection to create a
|
// Renew uses the information of certificate in the TLS connection to create a
|
||||||
// new one.
|
// new one.
|
||||||
func Renew(w http.ResponseWriter, r *http.Request) {
|
func Renew(w http.ResponseWriter, r *http.Request) {
|
||||||
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 {
|
if err != nil {
|
||||||
render.Error(w, err)
|
render.Error(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a := mustAuthority(r.Context())
|
// The token can be used by RAs to renew a certificate.
|
||||||
certChain, err := a.Renew(cert)
|
if token != "" {
|
||||||
|
ctx = authority.NewTokenContext(ctx, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
a := mustAuthority(ctx)
|
||||||
|
certChain, err := a.RenewContext(ctx, cert, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
|
render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
|
||||||
return
|
return
|
||||||
|
@ -44,15 +53,16 @@ func Renew(w http.ResponseWriter, r *http.Request) {
|
||||||
}, http.StatusCreated)
|
}, 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 {
|
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
|
||||||
return r.TLS.PeerCertificates[0], nil
|
return r.TLS.PeerCertificates[0], "", nil
|
||||||
}
|
}
|
||||||
if s := r.Header.Get(authorizationHeader); s != "" {
|
if s := r.Header.Get(authorizationHeader); s != "" {
|
||||||
if parts := strings.SplitN(s, bearerScheme+" ", 2); len(parts) == 2 {
|
if parts := strings.SplitN(s, bearerScheme+" ", 2); len(parts) == 2 {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
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 {
|
for name, tc := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
if err := tc.rr.Validate(); err != nil {
|
if err := tc.rr.Validate(); err != nil {
|
||||||
switch v := err.(type) {
|
var ee *errs.Error
|
||||||
case *errs.Error:
|
if errors.As(err, &ee) {
|
||||||
assert.HasPrefix(t, v.Error(), tc.err.Error())
|
assert.HasPrefix(t, ee.Error(), tc.err.Error())
|
||||||
assert.Equals(t, v.StatusCode(), tc.err.Status)
|
assert.Equals(t, ee.StatusCode(), tc.err.Status)
|
||||||
default:
|
} else {
|
||||||
t.Errorf("unexpected error type: %T", v)
|
t.Errorf("unexpected error type: %T", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, tc.err)
|
assert.Nil(t, tc.err)
|
||||||
|
|
|
@ -84,7 +84,6 @@ func (h *acmeAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *
|
||||||
}
|
}
|
||||||
|
|
||||||
func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey {
|
func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey {
|
||||||
|
|
||||||
if k == nil {
|
if k == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,11 +229,13 @@ func TestCreateAdminRequest_Validate(t *testing.T) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
assert.Type(t, &admin.Error{}, err)
|
assert.Type(t, &admin.Error{}, err)
|
||||||
adminErr, _ := err.(*admin.Error)
|
var adminErr *admin.Error
|
||||||
assert.Equals(t, tt.err.Type, adminErr.Type)
|
if assert.True(t, errors.As(err, &adminErr)) {
|
||||||
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
assert.Equals(t, tt.err.Type, adminErr.Type)
|
||||||
assert.Equals(t, tt.err.Status, adminErr.Status)
|
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
||||||
assert.Equals(t, tt.err.Message, adminErr.Message)
|
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 {
|
if err != nil {
|
||||||
assert.Type(t, &admin.Error{}, err)
|
assert.Type(t, &admin.Error{}, err)
|
||||||
adminErr, _ := err.(*admin.Error)
|
var ae *admin.Error
|
||||||
assert.Equals(t, tt.err.Type, adminErr.Type)
|
if assert.True(t, errors.As(err, &ae)) {
|
||||||
assert.Equals(t, tt.err.Detail, adminErr.Detail)
|
assert.Equals(t, tt.err.Type, ae.Type)
|
||||||
assert.Equals(t, tt.err.Status, adminErr.Status)
|
assert.Equals(t, tt.err.Detail, ae.Detail)
|
||||||
assert.Equals(t, tt.err.Message, adminErr.Message)
|
assert.Equals(t, tt.err.Status, ae.Status)
|
||||||
|
assert.Equals(t, tt.err.Message, ae.Message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,41 +4,47 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/acme"
|
|
||||||
"github.com/smallstep/certificates/api"
|
"github.com/smallstep/certificates/api"
|
||||||
"github.com/smallstep/certificates/authority"
|
"github.com/smallstep/certificates/authority"
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler is the Admin API request handler.
|
|
||||||
type Handler struct {
|
|
||||||
acmeResponder ACMEAdminResponder
|
|
||||||
policyResponder PolicyAdminResponder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route traffic and implement the Router interface.
|
|
||||||
//
|
|
||||||
// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder)
|
|
||||||
func (h *Handler) Route(r api.Router) {
|
|
||||||
Route(r, h.acmeResponder, h.policyResponder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHandler returns a new Authority Config Handler.
|
|
||||||
//
|
|
||||||
// Deprecated: use Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder)
|
|
||||||
func NewHandler(auth adminAuthority, adminDB admin.DB, acmeDB acme.DB, acmeResponder ACMEAdminResponder, policyResponder PolicyAdminResponder) api.RouterHandler {
|
|
||||||
return &Handler{
|
|
||||||
acmeResponder: acmeResponder,
|
|
||||||
policyResponder: policyResponder,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var mustAuthority = func(ctx context.Context) adminAuthority {
|
var mustAuthority = func(ctx context.Context) adminAuthority {
|
||||||
return authority.MustFromContext(ctx)
|
return authority.MustFromContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type router struct {
|
||||||
|
acmeResponder ACMEAdminResponder
|
||||||
|
policyResponder PolicyAdminResponder
|
||||||
|
webhookResponder WebhookAdminResponder
|
||||||
|
}
|
||||||
|
|
||||||
|
type RouterOption func(*router)
|
||||||
|
|
||||||
|
func WithACMEResponder(acmeResponder ACMEAdminResponder) RouterOption {
|
||||||
|
return func(r *router) {
|
||||||
|
r.acmeResponder = acmeResponder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPolicyResponder(policyResponder PolicyAdminResponder) RouterOption {
|
||||||
|
return func(r *router) {
|
||||||
|
r.policyResponder = policyResponder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithWebhookResponder(webhookResponder WebhookAdminResponder) RouterOption {
|
||||||
|
return func(r *router) {
|
||||||
|
r.webhookResponder = webhookResponder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Route traffic and implement the Router interface.
|
// Route traffic and implement the Router interface.
|
||||||
func Route(r api.Router, 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 {
|
authnz := func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return extractAuthorizeTokenAdmin(requireAPIEnabled(next))
|
return extractAuthorizeTokenAdmin(requireAPIEnabled(next))
|
||||||
}
|
}
|
||||||
|
@ -67,6 +73,10 @@ func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder Polic
|
||||||
return authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next)))))
|
return authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webhookMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return authnz(loadProvisionerByName(next))
|
||||||
|
}
|
||||||
|
|
||||||
// Provisioners
|
// Provisioners
|
||||||
r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner))
|
r.MethodFunc("GET", "/provisioners/{name}", authnz(GetProvisioner))
|
||||||
r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners))
|
r.MethodFunc("GET", "/provisioners", authnz(GetProvisioners))
|
||||||
|
@ -82,36 +92,42 @@ func Route(r api.Router, acmeResponder ACMEAdminResponder, policyResponder Polic
|
||||||
r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin))
|
r.MethodFunc("DELETE", "/admins/{id}", authnz(DeleteAdmin))
|
||||||
|
|
||||||
// ACME responder
|
// ACME responder
|
||||||
if acmeResponder != nil {
|
if router.acmeResponder != nil {
|
||||||
// ACME External Account Binding Keys
|
// ACME External Account Binding Keys
|
||||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
r.MethodFunc("GET", "/acme/eab/{provisionerName}/{reference}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys))
|
||||||
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.GetExternalAccountKeys))
|
r.MethodFunc("GET", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys))
|
||||||
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(acmeResponder.CreateExternalAccountKey))
|
r.MethodFunc("POST", "/acme/eab/{provisionerName}", acmeEABMiddleware(router.acmeResponder.CreateExternalAccountKey))
|
||||||
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(acmeResponder.DeleteExternalAccountKey))
|
r.MethodFunc("DELETE", "/acme/eab/{provisionerName}/{id}", acmeEABMiddleware(router.acmeResponder.DeleteExternalAccountKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Policy responder
|
// Policy responder
|
||||||
if policyResponder != nil {
|
if router.policyResponder != nil {
|
||||||
// Policy - Authority
|
// Policy - Authority
|
||||||
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(policyResponder.GetAuthorityPolicy))
|
r.MethodFunc("GET", "/policy", authorityPolicyMiddleware(router.policyResponder.GetAuthorityPolicy))
|
||||||
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(policyResponder.CreateAuthorityPolicy))
|
r.MethodFunc("POST", "/policy", authorityPolicyMiddleware(router.policyResponder.CreateAuthorityPolicy))
|
||||||
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(policyResponder.UpdateAuthorityPolicy))
|
r.MethodFunc("PUT", "/policy", authorityPolicyMiddleware(router.policyResponder.UpdateAuthorityPolicy))
|
||||||
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(policyResponder.DeleteAuthorityPolicy))
|
r.MethodFunc("DELETE", "/policy", authorityPolicyMiddleware(router.policyResponder.DeleteAuthorityPolicy))
|
||||||
|
|
||||||
// Policy - Provisioner
|
// Policy - Provisioner
|
||||||
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.GetProvisionerPolicy))
|
r.MethodFunc("GET", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.GetProvisionerPolicy))
|
||||||
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.CreateProvisionerPolicy))
|
r.MethodFunc("POST", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.CreateProvisionerPolicy))
|
||||||
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.UpdateProvisionerPolicy))
|
r.MethodFunc("PUT", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.UpdateProvisionerPolicy))
|
||||||
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(policyResponder.DeleteProvisionerPolicy))
|
r.MethodFunc("DELETE", "/provisioners/{provisionerName}/policy", provisionerPolicyMiddleware(router.policyResponder.DeleteProvisionerPolicy))
|
||||||
|
|
||||||
// Policy - ACME Account
|
// Policy - ACME Account
|
||||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
r.MethodFunc("GET", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy))
|
||||||
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.GetACMEAccountPolicy))
|
r.MethodFunc("GET", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy))
|
||||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
r.MethodFunc("POST", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy))
|
||||||
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.CreateACMEAccountPolicy))
|
r.MethodFunc("POST", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy))
|
||||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy))
|
||||||
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(policyResponder.UpdateACMEAccountPolicy))
|
r.MethodFunc("PUT", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy))
|
||||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(policyResponder.DeleteACMEAccountPolicy))
|
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/reference/{reference}", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy))
|
||||||
r.MethodFunc("DELETE", "/acme/policy/{provisionerName}/key/{keyID}", acmePolicyMiddleware(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.
|
// extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token.
|
||||||
func extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc {
|
func extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
tok := r.Header.Get("Authorization")
|
tok := r.Header.Get("Authorization")
|
||||||
if tok == "" {
|
if tok == "" {
|
||||||
render.Error(w, admin.NewError(admin.ErrorUnauthorizedType,
|
render.Error(w, admin.NewError(admin.ErrorUnauthorizedType,
|
||||||
|
|
|
@ -50,7 +50,8 @@ func (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *ht
|
||||||
|
|
||||||
auth := mustAuthority(ctx)
|
auth := mustAuthority(ctx)
|
||||||
authorityPolicy, err := auth.GetAuthorityPolicy(r.Context())
|
authorityPolicy, err := auth.GetAuthorityPolicy(r.Context())
|
||||||
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"))
|
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -74,7 +75,8 @@ func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r
|
||||||
auth := mustAuthority(ctx)
|
auth := mustAuthority(ctx)
|
||||||
authorityPolicy, err := auth.GetAuthorityPolicy(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"))
|
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -125,7 +127,8 @@ func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r
|
||||||
auth := mustAuthority(ctx)
|
auth := mustAuthority(ctx)
|
||||||
authorityPolicy, err := auth.GetAuthorityPolicy(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"))
|
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -175,7 +178,8 @@ func (par *policyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r
|
||||||
auth := mustAuthority(ctx)
|
auth := mustAuthority(ctx)
|
||||||
authorityPolicy, err := auth.GetAuthorityPolicy(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"))
|
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -468,7 +472,6 @@ func isBadRequest(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePolicy(p *linkedca.Policy) error {
|
func validatePolicy(p *linkedca.Policy) error {
|
||||||
|
|
||||||
// convert the policy; return early if nil
|
// convert the policy; return early if nil
|
||||||
options := policy.LinkedToCertificates(p)
|
options := policy.LinkedToCertificates(p)
|
||||||
if options == nil {
|
if options == nil {
|
||||||
|
|
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 {
|
for _, entry := range dbEntries {
|
||||||
adm, err := db.unmarshalAdmin(entry.Value, string(entry.Key))
|
adm, err := db.unmarshalAdmin(entry.Value, string(entry.Key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if k.IsType(admin.ErrorDeletedType) || k.IsType(admin.ErrorAuthorityMismatchType) {
|
if ae.IsType(admin.ErrorDeletedType) || ae.IsType(admin.ErrorAuthorityMismatchType) {
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,16 +68,16 @@ func TestDB_getDBAdminBytes(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if b, err := d.getDBAdminBytes(context.Background(), adminID); err != nil {
|
if b, err := d.getDBAdminBytes(context.Background(), adminID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -192,16 +192,16 @@ func TestDB_getDBAdmin(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if dba, err := d.getDBAdmin(context.Background(), adminID); err != nil {
|
if dba, err := d.getDBAdmin(context.Background(), adminID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -280,16 +280,16 @@ func TestDB_unmarshalDBAdmin(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||||
if dba, err := d.unmarshalDBAdmin(tc.in, adminID); err != nil {
|
if dba, err := d.unmarshalDBAdmin(tc.in, adminID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -355,16 +355,16 @@ func TestDB_unmarshalAdmin(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||||
if adm, err := d.unmarshalAdmin(tc.in, adminID); err != nil {
|
if adm, err := d.unmarshalAdmin(tc.in, adminID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -509,16 +509,16 @@ func TestDB_GetAdmin(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if adm, err := d.GetAdmin(context.Background(), adminID); err != nil {
|
if adm, err := d.GetAdmin(context.Background(), adminID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -661,16 +661,16 @@ func TestDB_DeleteAdmin(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if err := d.DeleteAdmin(context.Background(), adminID); err != nil {
|
if err := d.DeleteAdmin(context.Background(), adminID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -812,16 +812,16 @@ func TestDB_UpdateAdmin(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if err := d.UpdateAdmin(context.Background(), tc.adm); err != nil {
|
if err := d.UpdateAdmin(context.Background(), tc.adm); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -910,16 +910,16 @@ func TestDB_CreateAdmin(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if err := d.CreateAdmin(context.Background(), tc.adm); err != nil {
|
if err := d.CreateAdmin(context.Background(), tc.adm); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -1086,16 +1086,16 @@ func TestDB_GetAdmins(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if admins, err := d.GetAdmins(context.Background()); err != nil {
|
if admins, err := d.GetAdmins(context.Background()); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,7 @@ func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string)
|
||||||
|
|
||||||
func (db *DB) unmarshalDBAuthorityPolicy(data []byte) (*dbAuthorityPolicy, error) {
|
func (db *DB) unmarshalDBAuthorityPolicy(data []byte) (*dbAuthorityPolicy, error) {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
|
//nolint:nilnil // legacy
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var dba = new(dbAuthorityPolicy)
|
var dba = new(dbAuthorityPolicy)
|
||||||
|
@ -102,6 +103,7 @@ func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*db
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if dbap == nil {
|
if dbap == nil {
|
||||||
|
//nolint:nilnil // legacy
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if dbap.AuthorityID != authorityID {
|
if dbap.AuthorityID != authorityID {
|
||||||
|
@ -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 {
|
func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||||
|
|
||||||
dbap := &dbAuthorityPolicy{
|
dbap := &dbAuthorityPolicy{
|
||||||
ID: db.authorityID,
|
ID: db.authorityID,
|
||||||
AuthorityID: db.authorityID,
|
AuthorityID: db.authorityID,
|
||||||
|
@ -228,7 +229,6 @@ func dbToLinked(p *dbPolicy) *linkedca.Policy {
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkedToDB(p *linkedca.Policy) *dbPolicy {
|
func linkedToDB(p *linkedca.Policy) *dbPolicy {
|
||||||
|
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,16 +72,16 @@ func TestDB_getDBAuthorityPolicyBytes(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if b, err := d.getDBAuthorityPolicyBytes(tc.ctx, tc.authorityID); err != nil {
|
if b, err := d.getDBAuthorityPolicyBytes(tc.ctx, tc.authorityID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -208,16 +208,16 @@ func TestDB_getDBAuthorityPolicy(t *testing.T) {
|
||||||
dbp, err := d.getDBAuthorityPolicy(tc.ctx, tc.authorityID)
|
dbp, err := d.getDBAuthorityPolicy(tc.ctx, tc.authorityID)
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -309,16 +309,16 @@ func TestDB_CreateAuthorityPolicy(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||||
if err := d.CreateAuthorityPolicy(tc.ctx, tc.policy); err != nil {
|
if err := d.CreateAuthorityPolicy(tc.ctx, tc.policy); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -406,16 +406,16 @@ func TestDB_GetAuthorityPolicy(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||||
got, err := d.GetAuthorityPolicy(tc.ctx)
|
got, err := d.GetAuthorityPolicy(tc.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -578,16 +578,16 @@ func TestDB_UpdateAuthorityPolicy(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||||
if err := d.UpdateAuthorityPolicy(tc.ctx, tc.policy); err != nil {
|
if err := d.UpdateAuthorityPolicy(tc.ctx, tc.policy); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -718,16 +718,16 @@ func TestDB_DeleteAuthorityPolicy(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: tc.authorityID}
|
d := DB{db: tc.db, authorityID: tc.authorityID}
|
||||||
if err := d.DeleteAuthorityPolicy(tc.ctx); err != nil {
|
if err := d.DeleteAuthorityPolicy(tc.ctx); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,24 @@ type dbProvisioner struct {
|
||||||
SSHTemplate *linkedca.Template `json:"sshTemplate"`
|
SSHTemplate *linkedca.Template `json:"sshTemplate"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
DeletedAt time.Time `json:"deletedAt"`
|
DeletedAt time.Time `json:"deletedAt"`
|
||||||
|
Webhooks []dbWebhook `json:"webhooks,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbBasicAuth struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dbWebhook struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
BearerToken string `json:"bearerToken,omitempty"`
|
||||||
|
BasicAuth *dbBasicAuth `json:"basicAuth,omitempty"`
|
||||||
|
DisableTLSClientAuth bool `json:"disableTLSClientAuth,omitempty"`
|
||||||
|
CertType string `json:"certType,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dbp *dbProvisioner) clone() *dbProvisioner {
|
func (dbp *dbProvisioner) clone() *dbProvisioner {
|
||||||
|
@ -48,6 +66,7 @@ func (dbp *dbProvisioner) convert2linkedca() (*linkedca.Provisioner, error) {
|
||||||
SshTemplate: dbp.SSHTemplate,
|
SshTemplate: dbp.SSHTemplate,
|
||||||
CreatedAt: timestamppb.New(dbp.CreatedAt),
|
CreatedAt: timestamppb.New(dbp.CreatedAt),
|
||||||
DeletedAt: timestamppb.New(dbp.DeletedAt),
|
DeletedAt: timestamppb.New(dbp.DeletedAt),
|
||||||
|
Webhooks: dbWebhooksToLinkedca(dbp.Webhooks),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,14 +141,14 @@ func (db *DB) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, err
|
||||||
for _, entry := range dbEntries {
|
for _, entry := range dbEntries {
|
||||||
prov, err := db.unmarshalProvisioner(entry.Value, string(entry.Key))
|
prov, err := db.unmarshalProvisioner(entry.Value, string(entry.Key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if k.IsType(admin.ErrorDeletedType) || k.IsType(admin.ErrorAuthorityMismatchType) {
|
if ae.IsType(admin.ErrorDeletedType) || ae.IsType(admin.ErrorAuthorityMismatchType) {
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,6 +183,7 @@ func (db *DB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
|
||||||
X509Template: prov.X509Template,
|
X509Template: prov.X509Template,
|
||||||
SSHTemplate: prov.SshTemplate,
|
SSHTemplate: prov.SshTemplate,
|
||||||
CreatedAt: clock.Now(),
|
CreatedAt: clock.Now(),
|
||||||
|
Webhooks: linkedcaWebhooksToDB(prov.Webhooks),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.save(ctx, prov.Id, dbp, nil, "provisioner", provisionersTable); err != nil {
|
if err := db.save(ctx, prov.Id, dbp, nil, "provisioner", provisionersTable); err != nil {
|
||||||
|
@ -193,6 +213,7 @@ func (db *DB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner)
|
||||||
}
|
}
|
||||||
nu.X509Template = prov.X509Template
|
nu.X509Template = prov.X509Template
|
||||||
nu.SSHTemplate = prov.SshTemplate
|
nu.SSHTemplate = prov.SshTemplate
|
||||||
|
nu.Webhooks = linkedcaWebhooksToDB(prov.Webhooks)
|
||||||
|
|
||||||
return db.save(ctx, prov.Id, nu, old, "provisioner", provisionersTable)
|
return db.save(ctx, prov.Id, nu, old, "provisioner", provisionersTable)
|
||||||
}
|
}
|
||||||
|
@ -209,3 +230,70 @@ func (db *DB) DeleteProvisioner(ctx context.Context, id string) error {
|
||||||
|
|
||||||
return db.save(ctx, old.ID, nu, old, "provisioner", provisionersTable)
|
return db.save(ctx, old.ID, nu, old, "provisioner", provisionersTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dbWebhooksToLinkedca(dbwhs []dbWebhook) []*linkedca.Webhook {
|
||||||
|
if len(dbwhs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lwhs := make([]*linkedca.Webhook, len(dbwhs))
|
||||||
|
|
||||||
|
for i, dbwh := range dbwhs {
|
||||||
|
lwh := &linkedca.Webhook{
|
||||||
|
Name: dbwh.Name,
|
||||||
|
Id: dbwh.ID,
|
||||||
|
Url: dbwh.URL,
|
||||||
|
Kind: linkedca.Webhook_Kind(linkedca.Webhook_Kind_value[dbwh.Kind]),
|
||||||
|
Secret: dbwh.Secret,
|
||||||
|
DisableTlsClientAuth: dbwh.DisableTLSClientAuth,
|
||||||
|
CertType: linkedca.Webhook_CertType(linkedca.Webhook_CertType_value[dbwh.CertType]),
|
||||||
|
}
|
||||||
|
if dbwh.BearerToken != "" {
|
||||||
|
lwh.Auth = &linkedca.Webhook_BearerToken{
|
||||||
|
BearerToken: &linkedca.BearerToken{
|
||||||
|
BearerToken: dbwh.BearerToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if dbwh.BasicAuth != nil && (dbwh.BasicAuth.Username != "" || dbwh.BasicAuth.Password != "") {
|
||||||
|
lwh.Auth = &linkedca.Webhook_BasicAuth{
|
||||||
|
BasicAuth: &linkedca.BasicAuth{
|
||||||
|
Username: dbwh.BasicAuth.Username,
|
||||||
|
Password: dbwh.BasicAuth.Password,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lwhs[i] = lwh
|
||||||
|
}
|
||||||
|
|
||||||
|
return lwhs
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkedcaWebhooksToDB(lwhs []*linkedca.Webhook) []dbWebhook {
|
||||||
|
if len(lwhs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dbwhs := make([]dbWebhook, len(lwhs))
|
||||||
|
|
||||||
|
for i, lwh := range lwhs {
|
||||||
|
dbwh := dbWebhook{
|
||||||
|
Name: lwh.Name,
|
||||||
|
ID: lwh.Id,
|
||||||
|
URL: lwh.Url,
|
||||||
|
Kind: lwh.Kind.String(),
|
||||||
|
Secret: lwh.Secret,
|
||||||
|
DisableTLSClientAuth: lwh.DisableTlsClientAuth,
|
||||||
|
CertType: lwh.CertType.String(),
|
||||||
|
}
|
||||||
|
switch a := lwh.GetAuth().(type) {
|
||||||
|
case *linkedca.Webhook_BearerToken:
|
||||||
|
dbwh.BearerToken = a.BearerToken.BearerToken
|
||||||
|
case *linkedca.Webhook_BasicAuth:
|
||||||
|
dbwh.BasicAuth = &dbBasicAuth{
|
||||||
|
Username: a.BasicAuth.Username,
|
||||||
|
Password: a.BasicAuth.Password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dbwhs[i] = dbwh
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbwhs
|
||||||
|
}
|
||||||
|
|
|
@ -67,16 +67,16 @@ func TestDB_getDBProvisionerBytes(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db}
|
d := DB{db: tc.db}
|
||||||
if b, err := d.getDBProvisionerBytes(context.Background(), provID); err != nil {
|
if b, err := d.getDBProvisionerBytes(context.Background(), provID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -137,6 +137,7 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fail/deleted": func(t *testing.T) test {
|
"fail/deleted": func(t *testing.T) test {
|
||||||
|
|
||||||
now := clock.Now()
|
now := clock.Now()
|
||||||
dbp := &dbProvisioner{
|
dbp := &dbProvisioner{
|
||||||
ID: provID,
|
ID: provID,
|
||||||
|
@ -189,16 +190,16 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if dbp, err := d.getDBProvisioner(context.Background(), provID); err != nil {
|
if dbp, err := d.getDBProvisioner(context.Background(), provID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -210,6 +211,7 @@ func TestDB_getDBProvisioner(t *testing.T) {
|
||||||
assert.Equals(t, dbp.Name, tc.dbp.Name)
|
assert.Equals(t, dbp.Name, tc.dbp.Name)
|
||||||
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
|
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
|
||||||
assert.Fatal(t, dbp.DeletedAt.IsZero())
|
assert.Fatal(t, dbp.DeletedAt.IsZero())
|
||||||
|
assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -275,16 +277,16 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||||
if dbp, err := d.unmarshalDBProvisioner(tc.in, provID); err != nil {
|
if dbp, err := d.unmarshalDBProvisioner(tc.in, provID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -300,6 +302,7 @@ func TestDB_unmarshalDBProvisioner(t *testing.T) {
|
||||||
assert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate)
|
assert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate)
|
||||||
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
|
assert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)
|
||||||
assert.Fatal(t, dbp.DeletedAt.IsZero())
|
assert.Fatal(t, dbp.DeletedAt.IsZero())
|
||||||
|
assert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -353,6 +356,15 @@ func defaultDBP(t *testing.T) *dbProvisioner {
|
||||||
Data: []byte("zap"),
|
Data: []byte("zap"),
|
||||||
},
|
},
|
||||||
CreatedAt: clock.Now(),
|
CreatedAt: clock.Now(),
|
||||||
|
Webhooks: []dbWebhook{
|
||||||
|
{
|
||||||
|
Name: "metadata",
|
||||||
|
URL: "https://inventory.smallstep.com",
|
||||||
|
Kind: linkedca.Webhook_ENRICHING.String(),
|
||||||
|
Secret: "secret",
|
||||||
|
BearerToken: "token",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,16 +409,16 @@ func TestDB_unmarshalProvisioner(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{authorityID: admin.DefaultAuthorityID}
|
d := DB{authorityID: admin.DefaultAuthorityID}
|
||||||
if prov, err := d.unmarshalProvisioner(tc.in, provID); err != nil {
|
if prov, err := d.unmarshalProvisioner(tc.in, provID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -419,6 +431,7 @@ func TestDB_unmarshalProvisioner(t *testing.T) {
|
||||||
assert.Equals(t, prov.Claims, tc.dbp.Claims)
|
assert.Equals(t, prov.Claims, tc.dbp.Claims)
|
||||||
assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
|
assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
|
||||||
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
|
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
|
||||||
|
assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))
|
||||||
|
|
||||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -535,16 +548,16 @@ func TestDB_GetProvisioner(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if prov, err := d.GetProvisioner(context.Background(), provID); err != nil {
|
if prov, err := d.GetProvisioner(context.Background(), provID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -557,6 +570,7 @@ func TestDB_GetProvisioner(t *testing.T) {
|
||||||
assert.Equals(t, prov.Claims, tc.dbp.Claims)
|
assert.Equals(t, prov.Claims, tc.dbp.Claims)
|
||||||
assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
|
assert.Equals(t, prov.X509Template, tc.dbp.X509Template)
|
||||||
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
|
assert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)
|
||||||
|
assert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))
|
||||||
|
|
||||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -629,6 +643,7 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
||||||
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
|
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
|
||||||
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
|
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
|
||||||
assert.Equals(t, _dbp.Details, dbp.Details)
|
assert.Equals(t, _dbp.Details, dbp.Details)
|
||||||
|
assert.Equals(t, _dbp.Webhooks, dbp.Webhooks)
|
||||||
|
|
||||||
assert.True(t, _dbp.DeletedAt.Before(time.Now()))
|
assert.True(t, _dbp.DeletedAt.Before(time.Now()))
|
||||||
assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))
|
assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))
|
||||||
|
@ -668,6 +683,7 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
||||||
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
|
assert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)
|
||||||
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
|
assert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)
|
||||||
assert.Equals(t, _dbp.Details, dbp.Details)
|
assert.Equals(t, _dbp.Details, dbp.Details)
|
||||||
|
assert.Equals(t, _dbp.Webhooks, dbp.Webhooks)
|
||||||
|
|
||||||
assert.True(t, _dbp.DeletedAt.Before(time.Now()))
|
assert.True(t, _dbp.DeletedAt.Before(time.Now()))
|
||||||
assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))
|
assert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))
|
||||||
|
@ -683,16 +699,16 @@ func TestDB_DeleteProvisioner(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if err := d.DeleteProvisioner(context.Background(), provID); err != nil {
|
if err := d.DeleteProvisioner(context.Background(), provID); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -819,6 +835,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
||||||
assert.Equals(t, provs[0].Claims, fooProv.Claims)
|
assert.Equals(t, provs[0].Claims, fooProv.Claims)
|
||||||
assert.Equals(t, provs[0].X509Template, fooProv.X509Template)
|
assert.Equals(t, provs[0].X509Template, fooProv.X509Template)
|
||||||
assert.Equals(t, provs[0].SshTemplate, fooProv.SSHTemplate)
|
assert.Equals(t, provs[0].SshTemplate, fooProv.SSHTemplate)
|
||||||
|
assert.Equals(t, provs[0].Webhooks, dbWebhooksToLinkedca(fooProv.Webhooks))
|
||||||
|
|
||||||
retDetailsBytes, err := json.Marshal(provs[0].Details.GetData())
|
retDetailsBytes, err := json.Marshal(provs[0].Details.GetData())
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -831,6 +848,7 @@ func TestDB_GetProvisioners(t *testing.T) {
|
||||||
assert.Equals(t, provs[1].Claims, zapProv.Claims)
|
assert.Equals(t, provs[1].Claims, zapProv.Claims)
|
||||||
assert.Equals(t, provs[1].X509Template, zapProv.X509Template)
|
assert.Equals(t, provs[1].X509Template, zapProv.X509Template)
|
||||||
assert.Equals(t, provs[1].SshTemplate, zapProv.SSHTemplate)
|
assert.Equals(t, provs[1].SshTemplate, zapProv.SSHTemplate)
|
||||||
|
assert.Equals(t, provs[1].Webhooks, dbWebhooksToLinkedca(zapProv.Webhooks))
|
||||||
|
|
||||||
retDetailsBytes, err = json.Marshal(provs[1].Details.GetData())
|
retDetailsBytes, err = json.Marshal(provs[1].Details.GetData())
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -844,16 +862,16 @@ func TestDB_GetProvisioners(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if provs, err := d.GetProvisioners(context.Background()); err != nil {
|
if provs, err := d.GetProvisioners(context.Background()); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -895,6 +913,7 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
||||||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||||
|
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||||
|
|
||||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -932,6 +951,7 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
||||||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||||
|
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||||
|
|
||||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -952,16 +972,16 @@ func TestDB_CreateProvisioner(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if err := d.CreateProvisioner(context.Background(), tc.prov); err != nil {
|
if err := d.CreateProvisioner(context.Background(), tc.prov); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -1080,6 +1100,7 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
||||||
assert.Equals(t, _dbp.Claims, prov.Claims)
|
assert.Equals(t, _dbp.Claims, prov.Claims)
|
||||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||||
|
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||||
|
|
||||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -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)
|
data, err := json.Marshal(dbp)
|
||||||
assert.FatalError(t, err)
|
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.Claims, prov.Claims)
|
||||||
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
assert.Equals(t, _dbp.X509Template, prov.X509Template)
|
||||||
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
assert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)
|
||||||
|
assert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))
|
||||||
|
|
||||||
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
retDetailsBytes, err := json.Marshal(prov.Details.GetData())
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
@ -1188,16 +1216,16 @@ func TestDB_UpdateProvisioner(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
d := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}
|
||||||
if err := d.UpdateProvisioner(context.Background(), tc.prov); err != nil {
|
if err := d.UpdateProvisioner(context.Background(), tc.prov); err != nil {
|
||||||
switch k := err.(type) {
|
var ae *admin.Error
|
||||||
case *admin.Error:
|
if errors.As(err, &ae) {
|
||||||
if assert.NotNil(t, tc.adminErr) {
|
if assert.NotNil(t, tc.adminErr) {
|
||||||
assert.Equals(t, k.Type, tc.adminErr.Type)
|
assert.Equals(t, ae.Type, tc.adminErr.Type)
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
assert.Equals(t, k.Status, tc.adminErr.Status)
|
assert.Equals(t, ae.Status, tc.adminErr.Status)
|
||||||
assert.Equals(t, k.Err.Error(), tc.adminErr.Err.Error())
|
assert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())
|
||||||
assert.Equals(t, k.Detail, tc.adminErr.Detail)
|
assert.Equals(t, ae.Detail, tc.adminErr.Detail)
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -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.
|
// WrapError attempts to wrap the internal error.
|
||||||
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
||||||
switch e := err.(type) {
|
var ee *Error
|
||||||
case nil:
|
switch {
|
||||||
|
case err == nil:
|
||||||
return nil
|
return nil
|
||||||
case *Error:
|
case errors.As(err, &ee):
|
||||||
if e.Err == nil {
|
if ee.Err == nil {
|
||||||
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
ee.Err = errors.Errorf(msg+"; "+ee.Detail, args...)
|
||||||
} else {
|
} else {
|
||||||
e.Err = errors.Wrapf(e.Err, msg, args...)
|
ee.Err = errors.Wrapf(ee.Err, msg, args...)
|
||||||
}
|
}
|
||||||
return e
|
return ee
|
||||||
default:
|
default:
|
||||||
return newError(typ, errors.Wrapf(err, msg, args...))
|
return newError(typ, errors.Wrapf(err, msg, args...))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package authority
|
package authority
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -24,6 +26,7 @@ import (
|
||||||
adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql"
|
adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql"
|
||||||
"github.com/smallstep/certificates/authority/administrator"
|
"github.com/smallstep/certificates/authority/administrator"
|
||||||
"github.com/smallstep/certificates/authority/config"
|
"github.com/smallstep/certificates/authority/config"
|
||||||
|
"github.com/smallstep/certificates/authority/internal/constraints"
|
||||||
"github.com/smallstep/certificates/authority/policy"
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/cas"
|
"github.com/smallstep/certificates/cas"
|
||||||
|
@ -44,16 +47,18 @@ type Authority struct {
|
||||||
adminDB admin.DB
|
adminDB admin.DB
|
||||||
templates *templates.Templates
|
templates *templates.Templates
|
||||||
linkedCAToken string
|
linkedCAToken string
|
||||||
|
webhookClient *http.Client
|
||||||
|
|
||||||
// X509 CA
|
// X509 CA
|
||||||
password []byte
|
password []byte
|
||||||
issuerPassword []byte
|
issuerPassword []byte
|
||||||
x509CAService cas.CertificateAuthorityService
|
x509CAService cas.CertificateAuthorityService
|
||||||
rootX509Certs []*x509.Certificate
|
rootX509Certs []*x509.Certificate
|
||||||
rootX509CertPool *x509.CertPool
|
rootX509CertPool *x509.CertPool
|
||||||
federatedX509Certs []*x509.Certificate
|
federatedX509Certs []*x509.Certificate
|
||||||
certificates *sync.Map
|
intermediateX509Certs []*x509.Certificate
|
||||||
x509Enforcers []provisioner.CertificateEnforcer
|
certificates *sync.Map
|
||||||
|
x509Enforcers []provisioner.CertificateEnforcer
|
||||||
|
|
||||||
// SCEP CA
|
// SCEP CA
|
||||||
scepService *scep.Service
|
scepService *scep.Service
|
||||||
|
@ -68,7 +73,12 @@ type Authority struct {
|
||||||
sshCAUserFederatedCerts []ssh.PublicKey
|
sshCAUserFederatedCerts []ssh.PublicKey
|
||||||
sshCAHostFederatedCerts []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
|
initOnce bool
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
|
|
||||||
|
@ -80,13 +90,17 @@ type Authority struct {
|
||||||
authorizeRenewFunc provisioner.AuthorizeRenewFunc
|
authorizeRenewFunc provisioner.AuthorizeRenewFunc
|
||||||
authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc
|
authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc
|
||||||
|
|
||||||
// Policy engines
|
// Constraints and Policy engines
|
||||||
policyEngine *policy.Engine
|
constraintsEngine *constraints.Engine
|
||||||
|
policyEngine *policy.Engine
|
||||||
|
|
||||||
adminMutex sync.RWMutex
|
adminMutex sync.RWMutex
|
||||||
|
|
||||||
// Do Not initialize the authority
|
// If true, do not initialize the authority
|
||||||
skipInit bool
|
skipInit bool
|
||||||
|
|
||||||
|
// If true, do not output initialization logs
|
||||||
|
quietInit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info contains information about the authority.
|
// Info contains information about the authority.
|
||||||
|
@ -368,11 +382,17 @@ func (a *Authority) init() error {
|
||||||
}
|
}
|
||||||
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||||
SigningKey: a.config.IntermediateKey,
|
SigningKey: a.config.IntermediateKey,
|
||||||
Password: []byte(a.password),
|
Password: a.password,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// If not defined with an option, add intermediates to the list of
|
||||||
|
// certificates used for name constraints validation at issuance
|
||||||
|
// time.
|
||||||
|
if len(a.intermediateX509Certs) == 0 {
|
||||||
|
a.intermediateX509Certs = append(a.intermediateX509Certs, options.CertificateChain...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
a.x509CAService, err = cas.New(ctx, options)
|
a.x509CAService, err = cas.New(ctx, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -434,7 +454,7 @@ func (a *Authority) init() error {
|
||||||
if a.config.SSH.HostKey != "" {
|
if a.config.SSH.HostKey != "" {
|
||||||
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||||
SigningKey: a.config.SSH.HostKey,
|
SigningKey: a.config.SSH.HostKey,
|
||||||
Password: []byte(a.sshHostPassword),
|
Password: a.sshHostPassword,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -460,7 +480,7 @@ func (a *Authority) init() error {
|
||||||
if a.config.SSH.UserKey != "" {
|
if a.config.SSH.UserKey != "" {
|
||||||
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||||
SigningKey: a.config.SSH.UserKey,
|
SigningKey: a.config.SSH.UserKey,
|
||||||
Password: []byte(a.sshUserPassword),
|
Password: a.sshUserPassword,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -545,7 +565,7 @@ func (a *Authority) init() error {
|
||||||
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...)
|
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...)
|
||||||
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
|
||||||
SigningKey: a.config.IntermediateKey,
|
SigningKey: a.config.IntermediateKey,
|
||||||
Password: []byte(a.password),
|
Password: a.password,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -554,7 +574,7 @@ func (a *Authority) init() error {
|
||||||
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
|
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
|
||||||
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
|
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
|
||||||
DecryptionKey: a.config.IntermediateKey,
|
DecryptionKey: a.config.IntermediateKey,
|
||||||
Password: []byte(a.password),
|
Password: a.password,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -588,20 +608,74 @@ func (a *Authority) init() error {
|
||||||
return admin.WrapErrorISE(err, "error loading provisioners to initialize authority")
|
return admin.WrapErrorISE(err, "error loading provisioners to initialize authority")
|
||||||
}
|
}
|
||||||
if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") {
|
if len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, "linked") {
|
||||||
// Create First Provisioner
|
// Migration will currently only be kicked off once, because either one or more provisioners
|
||||||
prov, err := CreateFirstProvisioner(ctx, a.adminDB, string(a.password))
|
// are migrated or a default JWK provisioner will be created in the DB. It won't run for
|
||||||
if err != nil {
|
// linked or hosted deployments. Not for linked, because that case is explicitly checked
|
||||||
return admin.WrapErrorISE(err, "error creating first provisioner")
|
// 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{
|
if err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{
|
||||||
ProvisionerId: prov.Id,
|
ProvisionerId: firstJWKProvisioner.Id,
|
||||||
Subject: "step",
|
Subject: superAdminSubject,
|
||||||
Type: linkedca.Admin_SUPER_ADMIN,
|
Type: linkedca.Admin_SUPER_ADMIN,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return admin.WrapErrorISE(err, "error creating first admin")
|
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
|
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
|
// Load x509 and SSH Policy Engines
|
||||||
if err := a.reloadPolicyEngines(ctx); err != nil {
|
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -627,6 +716,18 @@ func (a *Authority) init() error {
|
||||||
a.templates.Data["Step"] = tmplVars
|
a.templates.Data["Step"] = tmplVars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the CRL generator, we can assume the configuration is validated.
|
||||||
|
if a.config.CRL.IsEnabled() {
|
||||||
|
// Default cache duration to the default one
|
||||||
|
if v := a.config.CRL.CacheDuration; v == nil || v.Duration <= 0 {
|
||||||
|
a.config.CRL.CacheDuration = config.DefaultCRLCacheDuration
|
||||||
|
}
|
||||||
|
// Start CRL generator
|
||||||
|
if err := a.startCRLGenerator(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// JWT numeric dates are seconds.
|
// JWT numeric dates are seconds.
|
||||||
a.startTime = time.Now().Truncate(time.Second)
|
a.startTime = time.Now().Truncate(time.Second)
|
||||||
// Set flag indicating that initialization has been completed, and should
|
// Set flag indicating that initialization has been completed, and should
|
||||||
|
@ -636,6 +737,14 @@ func (a *Authority) init() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initLogf is used to log initialization information. The output
|
||||||
|
// can be disabled by starting the CA with the `--quiet` flag.
|
||||||
|
func (a *Authority) initLogf(format string, v ...any) {
|
||||||
|
if !a.quietInit {
|
||||||
|
log.Printf(format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetID returns the define authority id or a zero uuid.
|
// GetID returns the define authority id or a zero uuid.
|
||||||
func (a *Authority) GetID() string {
|
func (a *Authority) GetID() string {
|
||||||
const zeroUUID = "00000000-0000-0000-0000-000000000000"
|
const zeroUUID = "00000000-0000-0000-0000-000000000000"
|
||||||
|
@ -685,6 +794,11 @@ func (a *Authority) IsAdminAPIEnabled() bool {
|
||||||
|
|
||||||
// Shutdown safely shuts down any clients, databases, etc. held by the Authority.
|
// Shutdown safely shuts down any clients, databases, etc. held by the Authority.
|
||||||
func (a *Authority) Shutdown() error {
|
func (a *Authority) Shutdown() error {
|
||||||
|
if a.crlTicker != nil {
|
||||||
|
a.crlTicker.Stop()
|
||||||
|
close(a.crlStopper)
|
||||||
|
}
|
||||||
|
|
||||||
if err := a.keyManager.Close(); err != nil {
|
if err := a.keyManager.Close(); err != nil {
|
||||||
log.Printf("error closing the key manager: %v", err)
|
log.Printf("error closing the key manager: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -693,6 +807,11 @@ func (a *Authority) Shutdown() error {
|
||||||
|
|
||||||
// CloseForReload closes internal services, to allow a safe reload.
|
// CloseForReload closes internal services, to allow a safe reload.
|
||||||
func (a *Authority) CloseForReload() {
|
func (a *Authority) CloseForReload() {
|
||||||
|
if a.crlTicker != nil {
|
||||||
|
a.crlTicker.Stop()
|
||||||
|
close(a.crlStopper)
|
||||||
|
}
|
||||||
|
|
||||||
if err := a.keyManager.Close(); err != nil {
|
if err := a.keyManager.Close(); err != nil {
|
||||||
log.Printf("error closing the key manager: %v", err)
|
log.Printf("error closing the key manager: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -733,11 +852,49 @@ func (a *Authority) requiresSCEPService() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSCEPService returns the configured SCEP Service
|
// GetSCEPService returns the configured SCEP Service.
|
||||||
// TODO: this function is intended to exist temporarily
|
//
|
||||||
// in order to make SCEP work more easily. It can be
|
// TODO: this function is intended to exist temporarily in order to make SCEP
|
||||||
// made more correct by using the right interfaces/abstractions
|
// work more easily. It can be made more correct by using the right
|
||||||
// after it works as expected.
|
// interfaces/abstractions after it works as expected.
|
||||||
func (a *Authority) GetSCEPService() *scep.Service {
|
func (a *Authority) GetSCEPService() *scep.Service {
|
||||||
return a.scepService
|
return a.scepService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Authority) startCRLGenerator() error {
|
||||||
|
if !a.config.CRL.IsEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that there is a valid CRL in the DB right now. If it doesn't exist
|
||||||
|
// or is expired, generate one now
|
||||||
|
_, ok := a.db.(db.CertificateRevocationListDB)
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("CRL Generation requested, but database does not support CRL generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always create a new CRL on startup in case the CA has been down and the
|
||||||
|
// time to next expected CRL update is less than the cache duration.
|
||||||
|
if err := a.GenerateCertificateRevocationList(); err != nil {
|
||||||
|
return errors.Wrap(err, "could not generate a CRL")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.crlStopper = make(chan struct{}, 1)
|
||||||
|
a.crlTicker = time.NewTicker(a.config.CRL.TickerDuration())
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-a.crlTicker.C:
|
||||||
|
log.Println("Regenerating CRL")
|
||||||
|
if err := a.GenerateCertificateRevocationList(); err != nil {
|
||||||
|
log.Printf("error regenerating the CRL: %v", err)
|
||||||
|
}
|
||||||
|
case <-a.crlStopper:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/authority/admin"
|
"github.com/smallstep/certificates/authority/admin"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
|
@ -285,7 +286,7 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error {
|
||||||
// extra extension cannot be found, authorize the renewal by default.
|
// extra extension cannot be found, authorize the renewal by default.
|
||||||
//
|
//
|
||||||
// TODO(mariano): should we authorize by default?
|
// TODO(mariano): should we authorize by default?
|
||||||
func (a *Authority) authorizeRenew(cert *x509.Certificate) error {
|
func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
||||||
serial := cert.SerialNumber.String()
|
serial := cert.SerialNumber.String()
|
||||||
var opts = []interface{}{errs.WithKeyVal("serialNumber", serial)}
|
var opts = []interface{}{errs.WithKeyVal("serialNumber", serial)}
|
||||||
|
|
||||||
|
@ -307,7 +308,7 @@ func (a *Authority) authorizeRenew(cert *x509.Certificate) error {
|
||||||
return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...)
|
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 errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -416,16 +417,16 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.
|
||||||
Subject: leaf.Subject.CommonName,
|
Subject: leaf.Subject.CommonName,
|
||||||
Time: time.Now().UTC(),
|
Time: time.Now().UTC(),
|
||||||
}, time.Minute); err != nil {
|
}, time.Minute); err != nil {
|
||||||
switch err {
|
switch {
|
||||||
case jose.ErrInvalidIssuer:
|
case errors.Is(err, jose.ErrInvalidIssuer):
|
||||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid issuer claim (iss)"))
|
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid issuer claim (iss)"))
|
||||||
case jose.ErrInvalidSubject:
|
case errors.Is(err, jose.ErrInvalidSubject):
|
||||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid subject claim (sub)"))
|
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: invalid subject claim (sub)"))
|
||||||
case jose.ErrNotValidYet:
|
case errors.Is(err, jose.ErrNotValidYet):
|
||||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token not valid yet (nbf)"))
|
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token not valid yet (nbf)"))
|
||||||
case jose.ErrExpired:
|
case errors.Is(err, jose.ErrExpired):
|
||||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token is expired (exp)"))
|
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token is expired (exp)"))
|
||||||
case 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)"))
|
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token: token issued in the future (iat)"))
|
||||||
default:
|
default:
|
||||||
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token"))
|
return nil, errs.UnauthorizedErr(err, errs.WithMessage("error validating renew token"))
|
||||||
|
@ -433,7 +434,7 @@ func (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.
|
||||||
}
|
}
|
||||||
|
|
||||||
audiences := a.config.GetAudiences().Renew
|
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)"))
|
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)
|
p, err := tc.auth.authorizeToken(context.Background(), tc.token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -399,8 +399,8 @@ func TestAuthority_authorizeRevoke(t *testing.T) {
|
||||||
|
|
||||||
if err := tc.auth.authorizeRevoke(context.Background(), tc.token); err != nil {
|
if err := tc.auth.authorizeRevoke(context.Background(), tc.token); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -484,14 +484,14 @@ func TestAuthority_authorizeSign(t *testing.T) {
|
||||||
got, err := tc.auth.authorizeSign(context.Background(), tc.token)
|
got, err := tc.auth.authorizeSign(context.Background(), tc.token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
assert.Equals(t, 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 err != nil {
|
||||||
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
|
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
|
|
||||||
ctxErr, ok := err.(*errs.Error)
|
var ctxErr *errs.Error
|
||||||
assert.Fatal(t, ok, "error is not of type *errs.Error")
|
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
|
||||||
assert.Equals(t, ctxErr.Details["token"], tc.token)
|
assert.Equals(t, ctxErr.Details["token"], tc.token)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -876,16 +876,16 @@ func TestAuthority_authorizeRenew(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
tc := genTestCase(t)
|
tc := genTestCase(t)
|
||||||
|
|
||||||
err := tc.auth.authorizeRenew(tc.cert)
|
err := tc.auth.authorizeRenew(context.Background(), tc.cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
|
|
||||||
ctxErr, ok := err.(*errs.Error)
|
var ctxErr *errs.Error
|
||||||
assert.Fatal(t, ok, "error is not of type *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())
|
assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1027,14 +1027,14 @@ func TestAuthority_authorizeSSHSign(t *testing.T) {
|
||||||
got, err := tc.auth.authorizeSSHSign(context.Background(), tc.token)
|
got, err := tc.auth.authorizeSSHSign(context.Background(), tc.token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
assert.Len(t, 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)
|
got, err := tc.auth.authorizeSSHRenew(context.Background(), tc.token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -1244,8 +1244,8 @@ func TestAuthority_authorizeSSHRevoke(t *testing.T) {
|
||||||
|
|
||||||
if err := tc.auth.authorizeSSHRevoke(context.Background(), tc.token); err != nil {
|
if err := tc.auth.authorizeSSHRevoke(context.Background(), tc.token); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -1337,8 +1337,8 @@ func TestAuthority_authorizeSSHRekey(t *testing.T) {
|
||||||
cert, signOpts, err := tc.auth.authorizeSSHRekey(context.Background(), tc.token)
|
cert, signOpts, err := tc.auth.authorizeSSHRekey(context.Background(), tc.token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -1459,6 +1459,37 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) {
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}))
|
}))
|
||||||
|
a4 := testAuthority(t)
|
||||||
|
a4.db = &db.MockAuthDB{
|
||||||
|
MUseToken: func(id, tok string) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
MGetCertificateData: func(serialNumber string) (*db.CertificateData, error) {
|
||||||
|
return &db.CertificateData{
|
||||||
|
Provisioner: &db.ProvisionerData{ID: "Max:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk", Name: "Max"},
|
||||||
|
RaInfo: &provisioner.RAInfo{ProvisionerName: "ra"},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
t4, c4 := generateX5cToken(a1, signer, jose.Claims{
|
||||||
|
Audience: []string{"https://ra.example.com/1.0/renew"},
|
||||||
|
Subject: "test.example.com",
|
||||||
|
Issuer: "step-ca-client/1.0",
|
||||||
|
NotBefore: jose.NewNumericDate(now),
|
||||||
|
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||||
|
}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {
|
||||||
|
cert.NotBefore = now
|
||||||
|
cert.NotAfter = now.Add(time.Hour)
|
||||||
|
b, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte("step-cli"), nil, nil})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{
|
||||||
|
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},
|
||||||
|
Value: b,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
badSigner, _ := generateX5cToken(a1, otherSigner, jose.Claims{
|
badSigner, _ := generateX5cToken(a1, otherSigner, jose.Claims{
|
||||||
Audience: []string{"https://example.com/1.0/renew"},
|
Audience: []string{"https://example.com/1.0/renew"},
|
||||||
Subject: "test.example.com",
|
Subject: "test.example.com",
|
||||||
|
@ -1627,6 +1658,7 @@ func TestAuthority_AuthorizeRenewToken(t *testing.T) {
|
||||||
{"ok", a1, args{ctx, t1}, c1, false},
|
{"ok", a1, args{ctx, t1}, c1, false},
|
||||||
{"ok expired cert", a1, args{ctx, t2}, c2, false},
|
{"ok expired cert", a1, args{ctx, t2}, c2, false},
|
||||||
{"ok provisioner issuer", a1, args{ctx, t3}, c3, false},
|
{"ok provisioner issuer", a1, args{ctx, t3}, c3, false},
|
||||||
|
{"ok ra provisioner", a4, args{ctx, t4}, c4, false},
|
||||||
{"fail token", a1, args{ctx, "not.a.token"}, nil, true},
|
{"fail token", a1, args{ctx, "not.a.token"}, nil, true},
|
||||||
{"fail token reuse", a1, args{ctx, t1}, nil, true},
|
{"fail token reuse", a1, args{ctx, t1}, nil, true},
|
||||||
{"fail token signature", a1, args{ctx, badSigner}, nil, true},
|
{"fail token signature", a1, args{ctx, badSigner}, nil, true},
|
||||||
|
|
|
@ -35,8 +35,13 @@ var (
|
||||||
// DefaultEnableSSHCA enable SSH CA features per provisioner or globally
|
// DefaultEnableSSHCA enable SSH CA features per provisioner or globally
|
||||||
// for all provisioners.
|
// for all provisioners.
|
||||||
DefaultEnableSSHCA = false
|
DefaultEnableSSHCA = false
|
||||||
// GlobalProvisionerClaims default claims for the Authority. Can be overridden
|
// DefaultCRLCacheDuration is the default cache duration for the CRL.
|
||||||
// by provisioner specific claims.
|
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{
|
GlobalProvisionerClaims = provisioner.Claims{
|
||||||
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
|
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
|
||||||
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||||
|
@ -72,7 +77,60 @@ type Config struct {
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Templates *templates.Templates `json:"templates,omitempty"`
|
Templates *templates.Templates `json:"templates,omitempty"`
|
||||||
CommonName string `json:"commonName,omitempty"`
|
CommonName string `json:"commonName,omitempty"`
|
||||||
|
CRL *CRLConfig `json:"crl,omitempty"`
|
||||||
SkipValidation bool `json:"-"`
|
SkipValidation bool `json:"-"`
|
||||||
|
|
||||||
|
// 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
|
// 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)
|
return nil, errors.Wrapf(err, "error parsing %s", filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store filename that was read to populate Config
|
||||||
|
c.loadedFromFilepath = filename
|
||||||
|
|
||||||
|
// initialize the Config
|
||||||
c.Init()
|
c.Init()
|
||||||
|
|
||||||
return &c, nil
|
return &c, nil
|
||||||
|
@ -183,6 +245,9 @@ func (c *Config) Init() {
|
||||||
if c.CommonName == "" {
|
if c.CommonName == "" {
|
||||||
c.CommonName = "Step Online CA"
|
c.CommonName = "Step Online CA"
|
||||||
}
|
}
|
||||||
|
if c.CRL != nil && c.CRL.Enabled && c.CRL.CacheDuration == nil {
|
||||||
|
c.CRL.CacheDuration = DefaultCRLCacheDuration
|
||||||
|
}
|
||||||
c.AuthorityConfig.init()
|
c.AuthorityConfig.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,6 +264,30 @@ func (c *Config) Save(filename string) error {
|
||||||
return errors.Wrapf(enc.Encode(c), "error writing %s", filename)
|
return errors.Wrapf(enc.Encode(c), "error writing %s", filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commit saves the current configuration to the same
|
||||||
|
// file it was initially loaded from.
|
||||||
|
//
|
||||||
|
// TODO(hs): rename Save() to WriteTo() and replace this
|
||||||
|
// with Save()? Or is Commit clear enough.
|
||||||
|
func (c *Config) Commit() error {
|
||||||
|
if !c.WasLoadedFromFile() {
|
||||||
|
return errors.New("cannot commit configuration if not loaded from file")
|
||||||
|
}
|
||||||
|
return c.Save(c.loadedFromFilepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WasLoadedFromFile returns whether or not the Config was
|
||||||
|
// loaded from a file.
|
||||||
|
func (c *Config) WasLoadedFromFile() bool {
|
||||||
|
return c.loadedFromFilepath != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filepath returns the path to the file the Config was
|
||||||
|
// loaded from.
|
||||||
|
func (c *Config) Filepath() string {
|
||||||
|
return c.loadedFromFilepath
|
||||||
|
}
|
||||||
|
|
||||||
// Validate validates the configuration.
|
// Validate validates the configuration.
|
||||||
func (c *Config) Validate() error {
|
func (c *Config) Validate() error {
|
||||||
switch {
|
switch {
|
||||||
|
@ -269,6 +358,11 @@ func (c *Config) Validate() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate crl config: nil is ok
|
||||||
|
if err := c.CRL.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return c.AuthorityConfig.Validate(c.GetAudiences())
|
return c.AuthorityConfig.Validate(c.GetAudiences())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -169,7 +169,7 @@ func (t *TLSOptions) TLSConfig() *tls.Config {
|
||||||
rs = tls.RenegotiateNever
|
rs = tls.RenegotiateNever
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:gosec // default MinVersion 1.2, if defined but empty 1.3 is used
|
//nolint:gosec // default MinVersion 1.2, if defined but empty 1.3 is used
|
||||||
return &tls.Config{
|
return &tls.Config{
|
||||||
CipherSuites: t.CipherSuites.Value(),
|
CipherSuites: t.CipherSuites.Value(),
|
||||||
MinVersion: t.MinVersion.Value(),
|
MinVersion: t.MinVersion.Value(),
|
||||||
|
|
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]),
|
PemCertificate: serializeCertificateChain(fullchain[0]),
|
||||||
PemCertificateChain: serializeCertificateChain(fullchain[1:]...),
|
PemCertificateChain: serializeCertificateChain(fullchain[1:]...),
|
||||||
Provisioner: createProvisionerIdentity(p),
|
Provisioner: createProvisionerIdentity(p),
|
||||||
|
AttestationData: createAttestationData(p),
|
||||||
RaProvisioner: raProvisioner,
|
RaProvisioner: raProvisioner,
|
||||||
EndpointId: endpointID,
|
EndpointId: endpointID,
|
||||||
})
|
})
|
||||||
|
@ -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) {
|
func createRegistrationAuthorityProvisioner(p provisioner.Interface) (*linkedca.RegistrationAuthorityProvisioner, string) {
|
||||||
if rap, ok := p.(raProvisioner); ok {
|
if rap, ok := p.(raProvisioner); ok {
|
||||||
info := rap.RAInfo()
|
if info := rap.RAInfo(); info != nil {
|
||||||
typ := linkedca.Provisioner_Type_value[strings.ToUpper(info.ProvisionerType)]
|
typ := linkedca.Provisioner_Type_value[strings.ToUpper(info.ProvisionerType)]
|
||||||
return &linkedca.RegistrationAuthorityProvisioner{
|
return &linkedca.RegistrationAuthorityProvisioner{
|
||||||
AuthorityId: info.AuthorityID,
|
AuthorityId: info.AuthorityID,
|
||||||
Provisioner: &linkedca.ProvisionerIdentity{
|
Provisioner: &linkedca.ProvisionerIdentity{
|
||||||
Id: info.ProvisionerID,
|
Id: info.ProvisionerID,
|
||||||
Type: linkedca.Provisioner_Type(typ),
|
Type: linkedca.Provisioner_Type(typ),
|
||||||
Name: info.ProvisionerName,
|
Name: info.ProvisionerName,
|
||||||
},
|
},
|
||||||
}, info.EndpointID
|
}, info.EndpointID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil, ""
|
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 {
|
func serializeCertificate(crt *x509.Certificate) string {
|
||||||
if crt == nil {
|
if crt == nil {
|
||||||
return ""
|
return ""
|
||||||
|
@ -461,7 +470,7 @@ func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
||||||
// nolint:gosec // used in bootstrap protocol
|
//nolint:gosec // used in bootstrap protocol
|
||||||
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
|
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
|
||||||
})))
|
})))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
@ -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
|
// WithGetIdentityFunc sets a custom function to retrieve the identity from
|
||||||
// an external resource.
|
// an external resource.
|
||||||
func WithGetIdentityFunc(fn func(ctx context.Context, p provisioner.Interface, email string) (*provisioner.Identity, error)) Option {
|
func WithGetIdentityFunc(fn func(ctx context.Context, p provisioner.Interface, email string) (*provisioner.Identity, error)) Option {
|
||||||
|
@ -151,16 +168,23 @@ func WithKeyManager(k kms.KeyManager) Option {
|
||||||
|
|
||||||
// WithX509Signer defines the signer used to sign X509 certificates.
|
// WithX509Signer defines the signer used to sign X509 certificates.
|
||||||
func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option {
|
func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option {
|
||||||
|
return WithX509SignerChain([]*x509.Certificate{crt}, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithX509SignerChain defines the signer used to sign X509 certificates. This
|
||||||
|
// option is similar to WithX509Signer but it supports a chain of intermediates.
|
||||||
|
func WithX509SignerChain(issuerChain []*x509.Certificate, s crypto.Signer) Option {
|
||||||
return func(a *Authority) error {
|
return func(a *Authority) error {
|
||||||
srv, err := cas.New(context.Background(), casapi.Options{
|
srv, err := cas.New(context.Background(), casapi.Options{
|
||||||
Type: casapi.SoftCAS,
|
Type: casapi.SoftCAS,
|
||||||
Signer: s,
|
Signer: s,
|
||||||
CertificateChain: []*x509.Certificate{crt},
|
CertificateChain: issuerChain,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
a.x509CAService = srv
|
a.x509CAService = srv
|
||||||
|
a.intermediateX509Certs = append(a.intermediateX509Certs, issuerChain...)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
// WithX509RootBundle is an option that allows to define the list of root
|
||||||
// certificates. This option will replace any root certificate defined before.
|
// certificates. This option will replace any root certificate defined before.
|
||||||
func WithX509RootBundle(pemCerts []byte) Option {
|
func WithX509RootBundle(pemCerts []byte) Option {
|
||||||
|
|
|
@ -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 {
|
func (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *linkedca.Admin, p *linkedca.Policy) error {
|
||||||
|
|
||||||
// no policy and thus nothing to evaluate; return early
|
// no policy and thus nothing to evaluate; return early
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -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 {
|
func (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string, p *linkedca.Policy) error {
|
||||||
|
|
||||||
// no policy and thus nothing to evaluate; return early
|
// no policy and thus nothing to evaluate; return early
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -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
|
// checkPolicy checks if a new or updated policy configuration results in the user
|
||||||
// locking themselves or other admins out of the CA.
|
// locking themselves or other admins out of the CA.
|
||||||
func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error {
|
func (a *Authority) checkPolicy(ctx context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error {
|
||||||
|
|
||||||
// convert the policy; return early if nil
|
// convert the policy; return early if nil
|
||||||
policyOptions := authPolicy.LinkedToCertificates(p)
|
policyOptions := authPolicy.LinkedToCertificates(p)
|
||||||
if policyOptions == nil {
|
if policyOptions == nil {
|
||||||
|
@ -216,7 +213,6 @@ func (a *Authority) reloadPolicyEngines(ctx context.Context) error {
|
||||||
)
|
)
|
||||||
|
|
||||||
if a.config.AuthorityConfig.EnableAdmin {
|
if a.config.AuthorityConfig.EnableAdmin {
|
||||||
|
|
||||||
// temporarily disable policy loading when LinkedCA is in use
|
// temporarily disable policy loading when LinkedCA is in use
|
||||||
if _, ok := a.adminDB.(*linkedCaClient); ok {
|
if _, ok := a.adminDB.(*linkedCaClient); ok {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -17,9 +17,9 @@ type Engine struct {
|
||||||
|
|
||||||
// New returns a new Engine using Options.
|
// New returns a new Engine using Options.
|
||||||
func New(options *Options) (*Engine, error) {
|
func New(options *Options) (*Engine, error) {
|
||||||
|
|
||||||
// if no options provided, return early
|
// if no options provided, return early
|
||||||
if options == nil {
|
if options == nil {
|
||||||
|
//nolint:nilnil // legacy
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,6 @@ func New(options *Options) (*Engine, error) {
|
||||||
// the X.509 policy (if available) and returns an error if one of the
|
// the X.509 policy (if available) and returns an error if one of the
|
||||||
// names in the certificate is not allowed.
|
// names in the certificate is not allowed.
|
||||||
func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {
|
func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {
|
||||||
|
|
||||||
// return early if there's no policy to evaluate
|
// return early if there's no policy to evaluate
|
||||||
if e == nil || e.x509Policy == nil {
|
if e == nil || e.x509Policy == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -69,7 +68,6 @@ func (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {
|
||||||
// AreSANsAllowed evaluates the slice of SANs against the X.509 policy
|
// AreSANsAllowed evaluates the slice of SANs against the X.509 policy
|
||||||
// (if available) and returns an error if one of the SANs is not allowed.
|
// (if available) and returns an error if one of the SANs is not allowed.
|
||||||
func (e *Engine) AreSANsAllowed(sans []string) error {
|
func (e *Engine) AreSANsAllowed(sans []string) error {
|
||||||
|
|
||||||
// return early if there's no policy to evaluate
|
// return early if there's no policy to evaluate
|
||||||
if e == nil || e.x509Policy == nil {
|
if e == nil || e.x509Policy == nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -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
|
// user or host policy (if configured) and returns an error if one of the
|
||||||
// principals in the certificate is not allowed.
|
// principals in the certificate is not allowed.
|
||||||
func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
|
func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
|
||||||
|
|
||||||
// return early if there's no policy to evaluate
|
// return early if there's no policy to evaluate
|
||||||
if e == nil || (e.sshHostPolicy == nil && e.sshUserPolicy == nil) {
|
if e == nil || (e.sshHostPolicy == nil && e.sshUserPolicy == nil) {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -19,7 +19,6 @@ type HostPolicy policy.SSHNamePolicyEngine
|
||||||
|
|
||||||
// NewX509PolicyEngine creates a new x509 name policy engine
|
// NewX509PolicyEngine creates a new x509 name policy engine
|
||||||
func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, error) {
|
func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, error) {
|
||||||
|
|
||||||
// return early if no policy engine options to configure
|
// return early if no policy engine options to configure
|
||||||
if policyOptions == nil {
|
if policyOptions == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -92,7 +91,6 @@ func NewSSHHostPolicyEngine(policyOptions SSHPolicyOptionsInterface) (HostPolicy
|
||||||
|
|
||||||
// newSSHPolicyEngine creates a new SSH name policy engine
|
// newSSHPolicyEngine creates a new SSH name policy engine
|
||||||
func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) {
|
func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) {
|
||||||
|
|
||||||
// return early if no policy engine options to configure
|
// return early if no policy engine options to configure
|
||||||
if policyOptions == nil {
|
if policyOptions == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -143,7 +141,6 @@ func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEn
|
||||||
}
|
}
|
||||||
|
|
||||||
func LinkedToCertificates(p *linkedca.Policy) *Options {
|
func LinkedToCertificates(p *linkedca.Policy) *Options {
|
||||||
|
|
||||||
// return early
|
// return early
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -185,11 +185,11 @@ func TestAuthority_checkPolicy(t *testing.T) {
|
||||||
} else {
|
} else {
|
||||||
assert.IsType(t, &PolicyError{}, err)
|
assert.IsType(t, &PolicyError{}, err)
|
||||||
|
|
||||||
pe, ok := err.(*PolicyError)
|
var pe *PolicyError
|
||||||
assert.True(t, ok)
|
if assert.True(t, errors.As(err, &pe)) {
|
||||||
|
assert.Equal(t, tc.err.Typ, pe.Typ)
|
||||||
assert.Equal(t, tc.err.Typ, pe.Typ)
|
assert.Equal(t, tc.err.Error(), pe.Error())
|
||||||
assert.Equal(t, tc.err.Error(), pe.Error())
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1179,10 +1179,11 @@ func TestAuthority_RemoveAuthorityPolicy(t *testing.T) {
|
||||||
}
|
}
|
||||||
err := a.RemoveAuthorityPolicy(tt.args.ctx)
|
err := a.RemoveAuthorityPolicy(tt.args.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pe, ok := err.(*PolicyError)
|
var pe *PolicyError
|
||||||
assert.True(t, ok)
|
if assert.True(t, errors.As(err, &pe)) {
|
||||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1250,10 +1251,11 @@ func TestAuthority_GetAuthorityPolicy(t *testing.T) {
|
||||||
}
|
}
|
||||||
got, err := a.GetAuthorityPolicy(tt.args.ctx)
|
got, err := a.GetAuthorityPolicy(tt.args.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pe, ok := err.(*PolicyError)
|
var pe *PolicyError
|
||||||
assert.True(t, ok)
|
if assert.True(t, errors.As(err, &pe)) {
|
||||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
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)
|
got, err := a.CreateAuthorityPolicy(tt.args.ctx, tt.args.adm, tt.args.p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pe, ok := err.(*PolicyError)
|
var pe *PolicyError
|
||||||
assert.True(t, ok)
|
if assert.True(t, errors.As(err, &pe)) {
|
||||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
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)
|
got, err := a.UpdateAuthorityPolicy(tt.args.ctx, tt.args.adm, tt.args.p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pe, ok := err.(*PolicyError)
|
var pe *PolicyError
|
||||||
assert.True(t, ok)
|
if assert.True(t, errors.As(err, &pe)) {
|
||||||
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
assert.Equal(t, tt.wantErr.Typ, pe.Typ)
|
||||||
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
assert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
|
|
@ -10,12 +10,13 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ACMEChallenge represents the supported acme challenges.
|
// ACMEChallenge represents the supported acme challenges.
|
||||||
type ACMEChallenge string
|
type ACMEChallenge string
|
||||||
|
|
||||||
// nolint:revive // better names
|
//nolint:stylecheck,revive // better names
|
||||||
const (
|
const (
|
||||||
// HTTP_01 is the http-01 ACME challenge.
|
// HTTP_01 is the http-01 ACME challenge.
|
||||||
HTTP_01 ACMEChallenge = "http-01"
|
HTTP_01 ACMEChallenge = "http-01"
|
||||||
|
@ -83,6 +84,17 @@ type ACME struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ForceCN bool `json:"forceCN,omitempty"`
|
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
|
// RequireEAB makes the provisioner require ACME EAB to be provided
|
||||||
// by clients when creating a new Account. If set to true, the 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
|
// 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
|
// AuthorizeOrderIdentifier verifies the provisioner is allowed to issue a
|
||||||
// certificate for an ACME Order Identifier.
|
// certificate for an ACME Order Identifier.
|
||||||
func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIdentifier) error {
|
func (p *ACME) AuthorizeOrderIdentifier(ctx context.Context, identifier ACMEIdentifier) error {
|
||||||
|
|
||||||
x509Policy := p.ctl.getPolicy().getX509()
|
x509Policy := p.ctl.getPolicy().getX509()
|
||||||
|
|
||||||
// identifier is allowed if no policy is configured
|
// identifier is allowed if no policy is configured
|
||||||
|
@ -253,22 +264,22 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
|
p.ctl.newWebhookController(nil, linkedca.Webhook_X509),
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts, nil
|
return opts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeRevoke is called just before the certificate is to be revoked by
|
// 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
|
// the CA. It can be used to authorize revocation of a certificate. With the
|
||||||
// currently is a no-op.
|
// ACME protocol, revocation authorization is specified and performed as part
|
||||||
// TODO(hs): add configuration option that toggles revocation? Or change function signature to make it more useful?
|
// of the client/server interaction, so this is a no-op.
|
||||||
// Or move certain logic out of the Revoke API to here? Would likely involve some more stuff in the ctx.
|
|
||||||
func (p *ACME) AuthorizeRevoke(ctx context.Context, token string) error {
|
func (p *ACME) AuthorizeRevoke(ctx context.Context, token string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeRenew returns an error if the renewal is disabled.
|
// 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
|
// revocation status. Just confirms that the provisioner that created the
|
||||||
// certificate was configured to allow renewals.
|
// certificate was configured to allow renewals.
|
||||||
func (p *ACME) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
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
|
package provisioner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -266,7 +269,7 @@ func TestACME_AuthorizeSign(t *testing.T) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) && assert.NotNil(t, opts) {
|
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 {
|
for _, o := range opts {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *ACME:
|
case *ACME:
|
||||||
|
@ -285,6 +288,8 @@ func TestACME_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
||||||
case *x509NamePolicyValidator:
|
case *x509NamePolicyValidator:
|
||||||
assert.Equals(t, nil, v.policyEngine)
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
|
case *WebhookController:
|
||||||
|
assert.Len(t, 0, v.webhooks)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
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/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
"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"
|
const awsSignatureURL = "http://169.254.169.254/latest/dynamic/instance-identity/signature"
|
||||||
|
|
||||||
// awsAPITokenURL is the url used to get the IMDSv2 API token
|
// 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" //nolint:gosec // no credentials here
|
||||||
const awsAPITokenURL = "http://169.254.169.254/latest/api/token"
|
|
||||||
|
|
||||||
// awsAPITokenTTL is the default TTL to use when requesting IMDSv2 API tokens
|
// 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()
|
// -- we keep this short-lived since we get a new token with every call to readURL()
|
||||||
const awsAPITokenTTL = "30"
|
const awsAPITokenTTL = "30"
|
||||||
|
|
||||||
// awsMetadataTokenHeader is the header that must be passed with every IMDSv2 request
|
// awsMetadataTokenHeader is the header that must be passed with every IMDSv2 request
|
||||||
// nolint:gosec // no credentials here
|
const awsMetadataTokenHeader = "X-aws-ec2-metadata-token" //nolint:gosec // no credentials here
|
||||||
const awsMetadataTokenHeader = "X-aws-ec2-metadata-token"
|
|
||||||
|
|
||||||
// awsMetadataTokenTTLHeader is the header used to indicate the token TTL requested
|
// 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" //nolint:gosec // no credentials here
|
||||||
const awsMetadataTokenTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds"
|
|
||||||
|
|
||||||
// awsCertificate is the certificate used to validate the instance identity
|
// awsCertificate is the certificate used to validate the instance identity
|
||||||
// signature.
|
// signature.
|
||||||
|
@ -487,6 +485,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
commonNameValidator(payload.Claims.Subject),
|
commonNameValidator(payload.Claims.Subject),
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -768,5 +767,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
// Ensure that all principal names are allowed
|
// Ensure that all principal names are allowed
|
||||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||||
|
// Call webhooks
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -522,8 +522,8 @@ func TestAWS_authorizeToken(t *testing.T) {
|
||||||
tc := tt(t)
|
tc := tt(t)
|
||||||
if claims, err := tc.p.authorizeToken(tc.token); err != nil {
|
if claims, err := tc.p.authorizeToken(tc.token); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -642,11 +642,11 @@ func TestAWS_AuthorizeSign(t *testing.T) {
|
||||||
code int
|
code int
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"ok", p1, args{t1, "foo.local"}, 8, http.StatusOK, false},
|
{"ok", p1, args{t1, "foo.local"}, 9, http.StatusOK, false},
|
||||||
{"ok", p2, args{t2, "instance-id"}, 12, 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"}, 12, 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"}, 12, http.StatusOK, false},
|
{"ok", p2, args{t2PrivateIP, "127.0.0.1"}, 13, http.StatusOK, false},
|
||||||
{"ok", p1, args{t4, "instance-id"}, 8, http.StatusOK, false},
|
{"ok", p1, args{t4, "instance-id"}, 9, http.StatusOK, false},
|
||||||
{"fail account", p3, args{token: t3}, 0, http.StatusUnauthorized, true},
|
{"fail account", p3, args{token: t3}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail token", p1, args{token: "token"}, 0, http.StatusUnauthorized, true},
|
{"fail token", p1, args{token: "token"}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail subject", p1, args{token: failSubject}, 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)
|
t.Errorf("AWS.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
case err != nil:
|
case err != nil:
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
default:
|
default:
|
||||||
assert.Equals(t, tt.wantLen, len(got))
|
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"})
|
assert.Equals(t, []string(v), []string{"ip-127-0-0-1.us-west-1.compute.internal"})
|
||||||
case *x509NamePolicyValidator:
|
case *x509NamePolicyValidator:
|
||||||
assert.Equals(t, nil, v.policyEngine)
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
|
case *WebhookController:
|
||||||
|
assert.Len(t, 0, v.webhooks)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
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
|
pub := key.Public().Key
|
||||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
assert.FatalError(t, err)
|
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)
|
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
@ -807,8 +809,8 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
} else if assert.NotNil(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 {
|
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)
|
t.Errorf("AWS.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
@ -24,8 +25,7 @@ import (
|
||||||
// azureOIDCBaseURL is the base discovery url for Microsoft Azure tokens.
|
// azureOIDCBaseURL is the base discovery url for Microsoft Azure tokens.
|
||||||
const azureOIDCBaseURL = "https://login.microsoftonline.com"
|
const azureOIDCBaseURL = "https://login.microsoftonline.com"
|
||||||
|
|
||||||
// azureIdentityTokenURL is the URL to get the identity token for an instance.
|
//nolint:gosec // azureIdentityTokenURL is the URL to get the identity token for an instance.
|
||||||
// nolint:gosec // no credentials here
|
|
||||||
const azureIdentityTokenURL = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F"
|
const azureIdentityTokenURL = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F"
|
||||||
|
|
||||||
// azureDefaultAudience is the default audience used.
|
// azureDefaultAudience is the default audience used.
|
||||||
|
@ -364,6 +364,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,6 +433,8 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
// Ensure that all principal names are allowed
|
// Ensure that all principal names are allowed
|
||||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||||
|
// Call webhooks
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -336,8 +336,8 @@ func TestAzure_authorizeToken(t *testing.T) {
|
||||||
tc := tt(t)
|
tc := tt(t)
|
||||||
if claims, name, group, subscriptionID, objectID, err := tc.p.authorizeToken(tc.token); err != nil {
|
if claims, name, group, subscriptionID, objectID, err := tc.p.authorizeToken(tc.token); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -474,11 +474,11 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
||||||
code int
|
code int
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"ok", p1, args{t1}, 7, http.StatusOK, false},
|
{"ok", p1, args{t1}, 8, http.StatusOK, false},
|
||||||
{"ok", p2, args{t2}, 12, http.StatusOK, false},
|
{"ok", p2, args{t2}, 13, http.StatusOK, false},
|
||||||
{"ok", p1, args{t11}, 7, http.StatusOK, false},
|
{"ok", p1, args{t11}, 8, http.StatusOK, false},
|
||||||
{"ok", p5, args{t5}, 7, http.StatusOK, false},
|
{"ok", p5, args{t5}, 8, http.StatusOK, false},
|
||||||
{"ok", p7, args{t7}, 7, http.StatusOK, false},
|
{"ok", p7, args{t7}, 8, http.StatusOK, false},
|
||||||
{"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true},
|
{"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true},
|
{"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail subscription", p6, args{t6}, 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)
|
t.Errorf("Azure.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
case err != nil:
|
case err != nil:
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
default:
|
default:
|
||||||
assert.Equals(t, tt.wantLen, len(got))
|
assert.Equals(t, tt.wantLen, len(got))
|
||||||
|
@ -530,6 +530,8 @@ func TestAzure_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, []string(v), []string{"virtualMachine"})
|
assert.Equals(t, []string(v), []string{"virtualMachine"})
|
||||||
case *x509NamePolicyValidator:
|
case *x509NamePolicyValidator:
|
||||||
assert.Equals(t, nil, v.policyEngine)
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
|
case *WebhookController:
|
||||||
|
assert.Len(t, 0, v.webhooks)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
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 {
|
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)
|
t.Errorf("Azure.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -624,7 +626,7 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) {
|
||||||
pub := key.Public().Key
|
pub := key.Public().Key
|
||||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
assert.FatalError(t, err)
|
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)
|
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
@ -673,8 +675,8 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
} else if assert.NotNil(t, got) {
|
} else if assert.NotNil(t, got) {
|
||||||
|
|
|
@ -38,7 +38,8 @@ type Claimer struct {
|
||||||
// NewClaimer initializes a new claimer with the given claims.
|
// NewClaimer initializes a new claimer with the given claims.
|
||||||
func NewClaimer(claims *Claims, global Claims) (*Claimer, error) {
|
func NewClaimer(claims *Claims, global Claims) (*Claimer, error) {
|
||||||
c := &Claimer{global: global, claims: claims}
|
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.
|
// Claims returns the merge of the inner and global claims.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package provisioner
|
package provisioner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1" // nolint:gosec // not used for cryptographic security
|
"crypto/sha1" //nolint:gosec // not used for cryptographic security
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"encoding/binary"
|
"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
|
// provisionerSum returns the SHA1 of the provisioners ID. From this we will
|
||||||
// create the unique and sorted id.
|
// create the unique and sorted id.
|
||||||
func provisionerSum(p Interface) []byte {
|
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()))
|
sum := sha1.Sum([]byte(p.GetID()))
|
||||||
return sum[:]
|
return sum[:]
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,6 +24,8 @@ type Controller struct {
|
||||||
AuthorizeRenewFunc AuthorizeRenewFunc
|
AuthorizeRenewFunc AuthorizeRenewFunc
|
||||||
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
|
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
|
||||||
policy *policyEngine
|
policy *policyEngine
|
||||||
|
webhookClient *http.Client
|
||||||
|
webhooks []*Webhook
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewController initializes a new provisioner controller.
|
// NewController initializes a new provisioner controller.
|
||||||
|
@ -43,6 +46,8 @@ func NewController(p Interface, claims *Claims, config Config, options *Options)
|
||||||
AuthorizeRenewFunc: config.AuthorizeRenewFunc,
|
AuthorizeRenewFunc: config.AuthorizeRenewFunc,
|
||||||
AuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc,
|
AuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc,
|
||||||
policy: policy,
|
policy: policy,
|
||||||
|
webhookClient: config.WebhookClient,
|
||||||
|
webhooks: options.GetWebhooks(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +77,19 @@ func (c *Controller) AuthorizeSSHRenew(ctx context.Context, cert *ssh.Certificat
|
||||||
return DefaultAuthorizeSSHRenew(ctx, c, cert)
|
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
|
// Identity is the type representing an externally supplied identity that is used
|
||||||
// by provisioners to populate certificate fields.
|
// by provisioners to populate certificate fields.
|
||||||
type Identity struct {
|
type Identity struct {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/x509util"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/authority/policy"
|
"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/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
@ -102,7 +103,6 @@ func (p *GCP) GetID() string {
|
||||||
return p.ID
|
return p.ID
|
||||||
}
|
}
|
||||||
return p.GetIDForToken()
|
return p.GetIDForToken()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIDForToken returns an identifier that will be used to load the provisioner
|
// 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{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,5 +439,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
// Ensure that all principal names are allowed
|
// Ensure that all principal names are allowed
|
||||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||||
|
// Call webhooks
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -391,8 +391,8 @@ func TestGCP_authorizeToken(t *testing.T) {
|
||||||
tc := tt(t)
|
tc := tt(t)
|
||||||
if claims, err := tc.p.authorizeToken(tc.token); err != nil {
|
if claims, err := tc.p.authorizeToken(tc.token); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -516,9 +516,9 @@ func TestGCP_AuthorizeSign(t *testing.T) {
|
||||||
code int
|
code int
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{"ok", p1, args{t1}, 7, http.StatusOK, false},
|
{"ok", p1, args{t1}, 8, http.StatusOK, false},
|
||||||
{"ok", p2, args{t2}, 12, http.StatusOK, false},
|
{"ok", p2, args{t2}, 13, http.StatusOK, false},
|
||||||
{"ok", p3, args{t3}, 7, http.StatusOK, false},
|
{"ok", p3, args{t3}, 8, http.StatusOK, false},
|
||||||
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
|
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true},
|
{"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true},
|
||||||
{"fail iss", p1, args{failIss}, 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)
|
t.Errorf("GCP.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
case err != nil:
|
case err != nil:
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
default:
|
default:
|
||||||
assert.Equals(t, tt.wantLen, len(got))
|
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"})
|
assert.Equals(t, []string(v), []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"})
|
||||||
case *x509NamePolicyValidator:
|
case *x509NamePolicyValidator:
|
||||||
assert.Equals(t, nil, v.policyEngine)
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
|
case *WebhookController:
|
||||||
|
assert.Len(t, 0, v.webhooks)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
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
|
pub := key.Public().Key
|
||||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
assert.FatalError(t, err)
|
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)
|
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
@ -682,8 +684,8 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
} else if assert.NotNil(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 {
|
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)
|
t.Errorf("GCP.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
@ -194,6 +195,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
|
||||||
defaultSANsValidator(claims.SANs),
|
defaultSANsValidator(claims.SANs),
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,6 +280,8 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
// Ensure that all principal names are allowed
|
// Ensure that all principal names are allowed
|
||||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||||
|
// Call webhooks
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -185,8 +185,8 @@ func TestJWK_authorizeToken(t *testing.T) {
|
||||||
t.Run(tt.name, func(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 got, err := tt.prov.authorizeToken(tt.args.token, testAudiences.Sign); err != nil {
|
||||||
if assert.NotNil(t, tt.err) {
|
if assert.NotNil(t, tt.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
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) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if err := tt.prov.AuthorizeRevoke(context.Background(), tt.args.token); err != nil {
|
if err := tt.prov.AuthorizeRevoke(context.Background(), tt.args.token); err != nil {
|
||||||
if assert.NotNil(t, tt.err) {
|
if assert.NotNil(t, tt.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -290,14 +290,14 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
||||||
ctx := NewContextWithMethod(context.Background(), SignMethod)
|
ctx := NewContextWithMethod(context.Background(), SignMethod)
|
||||||
if got, err := tt.prov.AuthorizeSign(ctx, tt.args.token); err != nil {
|
if got, err := tt.prov.AuthorizeSign(ctx, tt.args.token); err != nil {
|
||||||
if assert.NotNil(t, tt.err) {
|
if assert.NotNil(t, tt.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
assert.HasPrefix(t, err.Error(), tt.err.Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.NotNil(t, got) {
|
if assert.NotNil(t, got) {
|
||||||
assert.Equals(t, 9, len(got))
|
assert.Equals(t, 10, len(got))
|
||||||
for _, o := range got {
|
for _, o := range got {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *JWK:
|
case *JWK:
|
||||||
|
@ -319,6 +319,7 @@ func TestJWK_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, []string(v), tt.sans)
|
assert.Equals(t, []string(v), tt.sans)
|
||||||
case *x509NamePolicyValidator:
|
case *x509NamePolicyValidator:
|
||||||
assert.Equals(t, nil, v.policyEngine)
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
|
case *WebhookController:
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
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 {
|
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)
|
t.Errorf("JWK.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -411,7 +412,7 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) {
|
||||||
pub := key.Public().Key
|
pub := key.Public().Key
|
||||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
assert.FatalError(t, err)
|
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)
|
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
@ -461,8 +462,8 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
} else if assert.NotNil(t, got) {
|
} else if assert.NotNil(t, got) {
|
||||||
|
@ -626,8 +627,8 @@ func TestJWK_AuthorizeSSHRevoke(t *testing.T) {
|
||||||
tc := tt(t)
|
tc := tt(t)
|
||||||
if err := tc.p.AuthorizeSSHRevoke(context.Background(), tc.token); err != nil {
|
if err := tc.p.AuthorizeSSHRevoke(context.Background(), tc.token); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"go.step.sm/crypto/pemutil"
|
"go.step.sm/crypto/pemutil"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
"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.
|
// Init initializes and validates the fields of a K8sSA type.
|
||||||
func (p *K8sSA) Init(config Config) (err error) {
|
func (p *K8sSA) Init(config Config) (err error) {
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case p.Type == "":
|
case p.Type == "":
|
||||||
return errors.New("provisioner type cannot be empty")
|
return errors.New("provisioner type cannot be empty")
|
||||||
|
@ -243,6 +243,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,6 +289,8 @@ func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
// Ensure that all principal names are allowed
|
// Ensure that all principal names are allowed
|
||||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||||
|
// Call webhooks
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,8 +118,8 @@ func TestK8sSA_authorizeToken(t *testing.T) {
|
||||||
tc := tt(t)
|
tc := tt(t)
|
||||||
if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {
|
if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -167,8 +167,8 @@ func TestK8sSA_AuthorizeRevoke(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
tc := tt(t)
|
tc := tt(t)
|
||||||
if err := tc.p.AuthorizeRevoke(context.Background(), tc.token); err != nil {
|
if err := tc.p.AuthorizeRevoke(context.Background(), tc.token); err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
|
@ -223,8 +223,8 @@ func TestK8sSA_AuthorizeRenew(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
tc := tt(t)
|
tc := tt(t)
|
||||||
if err := tc.p.AuthorizeRenew(context.Background(), tc.cert); err != nil {
|
if err := tc.p.AuthorizeRenew(context.Background(), tc.cert); err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
|
@ -272,8 +272,8 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
|
||||||
tc := tt(t)
|
tc := tt(t)
|
||||||
if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil {
|
if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
|
@ -297,11 +297,13 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
||||||
case *x509NamePolicyValidator:
|
case *x509NamePolicyValidator:
|
||||||
assert.Equals(t, nil, v.policyEngine)
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
|
case *WebhookController:
|
||||||
|
assert.Len(t, 0, v.webhooks)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
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)
|
tc := tt(t)
|
||||||
if opts, err := tc.p.AuthorizeSSHSign(context.Background(), tc.token); err != nil {
|
if opts, err := tc.p.AuthorizeSSHSign(context.Background(), tc.token); err != nil {
|
||||||
if assert.NotNil(t, tc.err) {
|
if assert.NotNil(t, tc.err) {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tc.code)
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
||||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if assert.Nil(t, tc.err) {
|
if assert.Nil(t, tc.err) {
|
||||||
if assert.NotNil(t, opts) {
|
if assert.NotNil(t, opts) {
|
||||||
assert.Len(t, 8, opts)
|
assert.Len(t, 9, opts)
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case Interface:
|
case Interface:
|
||||||
|
@ -384,6 +386,8 @@ func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
|
||||||
case *sshNamePolicyValidator:
|
case *sshNamePolicyValidator:
|
||||||
assert.Equals(t, nil, v.userPolicyEngine)
|
assert.Equals(t, nil, v.userPolicyEngine)
|
||||||
assert.Equals(t, nil, v.hostPolicyEngine)
|
assert.Equals(t, nil, v.hostPolicyEngine)
|
||||||
|
case *WebhookController:
|
||||||
|
assert.Len(t, 0, v.webhooks)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
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
|
// 0 it will randomly rotate between 0-12 hours, but every time we call to Get
|
||||||
// it will automatically rotate.
|
// it will automatically rotate.
|
||||||
func (ks *keyStore) nextReloadDuration(age time.Duration) time.Duration {
|
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)
|
age -= time.Duration(n)
|
||||||
return abs(age)
|
return abs(age)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeysFromJWKsURI(uri string) (jose.JSONWebKeySet, time.Duration, error) {
|
func getKeysFromJWKsURI(uri string) (jose.JSONWebKeySet, time.Duration, error) {
|
||||||
var keys jose.JSONWebKeySet
|
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 {
|
if err != nil {
|
||||||
return keys, 0, errors.Wrapf(err, "failed to connect to %s", uri)
|
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/sshutil"
|
||||||
"go.step.sm/crypto/x25519"
|
"go.step.sm/crypto/x25519"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
|
@ -164,6 +165,7 @@ func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,6 +264,8 @@ func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOpti
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
// Ensure that all principal names are allowed
|
// Ensure that all principal names are allowed
|
||||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
|
||||||
|
// Call webhooks
|
||||||
|
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||||
), nil
|
), 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) {
|
func (p *noop) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
|
||||||
|
//nolint:nilnil // fine for noop
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
)
|
)
|
||||||
|
@ -356,6 +357,8 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(o.ctl.Claimer.MinTLSCertDuration(), o.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(o.ctl.Claimer.MinTLSCertDuration(), o.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
newX509NamePolicyValidator(o.ctl.getPolicy().getX509()),
|
newX509NamePolicyValidator(o.ctl.getPolicy().getX509()),
|
||||||
|
// webhooks
|
||||||
|
o.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -460,6 +463,8 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
|
||||||
&sshCertDefaultValidator{},
|
&sshCertDefaultValidator{},
|
||||||
// Ensure that all principal names are allowed
|
// Ensure that all principal names are allowed
|
||||||
newSSHNamePolicyValidator(o.ctl.getPolicy().getSSHHost(), o.ctl.getPolicy().getSSHUser()),
|
newSSHNamePolicyValidator(o.ctl.getPolicy().getSSHHost(), o.ctl.getPolicy().getSSHUser()),
|
||||||
|
// Call webhooks
|
||||||
|
o.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,7 +484,7 @@ func (o *OIDC) AuthorizeSSHRevoke(ctx context.Context, token string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAndDecode(uri string, v interface{}) 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 {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to connect to %s", uri)
|
return errors.Wrapf(err, "failed to connect to %s", uri)
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,8 +247,8 @@ func TestOIDC_authorizeToken(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
} else {
|
} else {
|
||||||
|
@ -318,12 +318,12 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
} else if assert.NotNil(t, got) {
|
} else if assert.NotNil(t, got) {
|
||||||
assert.Equals(t, 7, len(got))
|
assert.Equals(t, 8, len(got))
|
||||||
for _, o := range got {
|
for _, o := range got {
|
||||||
switch v := o.(type) {
|
switch v := o.(type) {
|
||||||
case *OIDC:
|
case *OIDC:
|
||||||
|
@ -343,6 +343,8 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, string(v), "name@smallstep.com")
|
assert.Equals(t, string(v), "name@smallstep.com")
|
||||||
case *x509NamePolicyValidator:
|
case *x509NamePolicyValidator:
|
||||||
assert.Equals(t, nil, v.policyEngine)
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
|
case *WebhookController:
|
||||||
|
assert.Len(t, 0, v.webhooks)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
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)
|
t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -452,8 +454,8 @@ func TestOIDC_AuthorizeRenew(t *testing.T) {
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("OIDC.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("OIDC.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -540,7 +542,7 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
|
||||||
pub := key.Public().Key
|
pub := key.Public().Key
|
||||||
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
assert.FatalError(t, err)
|
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)
|
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
@ -614,8 +616,8 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
assert.Nil(t, got)
|
assert.Nil(t, got)
|
||||||
} else if assert.NotNil(t, got) {
|
} else if assert.NotNil(t, got) {
|
||||||
|
@ -682,8 +684,8 @@ func TestOIDC_AuthorizeSSHRevoke(t *testing.T) {
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("OIDC.AuthorizeSSHRevoke() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("OIDC.AuthorizeSSHRevoke() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
sc, ok := err.(render.StatusCodedError)
|
var sc render.StatusCodedError
|
||||||
assert.Fatal(t, ok, "error does not implement StatusCodedError interface")
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
||||||
assert.Equals(t, sc.StatusCode(), tt.code)
|
assert.Equals(t, sc.StatusCode(), tt.code)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,6 +29,9 @@ func (fn certificateOptionsFunc) Options(so SignOptions) []x509util.Option {
|
||||||
type Options struct {
|
type Options struct {
|
||||||
X509 *X509Options `json:"x509,omitempty"`
|
X509 *X509Options `json:"x509,omitempty"`
|
||||||
SSH *SSHOptions `json:"ssh,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.
|
// GetX509Options returns the X.509 options.
|
||||||
|
@ -47,6 +50,14 @@ func (o *Options) GetSSHOptions() *SSHOptions {
|
||||||
return o.SSH
|
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.
|
// X509Options contains specific options for X.509 certificates.
|
||||||
type X509Options struct {
|
type X509Options struct {
|
||||||
// Template contains a X.509 certificate template. It can be a JSON template
|
// 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