forked from TrueCloudLab/certificates
Compare commits
15 commits
master
...
seb/ct-loc
Author | SHA1 | Date | |
---|---|---|---|
|
50a88d3265 | ||
|
600b1db302 | ||
|
3939e85526 | ||
|
4fef188a3a | ||
|
f47e356ba3 | ||
|
4b4b0d6202 | ||
|
c2908ef888 | ||
|
16c88c6bf1 | ||
|
b9104a92f9 | ||
|
dff3f6f270 | ||
|
963fe0fa91 | ||
|
b766f49995 | ||
|
7012500aac | ||
|
7b175004cb | ||
|
19c4842cdf |
518 changed files with 9948 additions and 118787 deletions
1
.VERSION
1
.VERSION
|
@ -1 +0,0 @@
|
|||
$Format:%d$
|
|
@ -1,5 +0,0 @@
|
|||
bin
|
||||
coverage.txt
|
||||
*.test
|
||||
*.out
|
||||
.travis-releases
|
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1 +0,0 @@
|
|||
.VERSION export-subst
|
28
.github/ISSUE_TEMPLATE/autocert_bug.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/autocert_bug.md
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
name: Autocert Bug
|
||||
about: Report a bug you found in autocert
|
||||
labels: area/autocert bug
|
||||
---
|
||||
|
||||
### Subject of the issue
|
||||
Describe your issue here
|
||||
|
||||
### Environment
|
||||
* Kubernetes version:
|
||||
* Cloud provider or hardware configuration:
|
||||
* OS (e.g., from /etc/os-release):
|
||||
* Kernel (e.g., `uname -a`):
|
||||
* Install tools:
|
||||
* Other:
|
||||
|
||||
### Steps to reproduce
|
||||
Tell us how to reproduce this issue
|
||||
|
||||
### Expected behaviour
|
||||
Tell us what should happen
|
||||
|
||||
### Actual behaviour
|
||||
Tell us what happens instead
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here
|
11
.github/ISSUE_TEMPLATE/autocert_enhancement.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/autocert_enhancement.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
name: Autocert Enhancement
|
||||
about: Suggest an enhancement to autocert
|
||||
labels: area/autocert enhancement
|
||||
---
|
||||
|
||||
### What would you like to be added
|
||||
|
||||
|
||||
### Why this is needed
|
||||
|
56
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
56
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
|
@ -1,56 +0,0 @@
|
|||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "needs triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Tell us how to reproduce this issue.
|
||||
placeholder: These are the steps!
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: your-env
|
||||
attributes:
|
||||
label: Your Environment
|
||||
value: |-
|
||||
* OS -
|
||||
* `step-ca` Version -
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: What happens instead?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: contributing
|
||||
attributes:
|
||||
label: Contributing
|
||||
value: |
|
||||
Vote on this issue by adding a 👍 reaction.
|
||||
To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already).
|
||||
validations:
|
||||
required: false
|
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
### Subject of the issue
|
||||
Describe your issue here.
|
||||
|
||||
### Your environment
|
||||
* OS -
|
||||
* Version -
|
||||
|
||||
### Steps to reproduce
|
||||
Tell us how to reproduce this issue. Please provide a working demo, you can use [this template](https://plnkr.co/edit/XorWgI?p=preview) as a base.
|
||||
|
||||
### Expected behaviour
|
||||
Tell us what should happen
|
||||
|
||||
### Actual behaviour
|
||||
Tell us what happens instead
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
9
.github/ISSUE_TEMPLATE/config.yml
vendored
9
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,9 +0,0 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Ask on Discord
|
||||
url: https://discord.gg/7xgjhVAg6g
|
||||
about: You can ask for help here!
|
||||
- name: Want to contribute to step certificates?
|
||||
url: https://github.com/smallstep/certificates/blob/master/docs/CONTRIBUTING.md
|
||||
about: Be sure to read contributing guidelines!
|
||||
|
22
.github/ISSUE_TEMPLATE/documentation-request.md
vendored
22
.github/ISSUE_TEMPLATE/documentation-request.md
vendored
|
@ -1,22 +0,0 @@
|
|||
---
|
||||
name: Documentation Request
|
||||
about: Request documentation for a feature
|
||||
title: '[Docs]:'
|
||||
labels: docs, needs triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Hello!
|
||||
<!-- Please leave this section as-is, it's designed to help others in the community know how to interact with our GitHub issues. -->
|
||||
|
||||
- Vote on this issue by adding a 👍 reaction
|
||||
- If you want to document this feature, comment to let us know (we'll work with you on design, scheduling, etc.)
|
||||
|
||||
## Affected area/feature
|
||||
|
||||
<!---
|
||||
Tell us which feature you'd like to see documented.
|
||||
- Where would you like that documentation to live (command line usage output, website, github markdown on the repo)?
|
||||
- If there are specific attributes or options you'd like to see documented, please include those in the request.
|
||||
-->
|
24
.github/ISSUE_TEMPLATE/enhancement.md
vendored
24
.github/ISSUE_TEMPLATE/enhancement.md
vendored
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
name: Enhancement
|
||||
about: Suggest an enhancement to step-ca
|
||||
title: ''
|
||||
labels: enhancement, needs triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Hello!
|
||||
<!-- Please leave this section as-is,
|
||||
it's designed to help others in the community know how to interact with our GitHub issues. -->
|
||||
|
||||
- Vote on this issue by adding a 👍 reaction
|
||||
- If you want to implement this feature, comment to let us know (we'll work with you on design, scheduling, etc.)
|
||||
|
||||
## Issue details
|
||||
|
||||
<!-- Enhancement requests are most helpful when they describe the problem you're having
|
||||
as well as articulating the potential solution you'd like to see built. -->
|
||||
|
||||
## Why is this needed?
|
||||
|
||||
<!-- Let us know why you think this enhancement would be good for the project or community. -->
|
20
.github/PULL_REQUEST_TEMPLATE
vendored
20
.github/PULL_REQUEST_TEMPLATE
vendored
|
@ -1,20 +1,4 @@
|
|||
<!---
|
||||
Please provide answers in the spaces below each prompt, where applicable.
|
||||
Not every PR requires responses for each prompt.
|
||||
Use your discretion.
|
||||
-->
|
||||
#### Name of feature:
|
||||
|
||||
#### Pain or issue this feature alleviates:
|
||||
|
||||
#### Why is this important to the project (if not answered above):
|
||||
|
||||
#### Is there documentation on how to use this feature? If so, where?
|
||||
|
||||
#### In what environments or workflows is this feature supported?
|
||||
|
||||
#### In what environments or workflows is this feature explicitly NOT supported (if any)?
|
||||
|
||||
#### Supporting links/other PRs/issues:
|
||||
### Description
|
||||
Please describe your pull request.
|
||||
|
||||
💔Thank you!
|
||||
|
|
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
|
@ -1,11 +0,0 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
|
@ -1,27 +0,0 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- 'v*'
|
||||
branches:
|
||||
- "master"
|
||||
pull_request:
|
||||
workflow_call:
|
||||
secrets:
|
||||
CODECOV_TOKEN:
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: smallstep/workflows/.github/workflows/goCI.yml@main
|
||||
with:
|
||||
only-latest-golang: false
|
||||
os-dependencies: 'libpcsclite-dev'
|
||||
run-codeql: true
|
||||
test-command: 'V=1 make test'
|
||||
secrets: inherit
|
9
.github/workflows/code-scan-cron.yml
vendored
9
.github/workflows/code-scan-cron.yml
vendored
|
@ -1,9 +0,0 @@
|
|||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
code-scan:
|
||||
uses: smallstep/workflows/.github/workflows/code-scan.yml@main
|
||||
secrets:
|
||||
GITLEAKS_LICENSE_KEY: ${{ secrets.GITLEAKS_LICENSE_KEY }}
|
22
.github/workflows/dependabot-auto-merge.yml
vendored
22
.github/workflows/dependabot-auto-merge.yml
vendored
|
@ -1,22 +0,0 @@
|
|||
name: Dependabot auto-merge
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v1.1.1
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Enable auto-merge for Dependabot PRs
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
91
.github/workflows/release.yml
vendored
91
.github/workflows/release.yml
vendored
|
@ -1,91 +0,0 @@
|
|||
name: Create Release & Upload Assets
|
||||
|
||||
on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: smallstep/certificates/.github/workflows/ci.yml@master
|
||||
secrets: inherit
|
||||
|
||||
create_release:
|
||||
name: Create Release
|
||||
needs: ci
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_IMAGE: smallstep/step-ca
|
||||
outputs:
|
||||
version: ${{ steps.extract-tag.outputs.VERSION }}
|
||||
is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
|
||||
docker_tags: ${{ env.DOCKER_TAGS }}
|
||||
docker_tags_hsm: ${{ env.DOCKER_TAGS_HSM }}
|
||||
steps:
|
||||
- name: Is Pre-release
|
||||
id: is_prerelease
|
||||
run: |
|
||||
set +e
|
||||
echo ${{ github.ref }} | grep "\-rc.*"
|
||||
OUT=$?
|
||||
if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi
|
||||
echo "IS_PRERELEASE=${IS_PRERELEASE}" >> ${GITHUB_OUTPUT}
|
||||
- name: Extract Tag Names
|
||||
id: extract-tag
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
|
||||
echo "DOCKER_TAGS=${{ env.DOCKER_IMAGE }}:${VERSION}" >> ${GITHUB_ENV}
|
||||
echo "DOCKER_TAGS_HSM=${{ env.DOCKER_IMAGE }}:${VERSION}-hsm" >> ${GITHUB_ENV}
|
||||
- name: Add Latest Tag
|
||||
if: steps.is_prerelease.outputs.IS_PRERELEASE == 'false'
|
||||
run: |
|
||||
echo "DOCKER_TAGS=${{ env.DOCKER_TAGS }},${{ env.DOCKER_IMAGE }}:latest" >> ${GITHUB_ENV}
|
||||
echo "DOCKER_TAGS_HSM=${{ env.DOCKER_TAGS_HSM }},${{ env.DOCKER_IMAGE }}:hsm" >> ${GITHUB_ENV}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
|
||||
|
||||
goreleaser:
|
||||
needs: create_release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
uses: smallstep/workflows/.github/workflows/goreleaser.yml@main
|
||||
secrets: inherit
|
||||
|
||||
build_upload_docker:
|
||||
name: Build & Upload Docker Images
|
||||
needs: create_release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm,linux/arm64
|
||||
tags: ${{ needs.create_release.outputs.docker_tags }}
|
||||
docker_image: smallstep/step-ca
|
||||
docker_file: docker/Dockerfile
|
||||
secrets: inherit
|
||||
|
||||
build_upload_docker_hsm:
|
||||
name: Build & Upload HSM Enabled Docker Images
|
||||
needs: create_release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main
|
||||
with:
|
||||
platforms: linux/amd64,linux/386,linux/arm,linux/arm64
|
||||
tags: ${{ needs.create_release.outputs.docker_tags_hsm }}
|
||||
docker_image: smallstep/step-ca
|
||||
docker_file: docker/Dockerfile.hsm
|
||||
secrets: inherit
|
16
.github/workflows/triage.yml
vendored
16
.github/workflows/triage.yml
vendored
|
@ -1,16 +0,0 @@
|
|||
name: Add Issues and PRs to Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
uses: smallstep/workflows/.github/workflows/triage.yml@main
|
||||
secrets: inherit
|
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -6,10 +6,6 @@
|
|||
*.so
|
||||
*.dylib
|
||||
|
||||
# Go Workspaces
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
|
@ -18,9 +14,7 @@ go.work.sum
|
|||
|
||||
# Others
|
||||
*.swp
|
||||
.releases
|
||||
.travis-releases
|
||||
coverage.txt
|
||||
output
|
||||
vendor
|
||||
.idea
|
||||
.envrc
|
||||
output
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:85
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:107
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:108
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:129
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:131
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:136
|
||||
deac15327f5605a1a963e50818760a95cee9d882:docs/kms.md:generic-api-key:138
|
||||
7c9ab9814fb676cb3c125c3dac4893271f1b7ae5:README.md:generic-api-key:282
|
||||
fb7140444ac8f1fa1245a80e49d17e206f7435f3:docs/provisioners.md:generic-api-key:110
|
||||
e4de7f07e82118b3f926716666b620db058fa9f7:docs/revocation.md:generic-api-key:73
|
||||
e4de7f07e82118b3f926716666b620db058fa9f7:docs/revocation.md:generic-api-key:113
|
||||
e4de7f07e82118b3f926716666b620db058fa9f7:docs/revocation.md:generic-api-key:151
|
||||
8b2de42e9cf6ce99f53a5049881e1d6077d5d66e:docs/docker.md:generic-api-key:152
|
||||
3939e855264117e81531df777a642ea953d325a7:autocert/init/ca/intermediate_ca_key:private-key:1
|
||||
e72f08703753facfa05f2d8c68f9f6a3745824b8:README.md:generic-api-key:244
|
||||
e70a5dae7de0b6ca40a0393c09c28872d4cfa071:autocert/README.md:generic-api-key:365
|
||||
e70a5dae7de0b6ca40a0393c09c28872d4cfa071:autocert/README.md:generic-api-key:366
|
||||
c284a2c0ab1c571a46443104be38c873ef0c7c6d:config.json:generic-api-key:10
|
238
.goreleaser.yml
238
.goreleaser.yml
|
@ -1,238 +0,0 @@
|
|||
# This is an example .goreleaser.yml file with some sane defaults.
|
||||
# Make sure to check the documentation at http://goreleaser.com
|
||||
project_name: step-ca
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
-
|
||||
id: step-ca
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
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-ca/main.go
|
||||
binary: step-ca
|
||||
ldflags:
|
||||
- -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}
|
||||
|
||||
archives:
|
||||
- &ARCHIVE
|
||||
# Can be used to change the archive formats for specific GOOSs.
|
||||
# Most common use case is to archive as zip on Windows.
|
||||
# Default is empty.
|
||||
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
|
||||
rlcp: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
wrap_in_directory: "{{ .ProjectName }}_{{ .Version }}"
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
allow_different_binary_count: true
|
||||
-
|
||||
<< : *ARCHIVE
|
||||
id: unversioned
|
||||
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
|
||||
|
||||
|
||||
nfpms:
|
||||
# Configure nFPM for .deb and .rpm releases
|
||||
#
|
||||
# See https://nfpm.goreleaser.com/configuration/
|
||||
# and https://goreleaser.com/customization/nfpm/
|
||||
#
|
||||
# Useful tools for debugging .debs:
|
||||
# List file contents: dpkg -c dist/step_...deb
|
||||
# Package metadata: dpkg --info dist/step_....deb
|
||||
#
|
||||
- &NFPM
|
||||
builds:
|
||||
- step-ca
|
||||
package_name: step-ca
|
||||
file_name_template: "{{ .PackageName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
|
||||
vendor: Smallstep Labs
|
||||
homepage: https://github.com/smallstep/certificates
|
||||
maintainer: Smallstep <techadmin@smallstep.com>
|
||||
description: >
|
||||
step-ca is an online certificate authority for secure, automated certificate management.
|
||||
license: Apache 2.0
|
||||
section: utils
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
priority: optional
|
||||
bindir: /usr/bin
|
||||
contents:
|
||||
- src: debian/copyright
|
||||
dst: /usr/share/doc/step-ca/copyright
|
||||
-
|
||||
<< : *NFPM
|
||||
id: unversioned
|
||||
file_name_template: "{{ .PackageName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
|
||||
|
||||
source:
|
||||
enabled: true
|
||||
rlcp: true
|
||||
name_template: '{{ .ProjectName }}_{{ .Version }}'
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
extra_files:
|
||||
- glob: ./.releases/*
|
||||
|
||||
signs:
|
||||
- cmd: cosign
|
||||
signature: "${artifact}.sig"
|
||||
certificate: "${artifact}.pem"
|
||||
args: ["sign-blob", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"]
|
||||
artifacts: all
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
|
||||
release:
|
||||
# Repo in which the release will be created.
|
||||
# Default is extracted from the origin remote URL or empty if its private hosted.
|
||||
# Note: it can only be one: either github, gitlab or gitea
|
||||
github:
|
||||
owner: smallstep
|
||||
name: certificates
|
||||
|
||||
# IDs of the archives to use.
|
||||
# Defaults to all.
|
||||
#ids:
|
||||
# - foo
|
||||
# - bar
|
||||
|
||||
# If set to true, will not auto-publish the release.
|
||||
# Default is false.
|
||||
draft: false
|
||||
|
||||
# If set to auto, will mark the release as not ready for production
|
||||
# in case there is an indicator for this in the tag e.g. v1.0.0-rc1
|
||||
# If set to true, will mark the release as not ready for production.
|
||||
# Default is false.
|
||||
prerelease: auto
|
||||
|
||||
# You can change the name of the release.
|
||||
# Default is `{{.Tag}}`
|
||||
name_template: "Step CA {{ .Tag }} ({{ .Env.RELEASE_DATE }})"
|
||||
|
||||
# Header template for the release body.
|
||||
# Defaults to empty.
|
||||
header: |
|
||||
## Official Release Artifacts
|
||||
|
||||
#### Linux
|
||||
|
||||
- 📦 [step-ca_linux_{{ .Version }}_amd64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_linux_{{ .Version }}_amd64.tar.gz)
|
||||
- 📦 [step-ca_{{ .Version }}_amd64.deb](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ .Version }}_amd64.deb)
|
||||
|
||||
#### OSX Darwin
|
||||
|
||||
- 📦 [step-ca_darwin_{{ .Version }}_amd64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_amd64.tar.gz)
|
||||
- 📦 [step-ca_darwin_{{ .Version }}_arm64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_arm64.tar.gz)
|
||||
|
||||
#### Windows
|
||||
|
||||
- 📦 [step-ca_windows_{{ .Version }}_amd64.zip](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_windows_{{ .Version }}_amd64.zip)
|
||||
|
||||
For more builds across platforms and architectures, see the `Assets` section below.
|
||||
And for packaged versions (Docker, k8s, Homebrew), see our [installation docs](https://smallstep.com/docs/step-ca/installation).
|
||||
|
||||
Don't see the artifact you need? Open an issue [here](https://github.com/smallstep/certificates/issues/new/choose).
|
||||
|
||||
## Signatures and Checksums
|
||||
|
||||
`step-ca` uses [sigstore/cosign](https://github.com/sigstore/cosign) for signing and verifying release artifacts.
|
||||
|
||||
Below is an example using `cosign` to verify a release artifact:
|
||||
|
||||
```
|
||||
cosign verify-blob \
|
||||
--certificate ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig.pem \
|
||||
--signature ~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig \
|
||||
--certificate-identity-regexp "https://github\.com/smallstep/certificates/.*" \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz
|
||||
```
|
||||
|
||||
The `checksums.txt` file (in the `Assets` section below) contains a checksum for every artifact in the release.
|
||||
|
||||
# Footer template for the release body.
|
||||
# Defaults to empty.
|
||||
footer: |
|
||||
## Thanks!
|
||||
|
||||
Those were the changes on {{ .Tag }}!
|
||||
|
||||
Come join us on [Discord](https://discord.gg/X2RKGwEbV9) to ask questions, chat about PKI, or get a sneak peak at the freshest PKI memes.
|
||||
|
||||
# You can disable this pipe in order to not upload any artifacts.
|
||||
# Defaults to false.
|
||||
#disable: true
|
||||
|
||||
# You can add extra pre-existing files to the release.
|
||||
# The filename on the release will be the last part of the path (base). If
|
||||
# another file with the same name exists, the latest one found will be used.
|
||||
# Defaults to empty.
|
||||
extra_files:
|
||||
- glob: ./.releases/*
|
||||
#extra_files:
|
||||
# - glob: ./path/to/file.txt
|
||||
# - glob: ./glob/**/to/**/file/**/*
|
||||
# - glob: ./glob/foo/to/bar/file/foobar/override_from_previous
|
||||
|
||||
scoops:
|
||||
-
|
||||
ids: [ default ]
|
||||
# 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"
|
||||
|
33
.travis.yml
Normal file
33
.travis.yml
Normal file
|
@ -0,0 +1,33 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.11.x
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- debhelper
|
||||
- fakeroot
|
||||
- bash-completion
|
||||
env:
|
||||
global:
|
||||
- V=1
|
||||
before_script:
|
||||
- make bootstrap
|
||||
script:
|
||||
- make
|
||||
- make artifacts
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash) -t "$CODECOV_TOKEN" || echo "Codecov did
|
||||
not collect coverage reports"
|
||||
notifications:
|
||||
email: false
|
||||
stage: Github Release
|
||||
deploy:
|
||||
provider: releases
|
||||
skip_cleanup: true
|
||||
api_key:
|
||||
secure: EVV43Vkqn67hhKGYn4WhQp2YO6KFmUDSkLXjYXYGX07Fm8p5KjRFBPOz9LV83QrvVmLigvg0CtR8Jqqcnq2SUhus3nhZaN2g19NhMypZLioyOVP0kAkas8ocuvxkwz3YxIK/yMrmTKbQ7JGXtbc8IjAox9ovNo1fFIQmVMAzPfu++OWBJ0j+gUqKtpaNA7gzsSv8UOw3/T3hNm6E1IbpWxl9BPSOzUOE9F/QOThANzifGfdxvqNJFkAgqu5DVPz8zQNbMrz4zH+KwASKxd6hjhzSSMzouKzOEHTA/elDCHEjke0Jos29MkGWHcIydLtCD95DGecqM8BFSC9f2acHDjmUO1rdfoLA3Pt+UiZJuTwyQm/jrHHhRnH8oJpK15G5LvxSqzY9YDWpAk38+jMw/udW6wt7BGAU8FEXLbq0bsFL3yfTepeWjmzT5WS0YXdiBz2SEK+Og9R2bSdtl4owghRzKNio5DNPuYAbqbpi+jqzqQVLj27x7LWoQ0MHvZcz9U+oO00r6M1tDCmFVRdtfgb2H+MIDY69qYGo5qoGMfH1btCWR8bA9wSYB/Z7hW/xZT9r7f/d5/P40k8yKINmTZqyUTQeplrE3y4BPVzKksclczBZa67syIUQ49I35QppnH4GFQHUwlra7r3W9zfZRvaLnp5qOIKAQe3MAIZqtLg=
|
||||
file_glob: true
|
||||
file: .travis-releases/*
|
||||
on:
|
||||
repo: smallstep/certificates
|
||||
tags: true
|
|
@ -1,7 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
read -r firstline < .VERSION
|
||||
last_half="${firstline##*tag: }"
|
||||
if [[ ${last_half::1} == "v" ]]; then
|
||||
version_string="${last_half%%[,)]*}"
|
||||
fi
|
||||
echo "${version_string:-v0.0.0}"
|
396
CHANGELOG.md
396
CHANGELOG.md
|
@ -1,407 +1,13 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## TEMPLATE -- do not alter or remove
|
||||
|
||||
---
|
||||
|
||||
## [x.y.z] - aaaa-bb-cc
|
||||
|
||||
## [Unreleased - 0.0.1] - DATE
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
### Security
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved authentication for ACME requests using kid and provisioner name
|
||||
(smallstep/certificates#1386).
|
||||
|
||||
|
||||
## [v0.24.2] - 2023-05-11
|
||||
|
||||
### Added
|
||||
|
||||
- Log SSH certificates (smallstep/certificates#1374)
|
||||
- CRL endpoints on the HTTP server (smallstep/certificates#1372)
|
||||
- Dynamic SCEP challenge validation using webhooks (smallstep/certificates#1366)
|
||||
- For Docker deployments, added DOCKER_STEPCA_INIT_PASSWORD_FILE. Useful for pointing to a Docker Secret in the container (smallstep/certificates#1384)
|
||||
|
||||
### Changed
|
||||
|
||||
- Depend on [smallstep/go-attestation](https://github.com/smallstep/go-attestation) instead of [google/go-attestation](https://github.com/google/go-attestation)
|
||||
- Render CRLs into http.ResponseWriter instead of memory (smallstep/certificates#1373)
|
||||
- Redaction of SCEP static challenge when listing provisioners (smallstep/certificates#1204)
|
||||
|
||||
### Fixed
|
||||
|
||||
- VaultCAS certificate lifetime (smallstep/certificates#1376)
|
||||
|
||||
## [v0.24.1] - 2023-04-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Docker image name for HSM support (smallstep/certificates#1348)
|
||||
|
||||
## [v0.24.0] - 2023-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Add ACME `device-attest-01` support with TPM 2.0
|
||||
(smallstep/certificates#1063).
|
||||
- Add support for new Azure SDK, sovereign clouds, and HSM keys on Azure KMS
|
||||
(smallstep/crypto#192, smallstep/crypto#197, smallstep/crypto#198,
|
||||
smallstep/certificates#1323, smallstep/certificates#1309).
|
||||
- Add support for ASN.1 functions on certificate templates
|
||||
(smallstep/crypto#208, smallstep/certificates#1345)
|
||||
- Add `DOCKER_STEPCA_INIT_ADDRESS` to configure the address to use in a docker
|
||||
container (smallstep/certificates#1262).
|
||||
- Make sure that the CSR used matches the attested key when using AME
|
||||
`device-attest-01` challenge (smallstep/certificates#1265).
|
||||
- Add support for compacting the Badger DB (smallstep/certificates#1298).
|
||||
- Build and release cleanups (smallstep/certificates#1322,
|
||||
smallstep/certificates#1329, smallstep/certificates#1340).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix support for PKCS #7 RSA-OAEP decryption through
|
||||
[smallstep/pkcs7#4](https://github.com/smallstep/pkcs7/pull/4), as used in
|
||||
SCEP.
|
||||
- Fix RA installation using `scripts/install-step-ra.sh`
|
||||
(smallstep/certificates#1255).
|
||||
- Clarify error messages on policy errors (smallstep/certificates#1287,
|
||||
smallstep/certificates#1278).
|
||||
- Clarify error message on OIDC email validation (smallstep/certificates#1290).
|
||||
- Mark the IDP critical in the generated CRL data (smallstep/certificates#1293).
|
||||
- Disable database if CA is initialized with the `--no-db` flag
|
||||
(smallstep/certificates#1294).
|
||||
|
||||
## [v0.23.2] - 2023-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- Added [`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) to
|
||||
docker images, and a new image, `smallstep/step-ca-hsm`, compiled with cgo
|
||||
(smallstep/certificates#1243).
|
||||
- Added [`scoop`](https://scoop.sh) packages back to the release
|
||||
(smallstep/certificates#1250).
|
||||
- Added optional flag `--pidfile` which allows passing a filename where step-ca
|
||||
will write its process id (smallstep/certificates#1251).
|
||||
- Added helpful message on CA startup when config can't be opened
|
||||
(smallstep/certificates#1252).
|
||||
- Improved validation and error messages on `device-attest-01` orders
|
||||
(smallstep/certificates#1235).
|
||||
|
||||
### Removed
|
||||
|
||||
- The deprecated CLI utils `step-awskms-init`, `step-cloudkms-init`,
|
||||
`step-pkcs11-init`, `step-yubikey-init` have been removed.
|
||||
[`step`](https://github.com/smallstep/cli) and
|
||||
[`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) should be
|
||||
used instead (smallstep/certificates#1240).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed remote management flags in docker images (smallstep/certificates#1228).
|
||||
|
||||
## [v0.23.1] - 2023-01-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added configuration property `.crl.idpURL` to be able to set a custom Issuing
|
||||
Distribution Point in the CRL (smallstep/certificates#1178).
|
||||
- Added WithContext methods to the CA client (smallstep/certificates#1211).
|
||||
- Docker: Added environment variables for enabling Remote Management and ACME
|
||||
provisioner (smallstep/certificates#1201).
|
||||
- Docker: The entrypoint script now generates and displays an initial JWK
|
||||
provisioner password by default when the CA is being initialized
|
||||
(smallstep/certificates#1223).
|
||||
|
||||
### Changed
|
||||
|
||||
- Ignore SSH principals validation when using an OIDC provisioner. The
|
||||
provisioner will ignore the principals passed and set the defaults or the ones
|
||||
including using WebHooks or templates (smallstep/certificates#1206).
|
||||
|
||||
## [v0.23.0] - 2022-11-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for ACME device-attest-01 challenge on iOS, iPadOS, tvOS and
|
||||
YubiKey.
|
||||
- Ability to disable ACME challenges and attestation formats.
|
||||
- Added flags to change ACME challenge ports for testing purposes.
|
||||
- Added name constraints evaluation and enforcement when issuing or renewing
|
||||
X.509 certificates.
|
||||
- Added provisioner webhooks for augmenting template data and authorizing
|
||||
certificate requests before signing.
|
||||
- Added automatic migration of provisioners when enabling remote management.
|
||||
- Added experimental support for CRLs.
|
||||
- Add certificate renewal support on RA mode. The `step ca renew` command must
|
||||
use the flag `--mtls=false` to use the token renewal flow.
|
||||
- Added support for initializing remote management using `step ca init`.
|
||||
- Added support for renewing X.509 certificates on RAs.
|
||||
- Added support for using SCEP with keys in a KMS.
|
||||
- Added client support to set the dialer's local address with the environment variable
|
||||
`STEP_CLIENT_ADDR`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Remove the email requirement for issuing SSH certificates with an OIDC
|
||||
provisioner.
|
||||
- Root files can contain more than one certificate.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed MySQL DSN parsing issues with an upgrade to
|
||||
[smallstep/nosql@v0.5.0](https://github.com/smallstep/nosql/releases/tag/v0.5.0).
|
||||
- Fixed renewal of certificates with missing subject attributes.
|
||||
- Fixed ACME support with [ejabberd](https://github.com/processone/ejabberd).
|
||||
|
||||
### Deprecated
|
||||
|
||||
- The CLIs `step-awskms-init`, `step-cloudkms-init`, `step-pkcs11-init`,
|
||||
`step-yubikey-init` are deprecated. Now you can use
|
||||
[`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) in
|
||||
combination with `step certificates create` to initialize your PKI.
|
||||
|
||||
## [0.22.1] - 2022-08-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed signature algorithm on EC (root) + RSA (intermediate) PKIs.
|
||||
|
||||
## [0.22.0] - 2022-08-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added automatic configuration of Linked RAs.
|
||||
- Send provisioner configuration on Linked RAs.
|
||||
|
||||
### Changed
|
||||
|
||||
- Certificates signed by an issuer using an RSA key will be signed using the
|
||||
same algorithm used to sign the issuer certificate. The signature will no
|
||||
longer default to PKCS #1. For example, if the issuer certificate was signed
|
||||
using RSA-PSS with SHA-256, a new certificate will also be signed using
|
||||
RSA-PSS with SHA-256.
|
||||
- Support two latest versions of Go (1.18, 1.19).
|
||||
- Validate revocation serial number (either base 10 or prefixed with an
|
||||
appropriate base).
|
||||
- Sanitize TLS options.
|
||||
|
||||
## [0.20.0] - 2022-05-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added Kubernetes auth method for Vault RAs.
|
||||
- Added support for reporting provisioners to linkedca.
|
||||
- Added support for certificate policies on authority level.
|
||||
- Added a Dockerfile with a step-ca build with HSM support.
|
||||
- A few new WithXX methods for instantiating authorities
|
||||
|
||||
### Changed
|
||||
|
||||
- Context usage in HTTP APIs.
|
||||
- Changed authentication for Vault RAs.
|
||||
- Error message returned to client when authenticating with expired certificate.
|
||||
- Strip padding from ACME CSRs.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- HTTP API handler types.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed SSH revocation.
|
||||
- CA client dial context for js/wasm target.
|
||||
- Incomplete `extraNames` support in templates.
|
||||
- SCEP GET request support.
|
||||
- Large SCEP request handling.
|
||||
|
||||
## [0.19.0] - 2022-04-19
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for certificate renewals after expiry using the claim `allowRenewalAfterExpiry`.
|
||||
- Added support for `extraNames` in X.509 templates.
|
||||
- Added `armv5` builds.
|
||||
- Added RA support using a Vault instance as the CA.
|
||||
- Added `WithX509SignerFunc` authority option.
|
||||
- Added a new `/roots.pem` endpoint to download the CA roots in PEM format.
|
||||
- Added support for Azure `Managed Identity` tokens.
|
||||
- Added support for automatic configuration of linked RAs.
|
||||
- Added support for the `--context` flag. It's now possible to start the
|
||||
CA with `step-ca --context=abc` to use the configuration from context `abc`.
|
||||
When a context has been configured and no configuration file is provided
|
||||
on startup, the configuration for the current context is used.
|
||||
- Added startup info logging and option to skip it (`--quiet`).
|
||||
- Added support for renaming the CA (Common Name).
|
||||
|
||||
### Changed
|
||||
|
||||
- Made SCEP CA URL paths dynamic.
|
||||
- Support two latest versions of Go (1.17, 1.18).
|
||||
- Upgrade go.step.sm/crypto to v0.16.1.
|
||||
- Upgrade go.step.sm/linkedca to v0.15.0.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Go 1.16 support.
|
||||
|
||||
### Removed
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed admin credentials on RAs.
|
||||
- Fixed ACME HTTP-01 challenges for IPv6 identifiers.
|
||||
- Various improvements under the hood.
|
||||
|
||||
### Security
|
||||
|
||||
## [0.18.2] - 2022-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- Added `subscriptionIDs` and `objectIDs` filters to the Azure provisioner.
|
||||
- [NoSQL](https://github.com/smallstep/nosql/pull/21) package allows filtering
|
||||
out database drivers using Go tags. For example, using the Go flag
|
||||
`--tags=nobadger,nobbolt,nomysql` will only compile `step-ca` with the pgx
|
||||
driver for PostgreSQL.
|
||||
|
||||
### Changed
|
||||
|
||||
- IPv6 addresses are normalized as IP addresses instead of hostnames.
|
||||
- More descriptive JWK decryption error message.
|
||||
- Make the X5C leaf certificate available to the templates using `{{ .AuthorizationCrt }}`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- During provisioner add - validate provisioner configuration before storing to DB.
|
||||
|
||||
## [0.18.1] - 2022-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- Support for ACME revocation.
|
||||
- Replace hash function with an RSA SSH CA to "rsa-sha2-256".
|
||||
- Support Nebula provisioners.
|
||||
- Example Ansible configurations.
|
||||
- Support PKCS#11 as a decrypter, as used by SCEP.
|
||||
|
||||
### Changed
|
||||
|
||||
- Automatically create database directory on `step ca init`.
|
||||
- Slightly improve errors reported when a template has invalid content.
|
||||
- Error reporting in logs and to clients.
|
||||
|
||||
### Fixed
|
||||
|
||||
- SCEP renewal using HTTPS on macOS.
|
||||
|
||||
## [0.18.0] - 2021-11-17
|
||||
|
||||
### Added
|
||||
|
||||
- Support for multiple certificate authority contexts.
|
||||
- Support for generating extractable keys and certificates on a pkcs#11 module.
|
||||
|
||||
### Changed
|
||||
|
||||
- Support two latest versions of Go (1.16, 1.17)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- go 1.15 support
|
||||
|
||||
## [0.17.6] - 2021-10-20
|
||||
|
||||
### Notes
|
||||
|
||||
- 0.17.5 failed in CI/CD
|
||||
|
||||
## [0.17.5] - 2021-10-20
|
||||
|
||||
### Added
|
||||
|
||||
- Support for Azure Key Vault as a KMS.
|
||||
- Adapt `pki` package to support key managers.
|
||||
- gocritic linter
|
||||
|
||||
### Fixed
|
||||
|
||||
- gocritic warnings
|
||||
|
||||
## [0.17.4] - 2021-09-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support host-only or user-only SSH CA.
|
||||
|
||||
## [0.17.3] - 2021-09-24
|
||||
|
||||
### Added
|
||||
|
||||
- go 1.17 to github action test matrix
|
||||
- Support for CloudKMS RSA-PSS signers without using templates.
|
||||
- Add flags to support individual passwords for the intermediate and SSH keys.
|
||||
- Global support for group admins in the OIDC provisioner.
|
||||
|
||||
### Changed
|
||||
|
||||
- Using go 1.17 for binaries
|
||||
|
||||
### Fixed
|
||||
|
||||
- Upgrade go-jose.v2 to fix a bug in the JWK fingerprint of Ed25519 keys.
|
||||
|
||||
### Security
|
||||
|
||||
- Use cosign to sign and upload signatures for multi-arch Docker container.
|
||||
- Add debian checksum
|
||||
|
||||
## [0.17.2] - 2021-08-30
|
||||
|
||||
### Added
|
||||
|
||||
- Additional way to distinguish Azure IID and Azure OIDC tokens.
|
||||
|
||||
### Security
|
||||
|
||||
- Sign over all goreleaser github artifacts using cosign
|
||||
|
||||
## [0.17.1] - 2021-08-26
|
||||
|
||||
## [0.17.0] - 2021-08-25
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for Linked CAs using protocol buffers and gRPC
|
||||
- `step-ca init` adds support for
|
||||
- configuring a StepCAS RA
|
||||
- configuring a Linked CA
|
||||
- congifuring a `step-ca` using Helm
|
||||
|
||||
### Changed
|
||||
|
||||
- Update badger driver to use v2 by default
|
||||
- Update TLS cipher suites to include 1.3
|
||||
|
||||
### Security
|
||||
|
||||
- Fix key version when SHA512WithRSA is used. There was a typo creating RSA keys with SHA256 digests instead of SHA512.
|
||||
|
|
163
CONTRIBUTING.md
163
CONTRIBUTING.md
|
@ -1,163 +0,0 @@
|
|||
# Contributing to `step certificates`
|
||||
|
||||
We welcome contributions to `step certificates` of any kind including
|
||||
documentation, themes, organization, tutorials, blog posts, bug reports,
|
||||
issues, feature requests, feature implementations, pull requests, helping
|
||||
to manage issues, etc.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Contributing to `step certificates`](#contributing-to-step-certificates)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Building From Source](#building-from-source)
|
||||
- [Build a standard `step-ca`](#build-a-standard-step-ca)
|
||||
- [Build `step-ca` using CGO](#build-step-ca-using-cgo)
|
||||
- [The CGO build enables PKCS #11 and YubiKey PIV support](#the-cgo-build-enables-pkcs-11-and-yubikey-piv-support)
|
||||
- [1. Install PCSC support](#1-install-pcsc-support)
|
||||
- [2. Build `step-ca`](#2-build-step-ca)
|
||||
- [Asking Support Questions](#asking-support-questions)
|
||||
- [Reporting Issues](#reporting-issues)
|
||||
- [Code Contribution](#code-contribution)
|
||||
- [Submitting Patches](#submitting-patches)
|
||||
- [Code Contribution Guidelines](#code-contribution-guidelines)
|
||||
- [Git Commit Message Guidelines](#git-commit-message-guidelines)
|
||||
|
||||
## Building From Source
|
||||
|
||||
Clone this repository to get a bleeding-edge build,
|
||||
or download the source archive for [the latest stable release](https://github.com/smallstep/certificates/releases/latest).
|
||||
|
||||
### Build a standard `step-ca`
|
||||
|
||||
The only prerequisites are [`go`](https://golang.org/) and make.
|
||||
|
||||
To build from source:
|
||||
|
||||
make bootstrap && make
|
||||
|
||||
Find your binaries in `bin/`.
|
||||
|
||||
### Build `step-ca` using CGO
|
||||
|
||||
#### The CGO build enables PKCS #11 and YubiKey PIV support
|
||||
|
||||
To build the CGO version of `step-ca`, you will need [`go`](https://golang.org/), make, and a C compiler.
|
||||
|
||||
You'll also need PCSC support on your operating system, as required by the `go-piv` module.
|
||||
On Linux, the [`libpcsclite-dev`](https://pcsclite.apdu.fr/) package provides PCSC support.
|
||||
On macOS and Windows, PCSC support is built into the OS.
|
||||
|
||||
#### 1. Install PCSC support
|
||||
|
||||
On Debian-based distributions, run:
|
||||
|
||||
```shell
|
||||
sudo apt-get install libpcsclite-dev
|
||||
```
|
||||
|
||||
On Fedora:
|
||||
|
||||
```shell
|
||||
sudo yum install pcsc-lite-devel
|
||||
```
|
||||
|
||||
On CentOS:
|
||||
|
||||
```
|
||||
sudo yum install 'dnf-command(config-manager)'
|
||||
sudo yum config-manager --set-enabled PowerTools
|
||||
sudo yum install pcsc-lite-devel
|
||||
```
|
||||
|
||||
#### 2. Build `step-ca`
|
||||
|
||||
To build `step-ca`, clone this repository and run the following:
|
||||
|
||||
```shell
|
||||
make bootstrap && make build GO_ENVS="CGO_ENABLED=1"
|
||||
```
|
||||
|
||||
When the build is complete, you will find binaries in `bin/`.
|
||||
|
||||
## Asking Support Questions
|
||||
|
||||
Feel free to post a question on our [GitHub Discussions](https://github.com/smallstep/certificates/discussions) page, or find us on [Discord](https://bit.ly/step-discord).
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you believe you have found a defect in `step certificates` or its
|
||||
documentation, use the GitHub [issue
|
||||
tracker](https://github.com/smallstep/certificates/issues) to report the
|
||||
problem. When reporting the issue, please provide the version of `step
|
||||
certificates` in use (`step-ca version`) and your operating system.
|
||||
|
||||
## Code Contribution
|
||||
|
||||
`step certificates` aims to become a fully featured online Certificate
|
||||
Authority. We encourage all contributions that meet the following criteria:
|
||||
|
||||
* fit naturally into a Certificate Authority.
|
||||
* strive not to break existing functionality.
|
||||
* close or update an open [`step certificates`
|
||||
issue](https://github.com/smallstep/certificates/issues)
|
||||
|
||||
**Bug fixes are, of course, always welcome.**
|
||||
|
||||
## Submitting Patches
|
||||
|
||||
`step certificates` welcomes all contributors and contributions. If you are
|
||||
interested in helping with the project, please reach out to us or, better yet,
|
||||
submit a PR :).
|
||||
|
||||
### Code Contribution Guidelines
|
||||
|
||||
Because we want to create the best possible product for our users and the best
|
||||
contribution experience for our developers, we have a set of guidelines which
|
||||
ensure that all contributions are acceptable. The guidelines are not intended
|
||||
as a filter or barrier to participation. If you are unfamiliar with the
|
||||
contribution process, the Smallstep team will guide you in order to get your
|
||||
contribution in accordance with the guidelines.
|
||||
|
||||
To make the contribution process as seamless as possible, we ask for the following:
|
||||
|
||||
* Go ahead and fork the project and make your changes. We encourage pull
|
||||
requests to allow for review and discussion of code changes.
|
||||
* When you’re ready to create a pull request, be sure to:
|
||||
* Sign the [CLA](https://cla-assistant.io/smallstep/certificates).
|
||||
* Have test cases for the new code. If you have questions about how to do
|
||||
this, please ask in your pull request.
|
||||
* Run `go fmt`.
|
||||
* Add documentation if you are adding new features or changing
|
||||
functionality.
|
||||
* Squash your commits into a single commit. `git rebase -i`. It’s okay to
|
||||
force update your pull request with `git push -f`.
|
||||
* Follow the **Git Commit Message Guidelines** below.
|
||||
|
||||
### Git Commit Message Guidelines
|
||||
|
||||
This [blog article](http://chris.beams.io/posts/git-commit/) is a good resource
|
||||
for learning how to write good commit messages, the most important part being
|
||||
that each commit message should have a title/subject in imperative mood
|
||||
starting with a capital letter and no trailing period: *"Return error on wrong
|
||||
use of the Paginator"*, **NOT** *"returning some error."*
|
||||
|
||||
Also, if your commit references one or more GitHub issues, always end your
|
||||
commit message body with *See #1234* or *Fixes #1234*. Replace *1234* with the
|
||||
GitHub issue ID. The last example will close the issue when the commit is
|
||||
merged into *master*.
|
||||
|
||||
Please use a short and descriptive branch name, e.g. **NOT** "patch-1". It's
|
||||
very common but creates a naming conflict each time when a submission is pulled
|
||||
for a review.
|
||||
|
||||
An example:
|
||||
|
||||
```text
|
||||
Add step certificate install
|
||||
|
||||
Add a command line utility for installing (and uninstalling) certificates to the
|
||||
local system truststores. This should help developers with local development
|
||||
flows.
|
||||
|
||||
Fixes #75
|
||||
```
|
616
Gopkg.lock
generated
Normal file
616
Gopkg.lock
generated
Normal file
|
@ -0,0 +1,616 @@
|
|||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:304cb78c285eaf02ab529ad02a257cad9b4845022915e6c82f87860ac53222d8"
|
||||
name = "github.com/alecthomas/gometalinter"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "bae2f1293d092fd8167939d5108d1b025eaef9de"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:c198fdc381e898e8fb62b8eb62758195091c313ad18e52a3067366e1dda2fb3c"
|
||||
name = "github.com/alecthomas/units"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:454adc7f974228ff789428b6dc098638c57a64aa0718f0bd61e53d3cd39d7a75"
|
||||
name = "github.com/chzyer/readline"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2972be24d48e78746da79ba8e24e8b488c9880de"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:848ef40f818e59905140552cc49ff3dc1a15f955e4b56d1c5c2cc4b54dbadf0c"
|
||||
name = "github.com/client9/misspell"
|
||||
packages = [
|
||||
".",
|
||||
"cmd/misspell",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "b90dc15cfd220ecf8bbc9043ecb928cef381f011"
|
||||
version = "v0.3.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2cd7915ab26ede7d95b8749e6b1f933f1c6d5398030684e6505940a10f31cfda"
|
||||
name = "github.com/ghodss/yaml"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:81fda4d18a16651bf92245ce5d6178cdd99f918db30ae9794732655f0686e895"
|
||||
name = "github.com/go-chi/chi"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "0ebf7795c516423a110473652e9ba3a59a504863"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b402bb9a24d108a9405a6f34675091b036c8b056aac843bf6ef2389a65c5cf48"
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = [
|
||||
"proto",
|
||||
"sortkeys",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "4cbf7e384e768b4e01799441fdf2a706a5635ae7"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "travis-1.9"
|
||||
digest = "1:e8f5d9c09a7209c740e769713376abda388c41b777ba8e9ed52767e21acf379f"
|
||||
name = "github.com/golang/lint"
|
||||
packages = [
|
||||
".",
|
||||
"golint",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "883fe33ffc4344bad1ecd881f61afd5ec5d80e0a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:239c4c7fd2159585454003d9be7207167970194216193a8a210b8d29576f19c9"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = [
|
||||
"proto",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/timestamp",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "c823c79ea1570fb5ff454033735a8e68575d1d0f"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:78ae27573e8adaf14269f9146230153a61c4b29bedc8742e464e053280dfa3d0"
|
||||
name = "github.com/google/certificate-transparency-go"
|
||||
packages = [
|
||||
".",
|
||||
"asn1",
|
||||
"client",
|
||||
"client/configpb",
|
||||
"jsonclient",
|
||||
"tls",
|
||||
"x509",
|
||||
"x509/pkix",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "b5e3a70217c2a317c27b3c852126d0f8f29fef2b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:3ee90c0d94da31b442dde97c99635aaafec68d0b8a3c12ee2075c6bdabeec6bb"
|
||||
name = "github.com/google/gofuzz"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:750e747d0aad97b79f4a4e00034bae415c2ea793fd9e61438d966ee9c79579bf"
|
||||
name = "github.com/google/shlex"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "6f45313302b9c56850fc17f99e40caebce98c716"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:824d147914b40e56e9e1eebd602bc6bb9761989d52fd8e4a498428467980eb17"
|
||||
name = "github.com/gordonklaus/ineffassign"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "1003c8bd00dc2869cb5ca5282e6ce33834fed514"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3e551bbb3a7c0ab2a2bf4660e7fcad16db089fdcfbb44b0199e62838038623ea"
|
||||
name = "github.com/json-iterator/go"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "1624edc4454b8682399def8740d46db5e4362ba4"
|
||||
version = "v1.1.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e51f40f0c19b39c1825eadd07d5c0a98a2ad5942b166d9fc4f54750ce9a04810"
|
||||
name = "github.com/juju/ansiterm"
|
||||
packages = [
|
||||
".",
|
||||
"tabwriter",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "720a0952cc2ac777afc295d9861263e2a4cf96a1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0a69a1c0db3591fcefb47f115b224592c8dfa4368b7ba9fae509d5e16cdc95c8"
|
||||
name = "github.com/konsorten/go-windows-terminal-sequences"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "5c8c8bd35d3832f5d134ae1e1e375b69a4d25242"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:bb08c7bb1c7224636b1a00639f079ed4391eb822945f26db74b8d8ee3f14d991"
|
||||
name = "github.com/lunixbochs/vtclean"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "2d01aacdc34a083dca635ba869909f5fc0cd4f41"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2a2a76072bd413b3484a0b5bb2fbb078b0b7dd8950e9276c900e14dce2354679"
|
||||
name = "github.com/manifoldco/promptui"
|
||||
packages = [
|
||||
".",
|
||||
"list",
|
||||
"screenbuf",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "20f2a94120aa14a334121a6de66616a7fa89a5cd"
|
||||
version = "v0.3.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0981502f9816113c9c8c4ac301583841855c8cf4da8c72f696b3ebedf6d0e4e5"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c"
|
||||
version = "v0.0.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:33422d238f147d247752996a26574ac48dcf472976eda7f5134015f06bf16563"
|
||||
name = "github.com/modern-go/concurrent"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
|
||||
version = "1.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e32bdbdb7c377a07a9a46378290059822efdce5c8d96fe71940d87cb4f918855"
|
||||
name = "github.com/modern-go/reflect2"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
|
||||
version = "1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:266d082179f3a29a4bdcf1dcc49d4a304f5c7107e65bd22d1fecacf45f1ac348"
|
||||
name = "github.com/newrelic/go-agent"
|
||||
packages = [
|
||||
".",
|
||||
"internal",
|
||||
"internal/cat",
|
||||
"internal/jsonx",
|
||||
"internal/logger",
|
||||
"internal/sysinfo",
|
||||
"internal/utilization",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "f5bce3387232559bcbe6a5f8227c4bf508dac1ba"
|
||||
version = "v1.11.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:07140002dbf37da92090f731b46fa47be4820b82fe5c14a035203b0e813d0ec2"
|
||||
name = "github.com/nicksnyder/go-i18n"
|
||||
packages = [
|
||||
"i18n",
|
||||
"i18n/bundle",
|
||||
"i18n/language",
|
||||
"i18n/translation",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "0dc1626d56435e9d605a29875701721c54bc9bbd"
|
||||
version = "v1.10.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e"
|
||||
name = "github.com/pelletier/go-toml"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2e76a73cb51f42d63a2a1a85b3dc5731fd4faf6821b434bd0ef2c099186031d6"
|
||||
name = "github.com/rs/xid"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "15d26544def341f036c5f8dca987a4cbe575032c"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:8baa3b16f20963c54e296627ea1dabfd79d1b486f81baf8759e99d73bddf2687"
|
||||
name = "github.com/samfoo/ansi"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "b6bd2ded7189ce35bc02233b554eb56a5146af73"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:def689e73e9252f6f7fe66834a76751a41b767e03daab299e607e7226c58a855"
|
||||
name = "github.com/shurcooL/sanitized_anchor_name"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3f53e9e4dfbb664cd62940c9c4b65a2171c66acd0b7621a1a6b8e78513525a52"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "ad15b42461921f1fb3529b058c6786c6a45d5162"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4d1f0640875aefefdb2151f297c144518a71f5729c4b9f9423f09df501f699c5"
|
||||
name = "github.com/smallstep/assert"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "de77670473b5492f5d0bce155b5c01534c2d13f7"
|
||||
|
||||
[[projects]]
|
||||
branch = "certificate-transparency"
|
||||
digest = "1:ed4ae9e597c66929a567de027512ee77a632ed8c4471e53c82c82c6658c4eb90"
|
||||
name = "github.com/smallstep/cli"
|
||||
packages = [
|
||||
"command",
|
||||
"config",
|
||||
"crypto/keys",
|
||||
"crypto/pemutil",
|
||||
"crypto/randutil",
|
||||
"crypto/tlsutil",
|
||||
"crypto/x509util",
|
||||
"errs",
|
||||
"jose",
|
||||
"pkg/blackfriday",
|
||||
"pkg/x509",
|
||||
"token",
|
||||
"token/provision",
|
||||
"ui",
|
||||
"usage",
|
||||
"utils",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "6cb4285dba37a5d03e1f4be41fc897b071181a70"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ba52e5a5fb800ce55108b7a5f181bb809aab71c16736051312b0aa969f82ad39"
|
||||
name = "github.com/tsenart/deadcode"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "210d2dc333e90c7e3eedf4f2242507a8e83ed4ab"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:6743b69de0d73e91004e4e201cf4965b59a0fa5caf6f0ffbe0cb9ee8807738a7"
|
||||
name = "github.com/urfave/cli"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "b67dcf995b6a7b7f14fad5fcb7cc5441b05e814b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a068d4e48e0f2e172903d25b6e066815fa8efd4b01102aec4c741f02a9650c03"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"cryptobyte",
|
||||
"cryptobyte/asn1",
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"pbkdf2",
|
||||
"ssh/terminal",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:50bed722f4f0bbb3d64b0ca49d41911f57a0ddb63e03666656f6621af3b70f9e"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"context",
|
||||
"context/ctxhttp",
|
||||
"html",
|
||||
"html/atom",
|
||||
"http/httpguts",
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"internal/timeseries",
|
||||
"trace",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "c44066c5c816ec500d459a2a324a753f78531ae0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:417d27a82efb8473554234a282be33d23b0d6adc121e636b55950f913ac071d6"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "9b800f95dbbc54abff0acf7ee32d88ba4e328c89"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"collate",
|
||||
"collate/build",
|
||||
"internal/colltab",
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"secure/bidirule",
|
||||
"transform",
|
||||
"unicode/bidi",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
"unicode/rangetable",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:384310e8a567edf6d5406d93318f9460c2d8db1a07ff5b6fece95b224343e7f1"
|
||||
name = "golang.org/x/tools"
|
||||
packages = [
|
||||
"go/ast/astutil",
|
||||
"go/gcexportdata",
|
||||
"go/internal/gcimporter",
|
||||
"go/types/typeutil",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "3a10b9bf0a52df7e992a8c3eb712a86d3c896c75"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:077c1c599507b3b3e9156d17d36e1e61928ee9b53a5b420f10f28ebd4a0b275c"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
pruneopts = "UT"
|
||||
revision = "4b09977fb92221987e99d190c8f88f2c92727a29"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9ab5a33d8cb5c120602a34d2e985ce17956a4e8c2edce7e6961568f95e40c09a"
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"binarylog/grpc_binarylog_v1",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"credentials/internal",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclog",
|
||||
"internal",
|
||||
"internal/backoff",
|
||||
"internal/binarylog",
|
||||
"internal/channelz",
|
||||
"internal/envconfig",
|
||||
"internal/grpcrand",
|
||||
"internal/grpcsync",
|
||||
"internal/syscall",
|
||||
"internal/transport",
|
||||
"keepalive",
|
||||
"metadata",
|
||||
"naming",
|
||||
"peer",
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "a02b0774206b209466313a0b525d2c738fe407eb"
|
||||
version = "v1.18.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:39efb07a0d773dc09785b237ada4e10b5f28646eb6505d97bc18f8d2ff439362"
|
||||
name = "gopkg.in/alecthomas/kingpin.v3-unstable"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a"
|
||||
name = "gopkg.in/inf.v0"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
|
||||
version = "v0.9.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7fbe10f3790dc4e6296c7c844c5a9b35513e5521c29c47e10ba99cd2956a2719"
|
||||
name = "gopkg.in/square/go-jose.v2"
|
||||
packages = [
|
||||
".",
|
||||
"cipher",
|
||||
"json",
|
||||
"jwt",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8"
|
||||
version = "v2.1.9"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:767b6c0b2c1d9487ee50cb8df1d0fdebf06ac0b19b723f6489d388e7b47c962d"
|
||||
name = "k8s.io/api"
|
||||
packages = [
|
||||
"admission/v1beta1",
|
||||
"authentication/v1",
|
||||
"core/v1",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "de494049e22a9ccf748c5bbda7492f42f344d0cd"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5eb353533eaebdfec2392210ab218a389965ba5d4dc02b4aef87b9549e5d0f84"
|
||||
name = "k8s.io/apimachinery"
|
||||
packages = [
|
||||
"pkg/api/resource",
|
||||
"pkg/apis/meta/v1",
|
||||
"pkg/apis/meta/v1/unstructured",
|
||||
"pkg/conversion",
|
||||
"pkg/conversion/queryparams",
|
||||
"pkg/fields",
|
||||
"pkg/labels",
|
||||
"pkg/runtime",
|
||||
"pkg/runtime/schema",
|
||||
"pkg/runtime/serializer",
|
||||
"pkg/runtime/serializer/json",
|
||||
"pkg/runtime/serializer/protobuf",
|
||||
"pkg/runtime/serializer/recognizer",
|
||||
"pkg/runtime/serializer/versioning",
|
||||
"pkg/selection",
|
||||
"pkg/types",
|
||||
"pkg/util/errors",
|
||||
"pkg/util/framer",
|
||||
"pkg/util/intstr",
|
||||
"pkg/util/json",
|
||||
"pkg/util/naming",
|
||||
"pkg/util/net",
|
||||
"pkg/util/runtime",
|
||||
"pkg/util/sets",
|
||||
"pkg/util/validation",
|
||||
"pkg/util/validation/field",
|
||||
"pkg/util/yaml",
|
||||
"pkg/watch",
|
||||
"third_party/forked/golang/reflect",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "4b3b852955ebe47857fcf134b531b23dd8f3e793"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:72fd56341405f53c745377e0ebc4abeff87f1a048e0eea6568a20212650f5a82"
|
||||
name = "k8s.io/klog"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "71442cd4037d612096940ceb0f3fec3f7fff66e0"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7719608fe0b52a4ece56c2dde37bedd95b938677d1ab0f84b8a7852e4c59f849"
|
||||
name = "sigs.k8s.io/yaml"
|
||||
packages = ["."]
|
||||
pruneopts = "UT"
|
||||
revision = "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
version = "v1.1.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/alecthomas/gometalinter",
|
||||
"github.com/client9/misspell/cmd/misspell",
|
||||
"github.com/ghodss/yaml",
|
||||
"github.com/go-chi/chi",
|
||||
"github.com/golang/lint/golint",
|
||||
"github.com/golang/protobuf/proto",
|
||||
"github.com/google/certificate-transparency-go",
|
||||
"github.com/google/certificate-transparency-go/client",
|
||||
"github.com/google/certificate-transparency-go/jsonclient",
|
||||
"github.com/google/certificate-transparency-go/tls",
|
||||
"github.com/google/certificate-transparency-go/x509",
|
||||
"github.com/gordonklaus/ineffassign",
|
||||
"github.com/newrelic/go-agent",
|
||||
"github.com/pkg/errors",
|
||||
"github.com/rs/xid",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/smallstep/assert",
|
||||
"github.com/smallstep/cli/config",
|
||||
"github.com/smallstep/cli/crypto/keys",
|
||||
"github.com/smallstep/cli/crypto/pemutil",
|
||||
"github.com/smallstep/cli/crypto/randutil",
|
||||
"github.com/smallstep/cli/crypto/tlsutil",
|
||||
"github.com/smallstep/cli/crypto/x509util",
|
||||
"github.com/smallstep/cli/errs",
|
||||
"github.com/smallstep/cli/jose",
|
||||
"github.com/smallstep/cli/pkg/x509",
|
||||
"github.com/smallstep/cli/token",
|
||||
"github.com/smallstep/cli/token/provision",
|
||||
"github.com/smallstep/cli/usage",
|
||||
"github.com/tsenart/deadcode",
|
||||
"github.com/urfave/cli",
|
||||
"golang.org/x/net/context",
|
||||
"golang.org/x/net/http2",
|
||||
"google.golang.org/grpc",
|
||||
"google.golang.org/grpc/credentials",
|
||||
"google.golang.org/grpc/peer",
|
||||
"gopkg.in/square/go-jose.v2",
|
||||
"gopkg.in/square/go-jose.v2/jwt",
|
||||
"k8s.io/api/admission/v1beta1",
|
||||
"k8s.io/api/core/v1",
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"k8s.io/apimachinery/pkg/runtime",
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
73
Gopkg.toml
Normal file
73
Gopkg.toml
Normal file
|
@ -0,0 +1,73 @@
|
|||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
required = [
|
||||
"github.com/alecthomas/gometalinter",
|
||||
"github.com/golang/lint/golint",
|
||||
"github.com/client9/misspell/cmd/misspell",
|
||||
"github.com/gordonklaus/ineffassign",
|
||||
"github.com/tsenart/deadcode",
|
||||
]
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/alecthomas/gometalinter"
|
||||
revision = "bae2f1293d092fd8167939d5108d1b025eaef9de"
|
||||
|
||||
[[override]]
|
||||
name = "gopkg.in/alecthomas/kingpin.v3-unstable"
|
||||
revision = "63abe20a23e29e80bbef8089bd3dee3ac25e5306"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/go-chi/chi"
|
||||
|
||||
[[override]]
|
||||
branch = "certificate-transparency"
|
||||
name = "github.com/smallstep/cli"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/newrelic/go-agent"
|
||||
version = "1.11.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "1.0.6"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/square/go-jose.v2"
|
||||
version = "2.1.9"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/google/certificate-transparency-go"
|
||||
|
||||
[[override]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/protobuf"
|
191
LICENSE
191
LICENSE
|
@ -1,193 +1,4 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2020 Smallstep Labs, Inc.
|
||||
Copyright (c) 2019 Smallstep Labs, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
305
Makefile
305
Makefile
|
@ -6,53 +6,56 @@ Q=$(if $V,,@)
|
|||
PREFIX?=
|
||||
SRC=$(shell find . -type f -name '*.go' -not -path "./vendor/*")
|
||||
GOOS_OVERRIDE ?=
|
||||
OUTPUT_ROOT=output/
|
||||
|
||||
all: lint test build
|
||||
# Set shell to bash for `echo -e`
|
||||
SHELL := /bin/bash
|
||||
|
||||
ci: testcgo build
|
||||
all: build lint test
|
||||
|
||||
.PHONY: all ci
|
||||
.PHONY: all
|
||||
|
||||
#########################################
|
||||
# Bootstrapping
|
||||
#########################################
|
||||
|
||||
bootstra%:
|
||||
$Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin latest
|
||||
$Q go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
$Q go install gotest.tools/gotestsum@latest
|
||||
$Q go install github.com/goreleaser/goreleaser@latest
|
||||
$Q go install github.com/sigstore/cosign/v2/cmd/cosign@latest
|
||||
$Q which dep || go get github.com/golang/dep/cmd/dep
|
||||
$Q dep ensure
|
||||
|
||||
.PHONY: bootstra%
|
||||
vendor: Gopkg.lock
|
||||
$Q dep ensure
|
||||
|
||||
BOOTSTRAP=\
|
||||
github.com/golang/lint/golint \
|
||||
github.com/client9/misspell/cmd/misspell \
|
||||
github.com/gordonklaus/ineffassign \
|
||||
github.com/tsenart/deadcode \
|
||||
github.com/alecthomas/gometalinter
|
||||
|
||||
define VENDOR_BIN_TMPL
|
||||
vendor/bin/$(notdir $(1)): vendor
|
||||
$Q go build -o $$@ ./vendor/$(1)
|
||||
VENDOR_BINS += vendor/bin/$(notdir $(1))
|
||||
endef
|
||||
|
||||
$(foreach pkg,$(BOOTSTRAP),$(eval $(call VENDOR_BIN_TMPL,$(pkg))))
|
||||
|
||||
.PHONY: bootstra% vendor
|
||||
|
||||
#################################################
|
||||
# Determine the type of `push` and `version`
|
||||
#################################################
|
||||
|
||||
# GITHUB Actions
|
||||
ifdef GITHUB_REF
|
||||
VERSION ?= $(shell echo $(GITHUB_REF) | sed 's/^refs\/tags\///')
|
||||
NOT_RC := $(shell echo $(VERSION) | grep -v -e -rc)
|
||||
ifeq ($(NOT_RC),)
|
||||
PUSHTYPE := release-candidate
|
||||
else
|
||||
PUSHTYPE := release
|
||||
endif
|
||||
else
|
||||
# Version flags to embed in the binaries
|
||||
VERSION ?= $(shell [ -d .git ] && git describe --tags --always --dirty="-dev")
|
||||
# If we are not in an active git dir then try reading the version from .VERSION.
|
||||
# .VERSION contains a slug populated by `git archive`.
|
||||
VERSION := $(or $(VERSION),$(shell ./.version.sh .VERSION))
|
||||
PUSHTYPE := branch
|
||||
endif
|
||||
|
||||
VERSION := $(shell echo $(VERSION) | sed 's/^v//')
|
||||
|
||||
ifdef V
|
||||
$(info GITHUB_REF is $(GITHUB_REF))
|
||||
$(info VERSION is $(VERSION))
|
||||
$(info PUSHTYPE is $(PUSHTYPE))
|
||||
# If TRAVIS_TAG is set then we know this ref has been tagged.
|
||||
ifdef TRAVIS_TAG
|
||||
PUSHTYPE=release
|
||||
else
|
||||
PUSHTYPE=master
|
||||
endif
|
||||
|
||||
#########################################
|
||||
|
@ -61,38 +64,22 @@ endif
|
|||
|
||||
DATE := $(shell date -u '+%Y-%m-%d %H:%M UTC')
|
||||
LDFLAGS := -ldflags='-w -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
|
||||
|
||||
# Always explicitly enable or disable cgo,
|
||||
# so that go doesn't silently fall back on
|
||||
# non-cgo when gcc is not found.
|
||||
ifeq (,$(findstring CGO_ENABLED,$(GO_ENVS)))
|
||||
ifneq ($(origin GOFLAGS),undefined)
|
||||
# This section is for backward compatibility with
|
||||
#
|
||||
# $ make build GOFLAGS=""
|
||||
#
|
||||
# which is how we recommended building step-ca with cgo support
|
||||
# until June 2023.
|
||||
GO_ENVS := $(GO_ENVS) CGO_ENABLED=1
|
||||
else
|
||||
GO_ENVS := $(GO_ENVS) CGO_ENABLED=0
|
||||
endif
|
||||
endif
|
||||
|
||||
download:
|
||||
$Q go mod download
|
||||
GOFLAGS := CGO_ENABLED=0
|
||||
|
||||
build: $(PREFIX)bin/$(BINNAME)
|
||||
@echo "Build Complete!"
|
||||
|
||||
$(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go)
|
||||
$(PREFIX)bin/$(BINNAME): vendor $(call rwildcard,*.go)
|
||||
$Q mkdir -p $(@D)
|
||||
$Q $(GOOS_OVERRIDE) GOFLAGS="$(GOFLAGS)" $(GO_ENVS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG)
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG)
|
||||
|
||||
# Target to force a build of step-ca without running tests
|
||||
simple: build
|
||||
# Target for building without calling dep ensure
|
||||
simple:
|
||||
$Q mkdir -p bin/
|
||||
$Q $(GOOS_OVERRIDE) $(GOFLAGS) go build -v -o bin/$(BINNAME) $(LDFLAGS) $(PKG)
|
||||
@echo "Build Complete!"
|
||||
|
||||
.PHONY: download build simple
|
||||
.PHONY: build simple
|
||||
|
||||
#########################################
|
||||
# Go generate
|
||||
|
@ -106,26 +93,24 @@ generate:
|
|||
#########################################
|
||||
# Test
|
||||
#########################################
|
||||
test: testdefault testtpmsimulator combinecoverage
|
||||
test:
|
||||
$Q $(GOFLAGS) go test -short -coverprofile=coverage.out ./...
|
||||
|
||||
testdefault:
|
||||
$Q $(GO_ENVS) gotestsum -- -coverprofile=defaultcoverage.out -short -covermode=atomic ./...
|
||||
vtest:
|
||||
$(Q)for d in $$(go list ./... | grep -v vendor); do \
|
||||
echo -e "TESTS FOR: for \033[0;35m$$d\033[0m"; \
|
||||
$(GOFLAGS) go test -v -bench=. -run=. -short -coverprofile=coverage.out $$d; \
|
||||
out=$$?; \
|
||||
if [[ $$out -ne 0 ]]; then ret=$$out; fi;\
|
||||
rm -f profile.coverage.out; \
|
||||
done; exit $$ret;
|
||||
|
||||
testtpmsimulator:
|
||||
$Q CGO_ENABLED=1 gotestsum -- -coverprofile=tpmsimulatorcoverage.out -short -covermode=atomic -tags tpmsimulator ./acme
|
||||
|
||||
testcgo:
|
||||
$Q gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...
|
||||
|
||||
combinecoverage:
|
||||
cat defaultcoverage.out tpmsimulatorcoverage.out > coverage.out
|
||||
|
||||
.PHONY: test testdefault testtpmsimulator testcgo combinecoverage
|
||||
.PHONY: test vtest
|
||||
|
||||
integrate: integration
|
||||
|
||||
integration: bin/$(BINNAME)
|
||||
$Q $(GO_ENVS) gotestsum -- -tags=integration ./integration/...
|
||||
$Q $(GOFLAGS) go test -tags=integration ./integration/...
|
||||
|
||||
.PHONY: integrate integration
|
||||
|
||||
|
@ -133,15 +118,26 @@ integration: bin/$(BINNAME)
|
|||
# Linting
|
||||
#########################################
|
||||
|
||||
LINTERS=\
|
||||
gofmt \
|
||||
golint \
|
||||
vet \
|
||||
misspell \
|
||||
ineffassign \
|
||||
deadcode
|
||||
|
||||
$(patsubst %,%-bin,$(filter-out gofmt vet,$(LINTERS))): %-bin: vendor/bin/%
|
||||
gofmt-bin vet-bin:
|
||||
|
||||
$(LINTERS): %: vendor/bin/gometalinter %-bin vendor
|
||||
$Q PATH=`pwd`/vendor/bin:$$PATH gometalinter --tests --disable-all --vendor \
|
||||
--deadline=5m -s data -s pkg --enable $@ ./...
|
||||
fmt:
|
||||
$Q goimports -l -w $(SRC)
|
||||
$Q gofmt -l -w $(SRC)
|
||||
|
||||
lint: SHELL:=/bin/bash
|
||||
lint:
|
||||
$Q LOG_LEVEL=error golangci-lint run --config <(curl -s https://raw.githubusercontent.com/smallstep/workflows/master/.golangci.yml) --timeout=30m
|
||||
$Q govulncheck ./...
|
||||
lint: $(LINTERS)
|
||||
|
||||
.PHONY: fmt lint
|
||||
.PHONY: $(LINTERS) lint fmt
|
||||
|
||||
#########################################
|
||||
# Install
|
||||
|
@ -157,23 +153,164 @@ uninstall:
|
|||
|
||||
.PHONY: install uninstall
|
||||
|
||||
#########################################
|
||||
# Building Docker Image
|
||||
#
|
||||
# Builds a dockerfile for step by building a linux version of the step-cli and
|
||||
# then copying the specific binary when building the container.
|
||||
#
|
||||
# This ensures the container is as small as possible without having to deal
|
||||
# with getting access to private repositories inside the container during build
|
||||
# time.
|
||||
#########################################
|
||||
|
||||
# XXX We put the output for the build in 'output' so we don't mess with how we
|
||||
# do rule overriding from the base Makefile (if you name it 'build' it messes up
|
||||
# the wildcarding).
|
||||
DOCKER_OUTPUT=$(OUTPUT_ROOT)docker/
|
||||
|
||||
DOCKER_MAKE=V=$V GOOS_OVERRIDE='GOOS=linux GOARCH=amd64' PREFIX=$(1) make $(1)bin/$(2)
|
||||
DOCKER_BUILD=$Q docker build -t smallstep/$(1):latest -f docker/$(2) --build-arg BINPATH=$(DOCKER_OUTPUT)bin/$(1) .
|
||||
|
||||
docker: docker-make docker/Dockerfile.step-ca
|
||||
$(call DOCKER_BUILD,step-ca,Dockerfile.step-ca)
|
||||
|
||||
docker-make:
|
||||
mkdir -p $(DOCKER_OUTPUT)
|
||||
$(call DOCKER_MAKE,$(DOCKER_OUTPUT),step-ca)
|
||||
|
||||
.PHONY: docker docker-make
|
||||
|
||||
#################################################
|
||||
# Releasing Docker Images
|
||||
#
|
||||
# Using the docker build infrastructure, this section is responsible for
|
||||
# logging into docker hub and pushing the built docker containers up with the
|
||||
# appropriate tags.
|
||||
#################################################
|
||||
|
||||
DOCKER_TAG=docker tag smallstep/$(1):latest smallstep/$(1):$(2)
|
||||
DOCKER_PUSH=docker push smallstep/$(1):$(2)
|
||||
|
||||
docker-tag:
|
||||
$(call DOCKER_TAG,step-ca,$(VERSION))
|
||||
|
||||
docker-push-tag: docker-tag
|
||||
$(call DOCKER_PUSH,step-ca,$(VERSION))
|
||||
|
||||
# Rely on DOCKER_USERNAME and DOCKER_PASSWORD being set inside the CI or
|
||||
# equivalent environment
|
||||
docker-login:
|
||||
$Q docker login -u="$(DOCKER_USERNAME)" -p="$(DOCKER_PASSWORD)"
|
||||
|
||||
.PHONY: docker-login docker-tag docker-push-tag
|
||||
|
||||
#################################################
|
||||
# Targets for pushing the docker images
|
||||
#################################################
|
||||
|
||||
# For all builds on the master branch, we actually build the container
|
||||
docker-master: docker
|
||||
|
||||
# For all builds on the master branch with an rc tag
|
||||
docker-release: docker-master docker-login docker-push-tag
|
||||
|
||||
.PHONY: docker-master docker-release
|
||||
|
||||
#########################################
|
||||
# Debian
|
||||
#########################################
|
||||
|
||||
changelog:
|
||||
$Q echo "step-certificates ($(VERSION)) unstable; urgency=medium" > debian/changelog
|
||||
$Q echo >> debian/changelog
|
||||
$Q echo " * See https://github.com/smallstep/certificates/releases" >> debian/changelog
|
||||
$Q echo >> debian/changelog
|
||||
$Q echo " -- Smallstep Labs, Inc. <techadmin@smallstep.com> $(shell date -uR)" >> debian/changelog
|
||||
|
||||
debian: changelog
|
||||
$Q mkdir -p $(RELEASE); \
|
||||
OUTPUT=../step-certificates_*.deb; \
|
||||
rm $$OUTPUT; \
|
||||
dpkg-buildpackage -b -rfakeroot -us -uc && cp $$OUTPUT $(RELEASE)/
|
||||
|
||||
distclean: clean
|
||||
|
||||
.PHONY: changelog debian distclean
|
||||
|
||||
#################################################
|
||||
# Build statically compiled step binary for various operating systems
|
||||
#################################################
|
||||
|
||||
BINARY_OUTPUT=$(OUTPUT_ROOT)binary/
|
||||
BUNDLE_MAKE=v=$v GOOS_OVERRIDE='GOOS=$(1) GOARCH=$(2)' PREFIX=$(3) make $(3)bin/$(BINNAME)
|
||||
RELEASE=./.travis-releases
|
||||
|
||||
binary-linux:
|
||||
$(call BUNDLE_MAKE,linux,amd64,$(BINARY_OUTPUT)linux/)
|
||||
|
||||
binary-darwin:
|
||||
$(call BUNDLE_MAKE,darwin,amd64,$(BINARY_OUTPUT)darwin/)
|
||||
|
||||
define BUNDLE
|
||||
$(q)BUNDLE_DIR=$(BINARY_OUTPUT)$(1)/bundle; \
|
||||
stepName=step-certificates_$(2); \
|
||||
mkdir -p $$BUNDLE_DIR $(RELEASE); \
|
||||
TMP=$$(mktemp -d $$BUNDLE_DIR/tmp.XXXX); \
|
||||
trap "rm -rf $$TMP" EXIT INT QUIT TERM; \
|
||||
newdir=$$TMP/$$stepName; \
|
||||
mkdir -p $$newdir/bin; \
|
||||
cp $(BINARY_OUTPUT)$(1)/bin/$(BINNAME) $$newdir/bin/; \
|
||||
cp README.md $$newdir/; \
|
||||
NEW_BUNDLE=$(RELEASE)/step-certificates_$(2)_$(1)_$(3).tar.gz; \
|
||||
rm -f $$NEW_BUNDLE; \
|
||||
tar -zcvf $$NEW_BUNDLE -C $$TMP $$stepName;
|
||||
endef
|
||||
|
||||
bundle-linux: binary-linux
|
||||
$(call BUNDLE,linux,$(VERSION),amd64)
|
||||
|
||||
bundle-darwin: binary-darwin
|
||||
$(call BUNDLE,darwin,$(VERSION),amd64)
|
||||
|
||||
.PHONY: binary-linux binary-darwin bundle-linux bundle-darwin
|
||||
|
||||
#################################################
|
||||
# Targets for creating OS specific artifacts
|
||||
#################################################
|
||||
|
||||
artifacts-linux-tag: bundle-linux debian
|
||||
|
||||
artifacts-darwin-tag: bundle-darwin
|
||||
|
||||
artifacts-tag: artifacts-linux-tag artifacts-darwin-tag
|
||||
|
||||
.PHONY: artifacts-linux-tag artifacts-darwin-tag artifacts-tag
|
||||
|
||||
#################################################
|
||||
# Targets for creating step artifacts
|
||||
#################################################
|
||||
|
||||
# For all builds that are not tagged
|
||||
artifacts-master:
|
||||
|
||||
# For all builds with a release tag
|
||||
artifacts-release: artifacts-tag
|
||||
|
||||
# This command is called by travis directly *after* a successful build
|
||||
artifacts: artifacts-$(PUSHTYPE) docker-$(PUSHTYPE)
|
||||
|
||||
.PHONY: artifacts-master artifacts-release artifacts
|
||||
|
||||
#########################################
|
||||
# Clean
|
||||
#########################################
|
||||
|
||||
clean:
|
||||
@echo "You will need to run 'make bootstrap' or 'dep ensure' directly to re-download any dependencies."
|
||||
$Q rm -rf vendor
|
||||
ifneq ($(BINNAME),"")
|
||||
$Q rm -f bin/$(BINNAME)
|
||||
endif
|
||||
|
||||
.PHONY: clean
|
||||
|
||||
#########################################
|
||||
# Dev
|
||||
#########################################
|
||||
|
||||
run:
|
||||
$Q go run cmd/step-ca/main.go $(shell step path)/config/ca.json
|
||||
|
||||
.PHONY: run
|
||||
|
||||
|
|
624
README.md
624
README.md
|
@ -1,131 +1,553 @@
|
|||
# Step Certificates
|
||||
|
||||
`step-ca` is an online certificate authority for secure, automated certificate management. It's the server counterpart to the [`step` CLI tool](https://github.com/smallstep/cli).
|
||||
An online certificate authority and related tools for secure automated
|
||||
certificate management, so you can use TLS everywhere.
|
||||
|
||||
You can use it to:
|
||||
- Issue X.509 certificates for your internal infrastructure:
|
||||
- HTTPS certificates that [work in browsers](https://smallstep.com/blog/step-v0-8-6-valid-HTTPS-certificates-for-dev-pre-prod.html) ([RFC5280](https://tools.ietf.org/html/rfc5280) and [CA/Browser Forum](https://cabforum.org/baseline-requirements-documents/) compliance)
|
||||
- TLS certificates for VMs, containers, APIs, mobile clients, database connections, printers, wifi networks, toaster ovens...
|
||||
- Client certificates to [enable mutual TLS (mTLS)](https://smallstep.com/hello-mtls) in your infra. mTLS is an optional feature in TLS where both client and server authenticate each other. Why add the complexity of a VPN when you can safely use mTLS over the public internet?
|
||||
- Issue SSH certificates:
|
||||
- For people, in exchange for single sign-on ID tokens
|
||||
- For hosts, in exchange for cloud instance identity documents
|
||||
- Easily automate certificate management:
|
||||
- It's an ACME v2 server
|
||||
- It has a JSON API
|
||||
- It comes with a [Go wrapper](./examples#user-content-basic-client-usage)
|
||||
- ... and there's a [command-line client](https://github.com/smallstep/cli) you can use in scripts!
|
||||
For more information and docs see [the Step website](https://smallstep.com/cli/)
|
||||
and the [blog post](https://smallstep.com/blog/step-certificates.html)
|
||||
announcing Step Certificate Authority.
|
||||
|
||||
Whatever your use case, `step-ca` is easy to use and hard to misuse, thanks to [safe, sane defaults](https://smallstep.com/docs/step-ca/certificate-authority-server-production#sane-cryptographic-defaults).
|
||||

|
||||
|
||||
---
|
||||
## Why?
|
||||
|
||||
**Don't want to run your own CA?**
|
||||
To get up and running quickly, or as an alternative to running your own `step-ca` server, consider creating a [free hosted smallstep Certificate Manager authority](https://info.smallstep.com/certificate-manager-early-access-mvp/).
|
||||
Managing your own *public key infrastructure* (PKI) can be tedious and error
|
||||
prone. Good security hygiene is hard. Setting up simple PKI is out of reach for
|
||||
many small teams, and following best practices like proper certificate revocation
|
||||
and rolling is challenging even for experts.
|
||||
|
||||
---
|
||||
Amongst numerous use cases, proper PKI makes it easy to use mTLS (mutual TLS) to improve security and to make it possible to connect services across the public internet. Unlike VPNs & SDNs, deploying and scaling mTLS is pretty easy. You're (hopefully) already using TLS, and your existing tools and standard libraries will provide most of what you need. If you know how to operate DNS and reverse proxies, you know how to operate mTLS infrastructure.
|
||||
|
||||
**Questions? Find us in [Discussions](https://github.com/smallstep/certificates/discussions) or [Join our Discord](https://u.step.sm/discord).**
|
||||

|
||||
|
||||
[Website](https://smallstep.com/certificates) |
|
||||
[Documentation](https://smallstep.com/docs) |
|
||||
[Installation](https://smallstep.com/docs/step-ca/installation) |
|
||||
[Getting Started](https://smallstep.com/docs/step-ca/getting-started) |
|
||||
[Contributor's Guide](./docs/CONTRIBUTING.md)
|
||||
There's just one problem: **you need certificates issued by your own certificate authority (CA)**. Building and operating a CA, issuing certificates, and making sure they're renewed before they expire is tricky. This project provides the infratructure, automations, and workflows you'll need.
|
||||
|
||||
[](https://github.com/smallstep/certificates/releases/latest)
|
||||
[](https://goreportcard.com/report/github.com/smallstep/certificates)
|
||||
[](https://github.com/smallstep/certificates)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://cla-assistant.io/smallstep/certificates)
|
||||
|
||||
[](https://github.com/smallstep/certificates/stargazers)
|
||||
[](https://twitter.com/intent/follow?screen_name=smallsteplabs)
|
||||
This project is part of smallstep's broader security architecture, which makes
|
||||
it much easier to implement good security practices early, and incrementally
|
||||
improve them as your system matures.
|
||||
|
||||

|
||||
> ## 🆕 Autocert
|
||||
> <a href="autocert/README.md"><img width="50%" src="https://raw.githubusercontent.com/smallstep/certificates/autocert/autocert/autocert-logo.png"></a>
|
||||
>
|
||||
> If you're using Kubernetes, make sure you [check out autocert](autocert/README.md): a kubernetes add-on that builds on `step certificates` to automatically injects TLS/HTTPS certificates into your containers.
|
||||
|
||||
## Features
|
||||
### Table of Contents
|
||||
|
||||
### 🦾 A fast, stable, flexible private CA
|
||||
- [Installing](#installing)
|
||||
- [Documentation](#documentation)
|
||||
- [Terminology](#terminology)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Commonly Asked Questions](docs/common-questions.md)
|
||||
- [Recommended Defaults](docs/recommendations.md)
|
||||
- [How To Create A New Release](docs/distribution.md)
|
||||
- [Versioning](#versioning)
|
||||
- [LICENSE](./LICENSE)
|
||||
- [CHANGELOG](./CHANGELOG.md)
|
||||
|
||||
Setting up a *public key infrastructure* (PKI) is out of reach for many small teams. `step-ca` makes it easier.
|
||||
|
||||
- Choose key types (RSA, ECDSA, EdDSA) and lifetimes to suit your needs
|
||||
- [Short-lived certificates](https://smallstep.com/blog/passive-revocation.html) with automated enrollment, renewal, and passive revocation
|
||||
- Capable of high availability (HA) deployment using [root federation](https://smallstep.com/blog/step-v0.8.3-federation-root-rotation.html) and/or multiple intermediaries
|
||||
- Can operate as [an online intermediate CA for an existing root CA](https://smallstep.com/docs/tutorials/intermediate-ca-new-ca)
|
||||
- [Badger, BoltDB, Postgres, and MySQL database backends](https://smallstep.com/docs/step-ca/configuration#databases)
|
||||
## Installing
|
||||
|
||||
### ⚙️ Many ways to automate
|
||||
These instructions will install an OS specific version of the `step` binary on
|
||||
your local machine.
|
||||
|
||||
There are several ways to authorize a request with the CA and establish a chain of trust that suits your flow.
|
||||
### Mac OS
|
||||
|
||||
You can issue certificates in exchange for:
|
||||
- [ACME challenge responses](#your-own-private-acme-server) from any ACMEv2 client
|
||||
- [OAuth OIDC single sign-on tokens](https://smallstep.com/blog/easily-curl-services-secured-by-https-tls.html), eg:
|
||||
- ID tokens from Okta, GSuite, Azure AD, Auth0.
|
||||
- ID tokens from an OAuth OIDC service that you host, like [Keycloak](https://www.keycloak.org/) or [Dex](https://github.com/dexidp/dex)
|
||||
- [Cloud instance identity documents](https://smallstep.com/blog/embarrassingly-easy-certificates-on-aws-azure-gcp/), for VMs on AWS, GCP, and Azure
|
||||
- [Single-use, short-lived JWK tokens](https://smallstep.com/docs/step-ca/provisioners#jwk) issued by your CD tool — Puppet, Chef, Ansible, Terraform, etc.
|
||||
- A trusted X.509 certificate (X5C provisioner)
|
||||
- A host certificate from your Nebula network
|
||||
- A SCEP challenge (SCEP provisioner)
|
||||
- An SSH host certificates needing renewal (the SSHPOP provisioner)
|
||||
- Learn more in our [provisioner documentation](https://smallstep.com/docs/step-ca/provisioners)
|
||||
Install `step` via [Homebrew](https://brew.sh/):
|
||||
|
||||
### 🏔 Your own private ACME server
|
||||
```
|
||||
brew install smallstep/smallstep/step
|
||||
```
|
||||
|
||||
ACME is the protocol used by Let's Encrypt to automate the issuance of HTTPS certificates. It's _super easy_ to issue certificates to any ACMEv2 ([RFC8555](https://tools.ietf.org/html/rfc8555)) client.
|
||||
### Linux
|
||||
|
||||
- [Use ACME in development & pre-production](https://smallstep.com/blog/private-acme-server/#local-development--pre-production)
|
||||
- Supports the most popular [ACME challenge types](https://letsencrypt.org/docs/challenge-types/):
|
||||
- For `http-01`, place a token at a well-known URL to prove that you control the web server
|
||||
- For `dns-01`, add a `TXT` record to prove that you control the DNS record set
|
||||
- For `tls-alpn-01`, respond to the challenge at the TLS layer ([as Caddy does](https://caddy.community/t/caddy-supports-the-acme-tls-alpn-challenge/4860)) to prove that you control the web server
|
||||
Download the latest Debian package from [releases](https://github.com/smallstep/certificates/releases):
|
||||
|
||||
- Works with any ACME client. We've written examples for:
|
||||
- [certbot](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#certbot)
|
||||
- [acme.sh](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#acmesh)
|
||||
- [win-acme](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#win-acme)
|
||||
- [Caddy](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#caddy-v2)
|
||||
- [Traefik](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#traefik)
|
||||
- [Apache](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#apache)
|
||||
- [nginx](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#nginx)
|
||||
- Get certificates programmatically using ACME, using these libraries:
|
||||
- [`lego`](https://github.com/go-acme/lego) for Golang ([example usage](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#golang))
|
||||
- certbot's [`acme` module](https://github.com/certbot/certbot/tree/master/acme) for Python ([example usage](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#python))
|
||||
- [`acme-client`](https://github.com/publishlab/node-acme-client) for Node.js ([example usage](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#node))
|
||||
- Our own [`step` CLI tool](https://github.com/smallstep/cli) is also an ACME client!
|
||||
- See our [ACME tutorial](https://smallstep.com/docs/tutorials/acme-challenge) for more
|
||||
```
|
||||
wget https://github.com/smallstep/certificates/releases/download/X.Y.Z/step-certificates_X.Y.Z_amd64.deb
|
||||
```
|
||||
|
||||
### 👩🏽💻 An online SSH Certificate Authority
|
||||
Install the Debian package:
|
||||
|
||||
- Delegate SSH authentication to `step-ca` by using [SSH certificates](https://smallstep.com/blog/use-ssh-certificates/) instead of public keys and `authorized_keys` files
|
||||
- For user certificates, [connect SSH to your single sign-on provider](https://smallstep.com/blog/diy-single-sign-on-for-ssh/), to improve security with short-lived certificates and MFA (or other security policies) via any OAuth OIDC provider.
|
||||
- For host certificates, improve security, [eliminate TOFU warnings](https://smallstep.com/blog/use-ssh-certificates/), and set up automated host certificate renewal.
|
||||
|
||||
### 🤓 A general purpose PKI tool, via [`step` CLI](https://github.com/smallstep/cli) [integration](https://smallstep.com/docs/step-cli/reference/ca/)
|
||||
|
||||
- Generate key pairs where they're needed so private keys are never transmitted across the network
|
||||
- [Authenticate and obtain a certificate](https://smallstep.com/docs/step-cli/reference/ca/certificate/) using any provisioner supported by `step-ca`
|
||||
- Securely [distribute root certificates](https://smallstep.com/docs/step-cli/reference/ca/root/) and [bootstrap](https://smallstep.com/docs/step-cli/reference/ca/bootstrap/) PKI relying parties
|
||||
- [Renew](https://smallstep.com/docs/step-cli/reference/ca/renew/) and [revoke](https://smallstep.com/docs/step-cli/reference/ca/revoke/) certificates issued by `step-ca`
|
||||
- [Install root certificates](https://smallstep.com/docs/step-cli/reference/certificate/install/) on your machine and browsers, so your CA is trusted
|
||||
- [Inspect](https://smallstep.com/docs/step-cli/reference/certificate/inspect/) and [lint](https://smallstep.com/docs/step-cli/reference/certificate/lint/) certificates
|
||||
|
||||
## Installation
|
||||
|
||||
See our installation docs [here](https://smallstep.com/docs/step-ca/installation).
|
||||
```
|
||||
sudo dpkg -i step-certificates_X.Y.Z_amd64.deb
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Official documentation](https://smallstep.com/docs/step-ca) is on smallstep.com
|
||||
* The `step` command reference is available via `step help`,
|
||||
[on smallstep.com](https://smallstep.com/docs/step-cli/reference/),
|
||||
or by running `step help --http=:8080` from the command line
|
||||
and visiting http://localhost:8080.
|
||||
Documentation can be found in three places:
|
||||
|
||||
## Feedback?
|
||||
1. On the command line with `step ca help xxx` where `xxx` is the subcommand you are interested in. Ex: `step help ca provisioners list`
|
||||
|
||||
* Tell us what you like and don't like about managing your PKI - we're eager to help solve problems in this space.
|
||||
* Tell us about a feature you'd like to see! [Add a feature request Issue](https://github.com/smallstep/certificates/issues/new?assignees=&labels=enhancement%2C+needs+triage&template=enhancement.md&title=), [ask on Discussions](https://github.com/smallstep/certificates/discussions), or hit us up on [Twitter](https://twitter.com/smallsteplabs).
|
||||
2. On the web at https://smallstep.com/docs/certificates
|
||||
|
||||
3. In your browser with `step ca help --http :8080` and visiting http://localhost:8080
|
||||
|
||||
## Terminology
|
||||
|
||||
### PKI - Public Key Infrastructure
|
||||
|
||||
A set of roles, policies, and procedures needed to create, manage, distribute,
|
||||
use, store, and revoke digital certificates and manage public-key encryption.
|
||||
The purpose of a PKI is to facilitate the secure electronic transfer of
|
||||
information for a range of network activities.
|
||||
|
||||
### Provisioners
|
||||
|
||||
Provisioners are people or code that are registered with the CA and authorized
|
||||
to issue "provisioning tokens". Provisioning tokens are single use tokens that
|
||||
can be used to authenticate with the CA and get a certificate.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Demonstrates setting up your own PKI and certificate authority using `step ca`
|
||||
and getting certificates using the `step` command line tool and SDK.
|
||||
|
||||

|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. [Step CLI](https://github.com/smallstep/cli/blob/master/README.md#installing)
|
||||
|
||||
2. [Step CA](#installing)
|
||||
|
||||
### Initializing PKI and configuring the Certificate Authority
|
||||
|
||||
To initialize a PKI and configure the Step Certificate Authority run:
|
||||
|
||||
```
|
||||
step ca init
|
||||
```
|
||||
|
||||
You'll be asked for a name for your PKI. This name will appear in your CA
|
||||
certificates. It doesn't really matter what you choose. The name of your
|
||||
organization or your project will suffice.
|
||||
|
||||
If you run:
|
||||
|
||||
```
|
||||
tree $(step path)
|
||||
```
|
||||
|
||||
You should see:
|
||||
|
||||
```
|
||||
.
|
||||
├── certs
|
||||
│ ├── intermediate_ca.crt
|
||||
│ └── root_ca.crt
|
||||
├── config
|
||||
│ ├── ca.json
|
||||
│ └── defaults.json
|
||||
└── secrets
|
||||
├── intermediate_ca_key
|
||||
└── root_ca_key
|
||||
```
|
||||
|
||||
The files created include:
|
||||
|
||||
* `root_ca.crt` and `root_ca_key`: the root certificate and private key for
|
||||
your PKI
|
||||
* `intermediate_ca.crt` and `intermediate_ca_key`: the intermediate certificate
|
||||
and private key that will be used to sign leaf certificates
|
||||
* `ca.json`: the configuration file necessary for running the Step CA.
|
||||
* `defaults.json`: file containing default parameters for the `step` CA cli
|
||||
interface. You can override these values with the appropriate flags or
|
||||
environment variables.
|
||||
|
||||
All of the files endinging in `_key` are password protected using the password
|
||||
you chose during PKI initialization. We advise you to change these passwords
|
||||
(using the `step crypto change-pass` utility) if you plan to run your CA in a
|
||||
non-development environment.
|
||||
|
||||
### What's Inside `ca.json`?
|
||||
|
||||
`ca.json` is responsible for configuring communication, authorization, and
|
||||
default new certificate values for the Step CA. Below is a short list of
|
||||
definitions and descriptions of available configuration attributes.
|
||||
|
||||
* `root`: location of the root certificate on the filesystem. The root certificate
|
||||
is used to mutually authenticate all api clients of the CA.
|
||||
|
||||
* `crt`: location of the intermediate certificate on the filesystem. The
|
||||
intermediate certificate is returned alongside each new certificate,
|
||||
allowing the client to complete the certificate chain.
|
||||
|
||||
* `key`: location of the intermediate private key on the filesystem. The
|
||||
intermediate key signs all new certificates generated by the CA.
|
||||
|
||||
* `password`: optionally store the password for decrypting the intermediate private
|
||||
key (this should be the same password you chose during PKI initialization). If
|
||||
the value is not stored in configuration then you will be prompted for it when
|
||||
starting the CA.
|
||||
|
||||
* `address`: e.g. `127.0.0.1:8080` - address and port on which the CA will bind
|
||||
and respond to requests.
|
||||
|
||||
* `dnsNames`: comma separated list of DNS Name(s) for the CA.
|
||||
|
||||
* `logger`: the default logging format for the CA is `text`. The other options
|
||||
is `json`.
|
||||
|
||||
* `tls`: settings for negotiating communication with the CA; includes acceptable
|
||||
ciphersuites, min/max TLS version, etc.
|
||||
|
||||
* `authority`: controls the request authorization and signature processes.
|
||||
|
||||
- `template`: default ASN1DN values for new certificates.
|
||||
|
||||
- `claims`: default validation for requested attributes in the certificate request.
|
||||
Can be overriden by similar claims objects defined by individual provisioners.
|
||||
|
||||
* `minTLSCertDuration`: do not allow certificates with a duration less
|
||||
than this value.
|
||||
|
||||
* `maxTLSCertDuration`: do not allow certificates with a duration greater
|
||||
than this value.
|
||||
|
||||
* `defaultTLSCertDuration`: if no certificate validity period is specified,
|
||||
use this value.
|
||||
|
||||
* `disableIssuedAtCheck`: disable a check verifying that provisioning
|
||||
tokens must be issued after the CA has booted. This is one prevention
|
||||
against token reuse. The default value is `false`. Do not change this
|
||||
unless you know what you are doing.
|
||||
|
||||
- `provisioners`: list of provisioners. Each provisioner has a `name`,
|
||||
associated public/private keys, and an optional `claims` attribute that will
|
||||
override any values set in the global `claims` directly underneath `authority`.
|
||||
|
||||
|
||||
`step ca init` will generate one provisioner. New provisioners can be added by
|
||||
running `step ca provisioner add`.
|
||||
|
||||
### Running the CA
|
||||
|
||||
To start the CA run:
|
||||
|
||||
```
|
||||
export STEPPATH=$(step path)
|
||||
step-ca $STEPPATH/config/ca.json
|
||||
```
|
||||
|
||||
### Configure Your Environment
|
||||
|
||||
**Note**: Configuring your environment is only necessary for remote servers
|
||||
(not the server on which the `step ca init` command was originally run).
|
||||
|
||||
Many of the cli utilities under `step ca [sub-command]` interface directly with
|
||||
a running instance of the Step CA. The CA exposes an HTTP API and clients are
|
||||
required to connect using HTTP over TLS (aka HTTPS). As part of bootstraping the
|
||||
Step CA, a certificate was generated using the root of trust that was
|
||||
created when you initilialized your PKI. In order to properly validate this
|
||||
certificate clients need access to the public root of trust, aka the public
|
||||
root certificate. If you are using the Step CLI on the same host where you
|
||||
initialized your PKI (the `root_ca.crt` is stored on disk locally), then you
|
||||
can continue to setting up a `default.json`, otherwise we will show you
|
||||
how to easily download your root certificate in the following step.
|
||||
|
||||
#### Download the Root Certificate
|
||||
|
||||
The next few steps are a guide for downloading the root certificate of your PKI
|
||||
from a running instance of the CA. First we'll define two servers:
|
||||
|
||||
* **remote server**: This is the server where the Step CA is running. This may
|
||||
also be the server where you initialized your PKI, but for security reasons
|
||||
you may have done that offline.
|
||||
|
||||
* **local server**: This is the server that wants access to the `step ca [sub-command]`
|
||||
|
||||
* **ca-url**: This is the url at which the CA is listening for requests. This
|
||||
should be a combination of the DNS name and port entered during PKI initialization.
|
||||
In the examples below we will use `https://ca.smallstep.com:8080`.
|
||||
|
||||
1. Get the Fingerprint.
|
||||
|
||||
From the **remote server**:
|
||||
|
||||
```
|
||||
$ FP=$(step certificate fingerprint $(step path)/certs/root_ca.crt)
|
||||
```
|
||||
|
||||
2. Bootstrap your environment.
|
||||
|
||||
From the **local server**:
|
||||
|
||||
```
|
||||
$ step ca bootstrap --fingerprint $FP --ca-url "https://ca.smallstep.com:8080"
|
||||
$ cat $(step path)/config/defaults.json
|
||||
```
|
||||
|
||||
3. Test.
|
||||
|
||||
```
|
||||
* step ca health
|
||||
```
|
||||
|
||||
#### Setting up Environment Defaults
|
||||
This is optional, but we recommend you populate a `defaults.json` file with a
|
||||
few variables that will make your command line experience much more pleasant.
|
||||
|
||||
You can do this manually or with the step command `step ca bootstrap`:
|
||||
|
||||
```
|
||||
$ step ca bootstrap \
|
||||
--ca-url https://ca.smallstep.com:8080 \
|
||||
--fingerprint 0d7d3834cf187726cf331c40a31aa7ef6b29ba4df601416c9788f6ee01058cf3
|
||||
# Let's see what we got...
|
||||
$ cat $STEPPATH/config/defaults.json
|
||||
{
|
||||
"ca-url": "https://ca.smallstep.com:8080",
|
||||
"fingerprint": "628cfc85090ca65bb246d224f1217445be155cfc6167db4ed8f1b0e3de1447c5",
|
||||
"root": "/Users/<you>/src/github.com/smallstep/step/.step/certs/root_ca.crt"
|
||||
}
|
||||
# Test it out
|
||||
$ step ca health
|
||||
```
|
||||
|
||||
* **ca-url** is the DNS name and port that you used when initializing the CA.
|
||||
|
||||
* **root** is the path to the root certificate on the file system.
|
||||
|
||||
* **fingerprint** is the root certificate fingerprint (SHA256).
|
||||
|
||||
You can always override these values with command-line flags or environment
|
||||
variables.
|
||||
|
||||
To manage the CA provisioners you can also add the property **ca-config** with
|
||||
the path to the CA configuration file, with that property you won't need to add
|
||||
it in commands like `step ca provisioners [add|remove]`.
|
||||
**Note**: to manage provisioners you must be on the host on which the CA is
|
||||
running. You need direct access to the `ca.json` file.
|
||||
|
||||
### Hot Reload
|
||||
|
||||
It is important that the CA be able to handle configuration changes with no downtime.
|
||||
Our CA has a built in `reload` function allowing it to:
|
||||
|
||||
1. Finish processing existing connections while blocking new ones.
|
||||
2. Parse the configuration file and re-initialize the API.
|
||||
3. Begin accepting blocked and new connections.
|
||||
|
||||
`reload` is triggered by sending a SIGHUP to the PID (see `man kill`
|
||||
for your OS) of the Step CA process. A few important details to note when using `reload`:
|
||||
|
||||
* The location of the modified configuration must be in the same location as it
|
||||
was in the original invocation of `step-ca`. So, if the original command was
|
||||
|
||||
```
|
||||
$ step-ca ./.step/config/ca.json
|
||||
```
|
||||
|
||||
then, upon `reload`, the Step CA will read it's new configuration from the same
|
||||
configuration file.
|
||||
|
||||
* Step CA requires the password to decrypt the intermediate certificate, again,
|
||||
upon `reload`. You can automate this in one of two ways:
|
||||
|
||||
* Use the `--password-file` flag in the original invocation.
|
||||
* Use the top level `password` attribute in the `ca.json` configuration file.
|
||||
|
||||
### Let's issue a certificate!
|
||||
|
||||
There are two steps to issuing a certificate at the command line:
|
||||
|
||||
1. Generate a provisioning token using your provisioning credentials.
|
||||
2. Generate a CSR and exchange it, along with the provisioning token, for a certificate.
|
||||
|
||||
If you would like to generate a certificate from the command line, the Step CLI
|
||||
provides a single command that will prompt you to select and decrypt an
|
||||
authorized provisioner and then request a new certificate.
|
||||
|
||||
```
|
||||
$ step ca certificate "foo.example.com" foo.crt foo.key
|
||||
```
|
||||
|
||||
If you would like to generate certificates on demand from an automated
|
||||
configuration management solution (no user input) you would split the above flow
|
||||
into two commands.
|
||||
|
||||
```
|
||||
$ TOKEN=$(step ca token foo.example.com \
|
||||
--kid 4vn46fbZT68Uxfs9LBwHkTvrjEvxQqx-W8nnE-qDjts \
|
||||
--ca-url https://ca.example.com \
|
||||
--root /path/to/root_ca.crt --password-file /path/to/provisioner/password)
|
||||
|
||||
$ step ca certificate "foo.example.com" foo.crt foo.key --token "$TOKEN"
|
||||
```
|
||||
|
||||
You can take a closer look at the contents of the certificate using `step certificate inspect`:
|
||||
|
||||
```
|
||||
$ step certificate inspect foo.crt
|
||||
```
|
||||
|
||||
### List|Add|Remove Provisioners
|
||||
|
||||
The Step CA configuration is initialized with one provisioner; one entity
|
||||
that is authorized by the CA to generate provisioning tokens for new certificates.
|
||||
We encourage you to have many provisioners - ideally one for each entity in your
|
||||
infrastructure.
|
||||
|
||||
**Why should I be using multiple provisioners?**
|
||||
|
||||
* Each certificate generated by the Step CA contains the ID of the provisioner
|
||||
that issued the *provisioning token* authorizing the creation of the cert. This
|
||||
ID is stored in the X.509 ExtraExtensions of the certificate under
|
||||
`OID: 1.3.6.1.4.1.37476.9000.64.1` and can be inspected by running `step
|
||||
certificate inspect foo.crt`. These IDs can and should be used to debug and
|
||||
gather information about the origin of a certificate. If every member of your
|
||||
ops team and the configuration management tools all use the same provisioner
|
||||
to authorize new certificates you lose valuable visibility into the workings
|
||||
of your PKI.
|
||||
* Each provisioner should require a **unique** password to decrypt it's private key
|
||||
-- we can generate unique passwords for you but we can't force you to use them.
|
||||
If you only have one provisioner then every entity in the infrastructure will
|
||||
need access to that one password. Jim from your dev ops team should not be using
|
||||
the same provisioner/password combo to authorize certificates for debugging as
|
||||
Chef is for your CICD - no matter how trustworthy Jim says he is.
|
||||
|
||||
Let's begin by listing the existing provisioners:
|
||||
|
||||
```
|
||||
$ bin/step ca provisioner list
|
||||
```
|
||||
|
||||
Now let's add a provisioner for Jim.
|
||||
|
||||
```
|
||||
$ bin/step ca provisioner add jim@smallstep.com --create
|
||||
```
|
||||
|
||||
**NOTE**: This change will not affect the Step CA until a `reload` is forced by
|
||||
sending a SIGHUP signal to the process.
|
||||
|
||||
List the provisioners again and you will see that nothing has changed.
|
||||
|
||||
```
|
||||
$ bin/step ca provisioner list
|
||||
```
|
||||
|
||||
Now let's `reload` the CA. You will need to re-enter your intermediate
|
||||
password unless it's in your `ca.json` or your are using `--password-file`.
|
||||
|
||||
```
|
||||
$ ps aux | grep step-ca # to get the PID
|
||||
$ kill -1 <pid>
|
||||
```
|
||||
|
||||
Once the CA is running again, list the provisioners, again.
|
||||
|
||||
```
|
||||
$ bin/step ca provisioner list
|
||||
```
|
||||
|
||||
Boom! Magic.
|
||||
Now suppose Jim forgets his password ('come on Jim!'), and he'd like to remove
|
||||
his old provisioner. Get the `kid` (Key ID) of Jim's provisioner by listing
|
||||
the provisioners and finding the appropriate one. Then run:
|
||||
|
||||
```
|
||||
$ bin/step ca provisioner remove jim@smallstep.com --kid <kid>
|
||||
```
|
||||
|
||||
Then `reload` the CA and verify that Jim's provisioner is no longer returned
|
||||
in the provisioner list.
|
||||
|
||||
We can also remove all of Jim's provisioners, supposing Jim forgot all the passwords
|
||||
('really Jim?'), by running the following:
|
||||
|
||||
```
|
||||
$ bin/step ca provisioner remove jim@smallstep.com --all
|
||||
```
|
||||
|
||||
The same entity may have multiple provisioners for authorizing different
|
||||
types of certs. Each of these provisioners must have unique keys.
|
||||
|
||||
## Notes on Securing the Step CA and your PKI.
|
||||
|
||||
In this section we recommend a few best practices when it comes to
|
||||
running, deploying, and managing your own online CA and PKI. Security is a moving
|
||||
target and we expect out recommendations to change and evolve as well.
|
||||
|
||||
### Initializing your PKI
|
||||
|
||||
When you initialize your PKI two private keys are generated; one intermediate
|
||||
private key and one root private key. It is very important that these private keys
|
||||
are kept secret. The root private key should be moved around as little as possible,
|
||||
preferably not all - meaning it never leaves the server on which it was created.
|
||||
|
||||
### Passwords
|
||||
|
||||
When you intialize your PKI (`step ca init`) the root and intermediate
|
||||
private keys will be encrypted with the same password. We recommend that you
|
||||
change the password with which the intermediate was encrypted at your earliest
|
||||
convenience.
|
||||
|
||||
```
|
||||
$ step crypto change-pass $STEPPATH/secrets/intermediate_ca_key
|
||||
```
|
||||
|
||||
Once you've changed the intermediate private key password you should never have
|
||||
to use the root private key password again.
|
||||
|
||||
We encourage users to always use a password manager to generate random passwords
|
||||
or let Step CLI generate passwords for you.
|
||||
|
||||
The next important matter is how your passwords are stored. We recommend using a
|
||||
[password manager](https://en.wikipedia.org/wiki/List_of_password_managers).
|
||||
There are many to choose from and the choice will depend on the risk & security
|
||||
profile of your organization.
|
||||
|
||||
In addition to using a password manager to store all passwords (private key,
|
||||
provisioner, etc.) we recommend using a threshold cryptography algorithm like
|
||||
[Shamir's Secret Sharing](https://en.wikipedia.org/wiki/Shamir's_Secret_Sharing)
|
||||
to divide the root private key across a handful of trusted parties.
|
||||
|
||||
### Provisioners
|
||||
|
||||
When you intialize your PKI (`step ca init`) a default provisioner will be created
|
||||
and it's private key will be encrypted using the same password used to encrypt
|
||||
the root private key. Before deploying the Step CA you should remove this
|
||||
provisioner and add new ones that are encrypted with new, secure, random passwords.
|
||||
See the section on [managing provisioners](#listaddremove-provisioners).
|
||||
|
||||
### Deploying
|
||||
|
||||
* Refrain from entering passwords for private keys or provisioners on the command line.
|
||||
Use the `--password-file` flag whenever possible.
|
||||
* Run the Step CA as a new user and make sure that the config files, private keys,
|
||||
and passwords used by the CA are stored in such a way that only this new user
|
||||
has permissions to read and write them.
|
||||
* Use short lived certificates. Our default validity period for new certificates
|
||||
is 24 hours. You can configure this value in the `ca.json` file. Shorter is
|
||||
better - less time to form an attack.
|
||||
* Short lived certificates are **not** a replacement for CRL and OCSP. CRL and OCSP
|
||||
are features that we plan to implement, but are not yet available. In the mean
|
||||
time short lived certificates are a decent alternative.
|
||||
* Keep your hosts secure by enforcing AuthN and AuthZ for every connection. SSH
|
||||
access is a big one.
|
||||
|
||||
## The Future
|
||||
|
||||
We plan to build more tools that facilitate the use and management of zero trust
|
||||
networks.
|
||||
|
||||
* Tell us what you like and don't like about managing your PKI - we're eager to
|
||||
help solve problems in this space.
|
||||
* Tell us what features you'd like to see - open issues or hit us on
|
||||
[Twitter](https://twitter.com/smallsteplabs).
|
||||
|
||||
## Versioning
|
||||
|
||||
We use [SemVer](http://semver.org/) for versioning. For the versions available,
|
||||
see the [tags on this repository](https://github.com/smallstep/cli).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Apache 2.0 License - see the
|
||||
[LICENSE](./LICENSE) file for details
|
||||
|
||||
### Individual Contributor License
|
||||
|
||||
[](https://cla-assistant.io/smallstep/certificates)
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
We appreciate any effort to discover and disclose security vulnerabilities responsibly.
|
||||
|
||||
If you would like to report a vulnerability in one of our projects, or have security concerns regarding Smallstep software, please email security@smallstep.com.
|
||||
|
||||
In order for us to best respond to your report, please include any of the following:
|
||||
* Steps to reproduce or proof-of-concept
|
||||
* Any relevant tools, including versions used
|
||||
* Tool output
|
134
acme/account.go
134
acme/account.go
|
@ -1,134 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
|
||||
"github.com/smallstep/certificates/authority/policy"
|
||||
)
|
||||
|
||||
// Account is a subset of the internal account type containing only those
|
||||
// attributes required for responses in the ACME protocol.
|
||||
type Account struct {
|
||||
ID string `json:"-"`
|
||||
Key *jose.JSONWebKey `json:"-"`
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
Status Status `json:"status"`
|
||||
OrdersURL string `json:"orders"`
|
||||
ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"`
|
||||
LocationPrefix string `json:"-"`
|
||||
ProvisionerName string `json:"-"`
|
||||
}
|
||||
|
||||
// GetLocation returns the URL location of the given account.
|
||||
func (a *Account) GetLocation() string {
|
||||
if a.LocationPrefix == "" {
|
||||
return ""
|
||||
}
|
||||
return a.LocationPrefix + a.ID
|
||||
}
|
||||
|
||||
// ToLog enables response logging.
|
||||
func (a *Account) ToLog() (interface{}, error) {
|
||||
b, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
return nil, WrapErrorISE(err, "error marshaling account for logging")
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// IsValid returns true if the Account is valid.
|
||||
func (a *Account) IsValid() bool {
|
||||
return a.Status == StatusValid
|
||||
}
|
||||
|
||||
// KeyToID converts a JWK to a thumbprint.
|
||||
func KeyToID(jwk *jose.JSONWebKey) (string, error) {
|
||||
kid, err := jwk.Thumbprint(crypto.SHA256)
|
||||
if err != nil {
|
||||
return "", WrapErrorISE(err, "error generating jwk thumbprint")
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(kid), nil
|
||||
}
|
||||
|
||||
// PolicyNames contains ACME account level policy names
|
||||
type PolicyNames struct {
|
||||
DNSNames []string `json:"dns"`
|
||||
IPRanges []string `json:"ips"`
|
||||
}
|
||||
|
||||
// X509Policy contains ACME account level X.509 policy
|
||||
type X509Policy struct {
|
||||
Allowed PolicyNames `json:"allow"`
|
||||
Denied PolicyNames `json:"deny"`
|
||||
AllowWildcardNames bool `json:"allowWildcardNames"`
|
||||
}
|
||||
|
||||
// Policy is an ACME Account level policy
|
||||
type Policy struct {
|
||||
X509 X509Policy `json:"x509"`
|
||||
}
|
||||
|
||||
func (p *Policy) GetAllowedNameOptions() *policy.X509NameOptions {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return &policy.X509NameOptions{
|
||||
DNSDomains: p.X509.Allowed.DNSNames,
|
||||
IPRanges: p.X509.Allowed.IPRanges,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Policy) GetDeniedNameOptions() *policy.X509NameOptions {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return &policy.X509NameOptions{
|
||||
DNSDomains: p.X509.Denied.DNSNames,
|
||||
IPRanges: p.X509.Denied.IPRanges,
|
||||
}
|
||||
}
|
||||
|
||||
// AreWildcardNamesAllowed returns if wildcard names
|
||||
// like *.example.com are allowed to be signed.
|
||||
// Defaults to false.
|
||||
func (p *Policy) AreWildcardNamesAllowed() bool {
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
return p.X509.AllowWildcardNames
|
||||
}
|
||||
|
||||
// ExternalAccountKey is an ACME External Account Binding key.
|
||||
type ExternalAccountKey struct {
|
||||
ID string `json:"id"`
|
||||
ProvisionerID string `json:"provisionerID"`
|
||||
Reference string `json:"reference"`
|
||||
AccountID string `json:"-"`
|
||||
HmacKey []byte `json:"-"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
BoundAt time.Time `json:"boundAt,omitempty"`
|
||||
Policy *Policy `json:"policy,omitempty"`
|
||||
}
|
||||
|
||||
// AlreadyBound returns whether this EAK is already bound to
|
||||
// an ACME Account or not.
|
||||
func (eak *ExternalAccountKey) AlreadyBound() bool {
|
||||
return !eak.BoundAt.IsZero()
|
||||
}
|
||||
|
||||
// BindTo binds the EAK to an Account.
|
||||
// It returns an error if it's already bound.
|
||||
func (eak *ExternalAccountKey) BindTo(account *Account) error {
|
||||
if eak.AlreadyBound() {
|
||||
return NewError(ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", eak.ID, eak.AccountID, eak.BoundAt)
|
||||
}
|
||||
eak.AccountID = account.ID
|
||||
eak.BoundAt = time.Now()
|
||||
eak.HmacKey = []byte{} // clearing the key bytes; can only be used once
|
||||
return nil
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.step.sm/crypto/jose"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
)
|
||||
|
||||
func TestKeyToID(t *testing.T) {
|
||||
type test struct {
|
||||
jwk *jose.JSONWebKey
|
||||
exp string
|
||||
err *Error
|
||||
}
|
||||
tests := map[string]func(t *testing.T) test{
|
||||
"fail/error-generating-thumbprint": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
jwk.Key = "foo"
|
||||
return test{
|
||||
jwk: jwk,
|
||||
err: NewErrorISE("error generating jwk thumbprint: square/go-jose: unknown key type 'string'"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
kid, err := jwk.Thumbprint(crypto.SHA256)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
return test{
|
||||
jwk: jwk,
|
||||
exp: base64.RawURLEncoding.EncodeToString(kid),
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run(t)
|
||||
if id, err := KeyToID(tc.jwk); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, id, tc.exp)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccount_GetLocation(t *testing.T) {
|
||||
locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/"
|
||||
type test struct {
|
||||
acc *Account
|
||||
exp string
|
||||
}
|
||||
tests := map[string]test{
|
||||
"empty": {acc: &Account{LocationPrefix: ""}, exp: ""},
|
||||
"not-empty": {acc: &Account{ID: "bar", LocationPrefix: locationPrefix}, exp: locationPrefix + "bar"},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equals(t, tc.acc.GetLocation(), tc.exp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccount_IsValid(t *testing.T) {
|
||||
type test struct {
|
||||
acc *Account
|
||||
exp bool
|
||||
}
|
||||
tests := map[string]test{
|
||||
"valid": {acc: &Account{Status: StatusValid}, exp: true},
|
||||
"invalid": {acc: &Account{Status: StatusInvalid}, exp: false},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equals(t, tc.acc.IsValid(), tc.exp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalAccountKey_BindTo(t *testing.T) {
|
||||
boundAt := time.Now()
|
||||
tests := []struct {
|
||||
name string
|
||||
eak *ExternalAccountKey
|
||||
acct *Account
|
||||
err *Error
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
eak: &ExternalAccountKey{
|
||||
ID: "eakID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "ref",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
},
|
||||
acct: &Account{
|
||||
ID: "accountID",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "fail/already-bound",
|
||||
eak: &ExternalAccountKey{
|
||||
ID: "eakID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "ref",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
AccountID: "someAccountID",
|
||||
BoundAt: boundAt,
|
||||
},
|
||||
acct: &Account{
|
||||
ID: "accountID",
|
||||
},
|
||||
err: NewError(ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", "eakID", "someAccountID", boundAt),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
eak := tt.eak
|
||||
acct := tt.acct
|
||||
err := eak.BindTo(acct)
|
||||
wantErr := tt.err != nil
|
||||
gotErr := err != nil
|
||||
if wantErr != gotErr {
|
||||
t.Errorf("ExternalAccountKey.BindTo() error = %v, wantErr %v", err, tt.err)
|
||||
}
|
||||
if wantErr {
|
||||
assert.NotNil(t, err)
|
||||
var ae *Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.Equals(t, ae.Type, tt.err.Type)
|
||||
assert.Equals(t, ae.Detail, tt.err.Detail)
|
||||
assert.Equals(t, ae.Subproblems, tt.err.Subproblems)
|
||||
}
|
||||
} else {
|
||||
assert.Equals(t, eak.AccountID, acct.ID)
|
||||
assert.Equals(t, eak.HmacKey, []byte{})
|
||||
assert.NotNil(t, eak.BoundAt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,254 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
// NewAccountRequest represents the payload for a new account request.
|
||||
type NewAccountRequest struct {
|
||||
Contact []string `json:"contact"`
|
||||
OnlyReturnExisting bool `json:"onlyReturnExisting"`
|
||||
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
|
||||
ExternalAccountBinding *ExternalAccountBinding `json:"externalAccountBinding,omitempty"`
|
||||
}
|
||||
|
||||
func validateContacts(cs []string) error {
|
||||
for _, c := range cs {
|
||||
if c == "" {
|
||||
return acme.NewError(acme.ErrorMalformedType, "contact cannot be empty string")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates a new-account request body.
|
||||
func (n *NewAccountRequest) Validate() error {
|
||||
if n.OnlyReturnExisting && len(n.Contact) > 0 {
|
||||
return acme.NewError(acme.ErrorMalformedType, "incompatible input; onlyReturnExisting must be alone")
|
||||
}
|
||||
return validateContacts(n.Contact)
|
||||
}
|
||||
|
||||
// UpdateAccountRequest represents an update-account request.
|
||||
type UpdateAccountRequest struct {
|
||||
Contact []string `json:"contact"`
|
||||
Status acme.Status `json:"status"`
|
||||
}
|
||||
|
||||
// Validate validates a update-account request body.
|
||||
func (u *UpdateAccountRequest) Validate() error {
|
||||
switch {
|
||||
case len(u.Status) > 0 && len(u.Contact) > 0:
|
||||
return acme.NewError(acme.ErrorMalformedType, "incompatible input; contact and "+
|
||||
"status updates are mutually exclusive")
|
||||
case len(u.Contact) > 0:
|
||||
if err := validateContacts(u.Contact); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case len(u.Status) > 0:
|
||||
if u.Status != acme.StatusDeactivated {
|
||||
return acme.NewError(acme.ErrorMalformedType, "cannot update account "+
|
||||
"status to %s, only deactivated", u.Status)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
// According to the ACME spec (https://tools.ietf.org/html/rfc8555#section-7.3.2)
|
||||
// accountUpdate should ignore any fields not recognized by the server.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// getAccountLocationPath returns the current account URL location.
|
||||
// Returned location will be of the form: https://<ca-url>/acme/<provisioner>/account/<accID>
|
||||
func getAccountLocationPath(ctx context.Context, linker acme.Linker, accID string) string {
|
||||
return linker.GetLink(ctx, acme.AccountLinkType, accID)
|
||||
}
|
||||
|
||||
// NewAccount is the handler resource for creating new ACME accounts.
|
||||
func NewAccount(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
payload, err := payloadFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
var nar NewAccountRequest
|
||||
if err := json.Unmarshal(payload.value, &nar); err != nil {
|
||||
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
|
||||
"failed to unmarshal new-account request payload"))
|
||||
return
|
||||
}
|
||||
if err := nar.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov, err := acmeProvisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpStatus := http.StatusCreated
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
var acmeErr *acme.Error
|
||||
if !errors.As(err, &acmeErr) || acmeErr.Status != http.StatusBadRequest {
|
||||
// Something went wrong ...
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Account does not exist //
|
||||
if nar.OnlyReturnExisting {
|
||||
render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType,
|
||||
"account does not exist"))
|
||||
return
|
||||
}
|
||||
|
||||
jwk, err := jwkFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
eak, err := validateExternalAccountBinding(ctx, &nar)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
acc = &acme.Account{
|
||||
Key: jwk,
|
||||
Contact: nar.Contact,
|
||||
Status: acme.StatusValid,
|
||||
LocationPrefix: getAccountLocationPath(ctx, linker, ""),
|
||||
ProvisionerName: prov.GetName(),
|
||||
}
|
||||
if err := db.CreateAccount(ctx, acc); err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error creating account"))
|
||||
return
|
||||
}
|
||||
|
||||
if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
|
||||
if err := eak.BindTo(acc); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if err := db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error updating external account binding key"))
|
||||
return
|
||||
}
|
||||
acc.ExternalAccountBinding = nar.ExternalAccountBinding
|
||||
}
|
||||
} else {
|
||||
// Account exists
|
||||
httpStatus = http.StatusOK
|
||||
}
|
||||
|
||||
linker.LinkAccount(ctx, acc)
|
||||
|
||||
w.Header().Set("Location", getAccountLocationPath(ctx, linker, acc.ID))
|
||||
render.JSONStatus(w, acc, httpStatus)
|
||||
}
|
||||
|
||||
// GetOrUpdateAccount is the api for updating an ACME account.
|
||||
func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
payload, err := payloadFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// If PostAsGet just respond with the account, otherwise process like a
|
||||
// normal Post request.
|
||||
if !payload.isPostAsGet {
|
||||
var uar UpdateAccountRequest
|
||||
if err := json.Unmarshal(payload.value, &uar); err != nil {
|
||||
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
|
||||
"failed to unmarshal new-account request payload"))
|
||||
return
|
||||
}
|
||||
if err := uar.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if len(uar.Status) > 0 || len(uar.Contact) > 0 {
|
||||
if len(uar.Status) > 0 {
|
||||
acc.Status = uar.Status
|
||||
} else if len(uar.Contact) > 0 {
|
||||
acc.Contact = uar.Contact
|
||||
}
|
||||
|
||||
if err := db.UpdateAccount(ctx, acc); err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error updating account"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
linker.LinkAccount(ctx, acc)
|
||||
|
||||
w.Header().Set("Location", linker.GetLink(ctx, acme.AccountLinkType, acc.ID))
|
||||
render.JSON(w, acc)
|
||||
}
|
||||
|
||||
func logOrdersByAccount(w http.ResponseWriter, oids []string) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
m := map[string]interface{}{
|
||||
"orders": oids,
|
||||
}
|
||||
rl.WithFields(m)
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrdersByAccountID ACME api for retrieving the list of order urls belonging to an account.
|
||||
func GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
accID := chi.URLParam(r, "accID")
|
||||
if acc.ID != accID {
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account ID '%s' does not match url param '%s'", acc.ID, accID))
|
||||
return
|
||||
}
|
||||
|
||||
orders, err := db.GetOrdersByAccountID(ctx, acc.ID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
linker.LinkOrdersByAccountID(ctx, orders)
|
||||
|
||||
render.JSON(w, orders)
|
||||
logOrdersByAccount(w, orders)
|
||||
}
|
File diff suppressed because it is too large
Load diff
168
acme/api/eab.go
168
acme/api/eab.go
|
@ -1,168 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
)
|
||||
|
||||
// ExternalAccountBinding represents the ACME externalAccountBinding JWS
|
||||
type ExternalAccountBinding struct {
|
||||
Protected string `json:"protected"`
|
||||
Payload string `json:"payload"`
|
||||
Sig string `json:"signature"`
|
||||
}
|
||||
|
||||
// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account.
|
||||
func validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) {
|
||||
acmeProv, err := acmeProvisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, acme.WrapErrorISE(err, "could not load ACME provisioner from context")
|
||||
}
|
||||
|
||||
if !acmeProv.RequireEAB {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if nar.ExternalAccountBinding == nil {
|
||||
return nil, acme.NewError(acme.ErrorExternalAccountRequiredType, "no external account binding provided")
|
||||
}
|
||||
|
||||
eabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding)
|
||||
if err != nil {
|
||||
return nil, acme.WrapErrorISE(err, "error marshaling externalAccountBinding into bytes")
|
||||
}
|
||||
|
||||
eabJWS, err := jose.ParseJWS(string(eabJSONBytes))
|
||||
if err != nil {
|
||||
return nil, acme.WrapErrorISE(err, "error parsing externalAccountBinding jws")
|
||||
}
|
||||
|
||||
// TODO(hs): implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration?
|
||||
|
||||
keyID, acmeErr := validateEABJWS(ctx, eabJWS)
|
||||
if acmeErr != nil {
|
||||
return nil, acmeErr
|
||||
}
|
||||
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
externalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)
|
||||
if err != nil {
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
return nil, acme.WrapError(acme.ErrorUnauthorizedType, err, "the field 'kid' references an unknown key")
|
||||
}
|
||||
return nil, acme.WrapErrorISE(err, "error retrieving external account key")
|
||||
}
|
||||
|
||||
if externalAccountKey == nil {
|
||||
return nil, acme.NewError(acme.ErrorUnauthorizedType, "the field 'kid' references an unknown key")
|
||||
}
|
||||
|
||||
if len(externalAccountKey.HmacKey) == 0 {
|
||||
return nil, acme.NewError(acme.ErrorServerInternalType, "external account binding key with id '%s' does not have secret bytes", keyID)
|
||||
}
|
||||
|
||||
if externalAccountKey.AlreadyBound() {
|
||||
return nil, acme.NewError(acme.ErrorUnauthorizedType, "external account binding key with id '%s' was already bound to account '%s' on %s", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt)
|
||||
}
|
||||
|
||||
payload, err := eabJWS.Verify(externalAccountKey.HmacKey)
|
||||
if err != nil {
|
||||
return nil, acme.WrapErrorISE(err, "error verifying externalAccountBinding signature")
|
||||
}
|
||||
|
||||
jwk, err := jwkFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var payloadJWK *jose.JSONWebKey
|
||||
if err = json.Unmarshal(payload, &payloadJWK); err != nil {
|
||||
return nil, acme.WrapError(acme.ErrorMalformedType, err, "error unmarshaling payload into jwk")
|
||||
}
|
||||
|
||||
if !keysAreEqual(jwk, payloadJWK) {
|
||||
return nil, acme.NewError(acme.ErrorUnauthorizedType, "keys in jws and eab payload do not match")
|
||||
}
|
||||
|
||||
return externalAccountKey, nil
|
||||
}
|
||||
|
||||
// keysAreEqual performs an equality check on two JWKs by comparing
|
||||
// the (base64 encoding) of the Key IDs.
|
||||
func keysAreEqual(x, y *jose.JSONWebKey) bool {
|
||||
if x == nil || y == nil {
|
||||
return false
|
||||
}
|
||||
digestX, errX := acme.KeyToID(x)
|
||||
digestY, errY := acme.KeyToID(y)
|
||||
if errX != nil || errY != nil {
|
||||
return false
|
||||
}
|
||||
return digestX == digestY
|
||||
}
|
||||
|
||||
// validateEABJWS verifies the contents of the External Account Binding JWS.
|
||||
// The protected header of the JWS MUST meet the following criteria:
|
||||
//
|
||||
// - The "alg" field MUST indicate a MAC-based algorithm
|
||||
// - The "kid" field MUST contain the key identifier provided by the CA
|
||||
// - The "nonce" field MUST NOT be present
|
||||
// - The "url" field MUST be set to the same value as the outer JWS
|
||||
func validateEABJWS(ctx context.Context, jws *jose.JSONWebSignature) (string, *acme.Error) {
|
||||
if jws == nil {
|
||||
return "", acme.NewErrorISE("no JWS provided")
|
||||
}
|
||||
|
||||
if len(jws.Signatures) != 1 {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "JWS must have one signature")
|
||||
}
|
||||
|
||||
header := jws.Signatures[0].Protected
|
||||
algorithm := header.Algorithm
|
||||
keyID := header.KeyID
|
||||
nonce := header.Nonce
|
||||
|
||||
if !(algorithm == jose.HS256 || algorithm == jose.HS384 || algorithm == jose.HS512) {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'alg' field set to invalid algorithm '%s'", algorithm)
|
||||
}
|
||||
|
||||
if keyID == "" {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'kid' field is required")
|
||||
}
|
||||
|
||||
if nonce != "" {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'nonce' must not be present")
|
||||
}
|
||||
|
||||
jwsURL, ok := header.ExtraHeaders["url"]
|
||||
if !ok {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'url' field is required")
|
||||
}
|
||||
|
||||
outerJWS, err := jwsFromContext(ctx)
|
||||
if err != nil {
|
||||
return "", acme.WrapErrorISE(err, "could not retrieve outer JWS from context")
|
||||
}
|
||||
|
||||
if len(outerJWS.Signatures) != 1 {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "outer JWS must have one signature")
|
||||
}
|
||||
|
||||
outerJWSURL, ok := outerJWS.Signatures[0].Protected.ExtraHeaders["url"]
|
||||
if !ok {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'url' field must be set in outer JWS")
|
||||
}
|
||||
|
||||
if jwsURL != outerJWSURL {
|
||||
return "", acme.NewError(acme.ErrorMalformedType, "'url' field is not the same value as the outer JWS")
|
||||
}
|
||||
|
||||
return keyID, nil
|
||||
}
|
1154
acme/api/eab_test.go
1154
acme/api/eab_test.go
File diff suppressed because it is too large
Load diff
|
@ -1,399 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
func link(url, typ string) string {
|
||||
return fmt.Sprintf("<%s>;rel=%q", url, typ)
|
||||
}
|
||||
|
||||
// Clock that returns time in UTC rounded to seconds.
|
||||
type Clock struct{}
|
||||
|
||||
// Now returns the UTC time rounded to seconds.
|
||||
func (c *Clock) Now() time.Time {
|
||||
return time.Now().UTC().Truncate(time.Second)
|
||||
}
|
||||
|
||||
var clock Clock
|
||||
|
||||
type payloadInfo struct {
|
||||
value []byte
|
||||
isPostAsGet bool
|
||||
isEmptyJSON bool
|
||||
}
|
||||
|
||||
// HandlerOptions required to create a new ACME API request handler.
|
||||
type HandlerOptions struct {
|
||||
// DB storage backend that implements the acme.DB interface.
|
||||
//
|
||||
// Deprecated: use acme.NewContex(context.Context, acme.DB)
|
||||
DB acme.DB
|
||||
|
||||
// CA is the certificate authority interface.
|
||||
//
|
||||
// Deprecated: use authority.NewContext(context.Context, *authority.Authority)
|
||||
CA acme.CertificateAuthority
|
||||
|
||||
// Backdate is the duration that the CA will subtract from the current time
|
||||
// to set the NotBefore in the certificate.
|
||||
Backdate provisioner.Duration
|
||||
|
||||
// DNS the host used to generate accurate ACME links. By default the authority
|
||||
// will use the Host from the request, so this value will only be used if
|
||||
// request.Host is empty.
|
||||
DNS string
|
||||
|
||||
// Prefix is a URL path prefix under which the ACME api is served. This
|
||||
// prefix is required to generate accurate ACME links.
|
||||
// E.g. https://ca.smallstep.com/acme/my-acme-provisioner/new-account --
|
||||
// "acme" is the prefix from which the ACME api is accessed.
|
||||
Prefix string
|
||||
|
||||
// PrerequisitesChecker checks if all prerequisites for serving ACME are
|
||||
// met by the CA configuration.
|
||||
PrerequisitesChecker func(ctx context.Context) (bool, error)
|
||||
}
|
||||
|
||||
var mustAuthority = func(ctx context.Context) acme.CertificateAuthority {
|
||||
return authority.MustFromContext(ctx)
|
||||
}
|
||||
|
||||
// handler is the ACME API request handler.
|
||||
type handler struct {
|
||||
opts *HandlerOptions
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface. For backward compatibility
|
||||
// this route adds will add a new middleware that will set the ACME components
|
||||
// on the context.
|
||||
//
|
||||
// Note: this method is deprecated in step-ca, other applications can still use
|
||||
// this to support ACME, but the recommendation is to use use
|
||||
// api.Route(api.Router) and acme.NewContext() instead.
|
||||
func (h *handler) Route(r api.Router) {
|
||||
client := acme.NewClient()
|
||||
linker := acme.NewLinker(h.opts.DNS, h.opts.Prefix)
|
||||
route(r, func(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if ca, ok := h.opts.CA.(*authority.Authority); ok && ca != nil {
|
||||
ctx = authority.NewContext(ctx, ca)
|
||||
}
|
||||
ctx = acme.NewContext(ctx, h.opts.DB, client, linker, h.opts.PrerequisitesChecker)
|
||||
next(w, r.WithContext(ctx))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// NewHandler returns a new ACME API handler.
|
||||
//
|
||||
// Note: this method is deprecated in step-ca, other applications can still use
|
||||
// this to support ACME, but the recommendation is to use use
|
||||
// api.Route(api.Router) and acme.NewContext() instead.
|
||||
func NewHandler(opts HandlerOptions) api.RouterHandler {
|
||||
return &handler{
|
||||
opts: &opts,
|
||||
}
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface. This method requires that
|
||||
// all the acme components, authority, db, client, linker, and prerequisite
|
||||
// checker to be present in the context.
|
||||
func Route(r api.Router) {
|
||||
route(r, nil)
|
||||
}
|
||||
|
||||
func route(r api.Router, middleware func(next nextHTTP) nextHTTP) {
|
||||
commonMiddleware := func(next nextHTTP) nextHTTP {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
// Linker middleware gets the provisioner and current url from the
|
||||
// request and sets them in the context.
|
||||
linker := acme.MustLinkerFromContext(r.Context())
|
||||
linker.Middleware(http.HandlerFunc(checkPrerequisites(next))).ServeHTTP(w, r)
|
||||
}
|
||||
if middleware != nil {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
validatingMiddleware := func(next nextHTTP) nextHTTP {
|
||||
return commonMiddleware(addNonce(addDirLink(verifyContentType(parseJWS(validateJWS(next))))))
|
||||
}
|
||||
extractPayloadByJWK := func(next nextHTTP) nextHTTP {
|
||||
return validatingMiddleware(extractJWK(verifyAndExtractJWSPayload(next)))
|
||||
}
|
||||
extractPayloadByKid := func(next nextHTTP) nextHTTP {
|
||||
return validatingMiddleware(lookupJWK(verifyAndExtractJWSPayload(next)))
|
||||
}
|
||||
extractPayloadByKidOrJWK := func(next nextHTTP) nextHTTP {
|
||||
return validatingMiddleware(extractOrLookupJWK(verifyAndExtractJWSPayload(next)))
|
||||
}
|
||||
|
||||
getPath := acme.GetUnescapedPathSuffix
|
||||
|
||||
// Standard ACME API
|
||||
r.MethodFunc("GET", getPath(acme.NewNonceLinkType, "{provisionerID}"),
|
||||
commonMiddleware(addNonce(addDirLink(GetNonce))))
|
||||
r.MethodFunc("HEAD", getPath(acme.NewNonceLinkType, "{provisionerID}"),
|
||||
commonMiddleware(addNonce(addDirLink(GetNonce))))
|
||||
r.MethodFunc("GET", getPath(acme.DirectoryLinkType, "{provisionerID}"),
|
||||
commonMiddleware(GetDirectory))
|
||||
r.MethodFunc("HEAD", getPath(acme.DirectoryLinkType, "{provisionerID}"),
|
||||
commonMiddleware(GetDirectory))
|
||||
|
||||
r.MethodFunc("POST", getPath(acme.NewAccountLinkType, "{provisionerID}"),
|
||||
extractPayloadByJWK(NewAccount))
|
||||
r.MethodFunc("POST", getPath(acme.AccountLinkType, "{provisionerID}", "{accID}"),
|
||||
extractPayloadByKid(GetOrUpdateAccount))
|
||||
r.MethodFunc("POST", getPath(acme.KeyChangeLinkType, "{provisionerID}", "{accID}"),
|
||||
extractPayloadByKid(NotImplemented))
|
||||
r.MethodFunc("POST", getPath(acme.NewOrderLinkType, "{provisionerID}"),
|
||||
extractPayloadByKid(NewOrder))
|
||||
r.MethodFunc("POST", getPath(acme.OrderLinkType, "{provisionerID}", "{ordID}"),
|
||||
extractPayloadByKid(isPostAsGet(GetOrder)))
|
||||
r.MethodFunc("POST", getPath(acme.OrdersByAccountLinkType, "{provisionerID}", "{accID}"),
|
||||
extractPayloadByKid(isPostAsGet(GetOrdersByAccountID)))
|
||||
r.MethodFunc("POST", getPath(acme.FinalizeLinkType, "{provisionerID}", "{ordID}"),
|
||||
extractPayloadByKid(FinalizeOrder))
|
||||
r.MethodFunc("POST", getPath(acme.AuthzLinkType, "{provisionerID}", "{authzID}"),
|
||||
extractPayloadByKid(isPostAsGet(GetAuthorization)))
|
||||
r.MethodFunc("POST", getPath(acme.ChallengeLinkType, "{provisionerID}", "{authzID}", "{chID}"),
|
||||
extractPayloadByKid(GetChallenge))
|
||||
r.MethodFunc("POST", getPath(acme.CertificateLinkType, "{provisionerID}", "{certID}"),
|
||||
extractPayloadByKid(isPostAsGet(GetCertificate)))
|
||||
r.MethodFunc("POST", getPath(acme.RevokeCertLinkType, "{provisionerID}"),
|
||||
extractPayloadByKidOrJWK(RevokeCert))
|
||||
}
|
||||
|
||||
// GetNonce just sets the right header since a Nonce is added to each response
|
||||
// by middleware by default.
|
||||
func GetNonce(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "HEAD" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
TermsOfService string `json:"termsOfService,omitempty"`
|
||||
Website string `json:"website,omitempty"`
|
||||
CaaIdentities []string `json:"caaIdentities,omitempty"`
|
||||
ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"`
|
||||
}
|
||||
|
||||
// Directory represents an ACME directory for configuring clients.
|
||||
type Directory struct {
|
||||
NewNonce string `json:"newNonce"`
|
||||
NewAccount string `json:"newAccount"`
|
||||
NewOrder string `json:"newOrder"`
|
||||
RevokeCert string `json:"revokeCert"`
|
||||
KeyChange string `json:"keyChange"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// ToLog enables response logging for the Directory type.
|
||||
func (d *Directory) ToLog() (interface{}, error) {
|
||||
b, err := json.Marshal(d)
|
||||
if err != nil {
|
||||
return nil, acme.WrapErrorISE(err, "error marshaling directory for logging")
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// GetDirectory is the ACME resource for returning a directory configuration
|
||||
// for client configuration.
|
||||
func GetDirectory(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
acmeProv, err := acmeProvisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
render.JSON(w, &Directory{
|
||||
NewNonce: linker.GetLink(ctx, acme.NewNonceLinkType),
|
||||
NewAccount: linker.GetLink(ctx, acme.NewAccountLinkType),
|
||||
NewOrder: linker.GetLink(ctx, acme.NewOrderLinkType),
|
||||
RevokeCert: linker.GetLink(ctx, acme.RevokeCertLinkType),
|
||||
KeyChange: linker.GetLink(ctx, acme.KeyChangeLinkType),
|
||||
Meta: createMetaObject(acmeProv),
|
||||
})
|
||||
}
|
||||
|
||||
// createMetaObject creates a Meta object if the ACME provisioner
|
||||
// has one or more properties that are written in the ACME directory output.
|
||||
// It returns nil if none of the properties are set.
|
||||
func createMetaObject(p *provisioner.ACME) *Meta {
|
||||
if shouldAddMetaObject(p) {
|
||||
return &Meta{
|
||||
TermsOfService: p.TermsOfService,
|
||||
Website: p.Website,
|
||||
CaaIdentities: p.CaaIdentities,
|
||||
ExternalAccountRequired: p.RequireEAB,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldAddMetaObject returns whether or not the ACME provisioner
|
||||
// has properties configured that must be added to the ACME directory object.
|
||||
func shouldAddMetaObject(p *provisioner.ACME) bool {
|
||||
switch {
|
||||
case p.TermsOfService != "":
|
||||
return true
|
||||
case p.Website != "":
|
||||
return true
|
||||
case len(p.CaaIdentities) > 0:
|
||||
return true
|
||||
case p.RequireEAB:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// NotImplemented returns a 501 and is generally a placeholder for functionality which
|
||||
// MAY be added at some point in the future but is not in any way a guarantee of such.
|
||||
func NotImplemented(w http.ResponseWriter, _ *http.Request) {
|
||||
render.Error(w, acme.NewError(acme.ErrorNotImplementedType, "this API is not implemented"))
|
||||
}
|
||||
|
||||
// GetAuthorization ACME api for retrieving an Authz.
|
||||
func GetAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
az, err := db.GetAuthorization(ctx, chi.URLParam(r, "authzID"))
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error retrieving authorization"))
|
||||
return
|
||||
}
|
||||
if acc.ID != az.AccountID {
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"account '%s' does not own authorization '%s'", acc.ID, az.ID))
|
||||
return
|
||||
}
|
||||
if err = az.UpdateStatus(ctx, db); err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error updating authorization status"))
|
||||
return
|
||||
}
|
||||
|
||||
linker.LinkAuthorization(ctx, az)
|
||||
|
||||
w.Header().Set("Location", linker.GetLink(ctx, acme.AuthzLinkType, az.ID))
|
||||
render.JSON(w, az)
|
||||
}
|
||||
|
||||
// GetChallenge ACME api for retrieving a Challenge.
|
||||
func GetChallenge(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := payloadFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: We should be checking that the request is either a POST-as-GET, or
|
||||
// that for all challenges except for device-attest-01, the payload is an
|
||||
// empty JSON block ({}). However, older ACME clients still send a vestigial
|
||||
// body (rather than an empty JSON block) and strict enforcement would
|
||||
// render these clients broken.
|
||||
|
||||
azID := chi.URLParam(r, "authzID")
|
||||
ch, err := db.GetChallenge(ctx, chi.URLParam(r, "chID"), azID)
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error retrieving challenge"))
|
||||
return
|
||||
}
|
||||
ch.AuthorizationID = azID
|
||||
if acc.ID != ch.AccountID {
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"account '%s' does not own challenge '%s'", acc.ID, ch.ID))
|
||||
return
|
||||
}
|
||||
jwk, err := jwkFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if err = ch.Validate(ctx, db, jwk, payload.value); err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error validating challenge"))
|
||||
return
|
||||
}
|
||||
|
||||
linker.LinkChallenge(ctx, ch, azID)
|
||||
|
||||
w.Header().Add("Link", link(linker.GetLink(ctx, acme.AuthzLinkType, azID), "up"))
|
||||
w.Header().Set("Location", linker.GetLink(ctx, acme.ChallengeLinkType, azID, ch.ID))
|
||||
render.JSON(w, ch)
|
||||
}
|
||||
|
||||
// GetCertificate ACME api for retrieving a Certificate.
|
||||
func GetCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
certID := chi.URLParam(r, "certID")
|
||||
cert, err := db.GetCertificate(ctx, certID)
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error retrieving certificate"))
|
||||
return
|
||||
}
|
||||
if cert.AccountID != acc.ID {
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"account '%s' does not own certificate '%s'", acc.ID, certID))
|
||||
return
|
||||
}
|
||||
|
||||
var certBytes []byte
|
||||
for _, c := range append([]*x509.Certificate{cert.Leaf}, cert.Intermediates...) {
|
||||
certBytes = append(certBytes, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: c.Raw,
|
||||
})...)
|
||||
}
|
||||
|
||||
api.LogCertificate(w, cert.Leaf)
|
||||
w.Header().Set("Content-Type", "application/pem-certificate-chain")
|
||||
w.Write(certBytes)
|
||||
}
|
|
@ -1,866 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
get func(url string) (*http.Response, error)
|
||||
lookupTxt func(name string) ([]string, error)
|
||||
tlsDial func(network, addr string, config *tls.Config) (*tls.Conn, error)
|
||||
}
|
||||
|
||||
func (m *mockClient) Get(u string) (*http.Response, error) { return m.get(u) }
|
||||
func (m *mockClient) LookupTxt(name string) ([]string, error) { return m.lookupTxt(name) }
|
||||
func (m *mockClient) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) {
|
||||
return m.tlsDial(network, addr, config)
|
||||
}
|
||||
|
||||
func mockMustAuthority(t *testing.T, a acme.CertificateAuthority) {
|
||||
t.Helper()
|
||||
fn := mustAuthority
|
||||
t.Cleanup(func() {
|
||||
mustAuthority = fn
|
||||
})
|
||||
mustAuthority = func(ctx context.Context) acme.CertificateAuthority {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_GetNonce(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
}{
|
||||
{"GET", 204},
|
||||
{"HEAD", 200},
|
||||
}
|
||||
|
||||
// Request with chi context
|
||||
req := httptest.NewRequest("GET", "http://ca.smallstep.com/nonce", nil)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// h := &Handler{}
|
||||
w := httptest.NewRecorder()
|
||||
req.Method = tt.name
|
||||
GetNonce(w, req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("Handler.GetNonce StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_GetDirectory(t *testing.T) {
|
||||
linker := acme.NewLinker("ca.smallstep.com", "acme")
|
||||
_ = linker
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
dir Directory
|
||||
err *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/no-provisioner": func(t *testing.T) test {
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("provisioner is not in context"),
|
||||
}
|
||||
},
|
||||
"fail/different-provisioner": func(t *testing.T) test {
|
||||
ctx := acme.NewProvisionerContext(context.Background(), &fakeProvisioner{})
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("provisioner in context is not an ACME provisioner"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
prov := newProv()
|
||||
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),
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
dir: expDir,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
"ok/eab-required": func(t *testing.T) test {
|
||||
prov := newACMEProv(t)
|
||||
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{
|
||||
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,
|
||||
},
|
||||
}
|
||||
return test{
|
||||
ctx: ctx,
|
||||
dir: expDir,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := acme.NewLinkerContext(tc.ctx, acme.NewLinker("test.ca.smallstep.com", "acme"))
|
||||
req := httptest.NewRequest("GET", "/foo/bar", nil)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
GetDirectory(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, res.StatusCode, tc.statusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 && assert.NotNil(t, tc.err) {
|
||||
var ae acme.Error
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
var dir Directory
|
||||
json.Unmarshal(bytes.TrimSpace(body), &dir)
|
||||
if !cmp.Equal(tc.dir, dir) {
|
||||
t.Errorf("GetDirectory() diff =\n%s", cmp.Diff(tc.dir, dir))
|
||||
}
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_GetAuthorization(t *testing.T) {
|
||||
expiry := time.Now().UTC().Add(6 * time.Hour)
|
||||
az := acme.Authorization{
|
||||
ID: "authzID",
|
||||
AccountID: "accID",
|
||||
Identifier: acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "example.com",
|
||||
},
|
||||
Status: "pending",
|
||||
ExpiresAt: expiry,
|
||||
Wildcard: false,
|
||||
Challenges: []*acme.Challenge{
|
||||
{
|
||||
Type: "http-01",
|
||||
Status: "pending",
|
||||
Token: "tok2",
|
||||
URL: "https://ca.smallstep.com/acme/challenge/chHTTPID",
|
||||
ID: "chHTTP01ID",
|
||||
},
|
||||
{
|
||||
Type: "dns-01",
|
||||
Status: "pending",
|
||||
Token: "tok2",
|
||||
URL: "https://ca.smallstep.com/acme/challenge/chDNSID",
|
||||
ID: "chDNSID",
|
||||
},
|
||||
},
|
||||
}
|
||||
prov := newProv()
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
|
||||
// Request with chi context
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("authzID", az.ID)
|
||||
u := fmt.Sprintf("%s/acme/%s/authz/%s",
|
||||
baseURL.String(), provName, az.ID)
|
||||
|
||||
type test struct {
|
||||
db acme.DB
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
err *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/no-account": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &acme.MockDB{},
|
||||
ctx: context.Background(),
|
||||
statusCode: 400,
|
||||
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
||||
}
|
||||
},
|
||||
"fail/nil-account": func(t *testing.T) test {
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, nil)
|
||||
return test{
|
||||
db: &acme.MockDB{},
|
||||
ctx: ctx,
|
||||
statusCode: 400,
|
||||
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
||||
}
|
||||
},
|
||||
"fail/db.GetAuthorization-error": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockError: acme.NewErrorISE("force"),
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("force"),
|
||||
}
|
||||
},
|
||||
"fail/account-id-mismatch": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*acme.Authorization, error) {
|
||||
assert.Equals(t, id, az.ID)
|
||||
return &acme.Authorization{
|
||||
AccountID: "foo",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 401,
|
||||
err: acme.NewError(acme.ErrorUnauthorizedType, "account id mismatch"),
|
||||
}
|
||||
},
|
||||
"fail/db.UpdateAuthorization-error": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*acme.Authorization, error) {
|
||||
assert.Equals(t, id, az.ID)
|
||||
return &acme.Authorization{
|
||||
AccountID: "accID",
|
||||
Status: acme.StatusPending,
|
||||
ExpiresAt: time.Now().Add(-1 * time.Hour),
|
||||
}, nil
|
||||
},
|
||||
MockUpdateAuthorization: func(ctx context.Context, az *acme.Authorization) error {
|
||||
assert.Equals(t, az.Status, acme.StatusInvalid)
|
||||
return acme.NewErrorISE("force")
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*acme.Authorization, error) {
|
||||
assert.Equals(t, id, az.ID)
|
||||
return &az, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
|
||||
req := httptest.NewRequest("GET", "/foo/bar", nil)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
GetAuthorization(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, res.StatusCode, tc.statusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 && assert.NotNil(t, tc.err) {
|
||||
var ae acme.Error
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
//var gotAz acme.Authz
|
||||
//assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &gotAz))
|
||||
expB, err := json.Marshal(az)
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, bytes.TrimSpace(body), expB)
|
||||
assert.Equals(t, res.Header["Location"], []string{u})
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_GetCertificate(t *testing.T) {
|
||||
leaf, err := pemutil.ReadCertificate("../../authority/testdata/certs/foo.crt")
|
||||
assert.FatalError(t, err)
|
||||
inter, err := pemutil.ReadCertificate("../../authority/testdata/certs/intermediate_ca.crt")
|
||||
assert.FatalError(t, err)
|
||||
root, err := pemutil.ReadCertificate("../../authority/testdata/certs/root_ca.crt")
|
||||
assert.FatalError(t, err)
|
||||
|
||||
certBytes := append(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: leaf.Raw,
|
||||
}), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: inter.Raw,
|
||||
})...)
|
||||
certBytes = append(certBytes, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: root.Raw,
|
||||
})...)
|
||||
certID := "certID"
|
||||
|
||||
prov := newProv()
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
// Request with chi context
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("certID", certID)
|
||||
u := fmt.Sprintf("%s/acme/%s/certificate/%s",
|
||||
baseURL.String(), provName, certID)
|
||||
|
||||
type test struct {
|
||||
db acme.DB
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
err *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/no-account": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &acme.MockDB{},
|
||||
ctx: context.Background(),
|
||||
statusCode: 400,
|
||||
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
||||
}
|
||||
},
|
||||
"fail/nil-account": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), accContextKey, nil)
|
||||
return test{
|
||||
db: &acme.MockDB{},
|
||||
ctx: ctx,
|
||||
statusCode: 400,
|
||||
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
||||
}
|
||||
},
|
||||
"fail/db.GetCertificate-error": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockError: acme.NewErrorISE("force"),
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("force"),
|
||||
}
|
||||
},
|
||||
"fail/account-id-mismatch": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetCertificate: func(ctx context.Context, id string) (*acme.Certificate, error) {
|
||||
assert.Equals(t, id, certID)
|
||||
return &acme.Certificate{AccountID: "foo"}, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 401,
|
||||
err: acme.NewError(acme.ErrorUnauthorizedType, "account id mismatch"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetCertificate: func(ctx context.Context, id string) (*acme.Certificate, error) {
|
||||
assert.Equals(t, id, certID)
|
||||
return &acme.Certificate{
|
||||
AccountID: "accID",
|
||||
OrderID: "ordID",
|
||||
Leaf: leaf,
|
||||
Intermediates: []*x509.Certificate{inter, root},
|
||||
ID: id,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := acme.NewDatabaseContext(tc.ctx, tc.db)
|
||||
req := httptest.NewRequest("GET", u, nil)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
GetCertificate(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, res.StatusCode, tc.statusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 && assert.NotNil(t, tc.err) {
|
||||
var ae acme.Error
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.HasPrefix(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
assert.Equals(t, bytes.TrimSpace(body), bytes.TrimSpace(certBytes))
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/pem-certificate-chain"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_GetChallenge(t *testing.T) {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("chID", "chID")
|
||||
chiCtx.URLParams.Add("authzID", "authzID")
|
||||
prov := newProv()
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
|
||||
u := fmt.Sprintf("%s/acme/%s/challenge/%s/%s",
|
||||
baseURL.String(), provName, "authzID", "chID")
|
||||
|
||||
type test struct {
|
||||
db acme.DB
|
||||
vc acme.Client
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
ch *acme.Challenge
|
||||
err *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/no-account": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &acme.MockDB{},
|
||||
ctx: context.Background(),
|
||||
statusCode: 400,
|
||||
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
||||
}
|
||||
},
|
||||
"fail/nil-account": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &acme.MockDB{},
|
||||
ctx: context.WithValue(context.Background(), accContextKey, nil),
|
||||
statusCode: 400,
|
||||
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
|
||||
}
|
||||
},
|
||||
"fail/no-payload": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := context.WithValue(context.Background(), accContextKey, acc)
|
||||
return test{
|
||||
db: &acme.MockDB{},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("payload expected in request context"),
|
||||
}
|
||||
},
|
||||
"fail/nil-payload": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, nil)
|
||||
return test{
|
||||
db: &acme.MockDB{},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("payload expected in request context"),
|
||||
}
|
||||
},
|
||||
"fail/db.GetChallenge-error": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
||||
assert.Equals(t, chID, "chID")
|
||||
assert.Equals(t, azID, "authzID")
|
||||
return nil, acme.NewErrorISE("force")
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("force"),
|
||||
}
|
||||
},
|
||||
"fail/account-id-mismatch": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
||||
assert.Equals(t, chID, "chID")
|
||||
assert.Equals(t, azID, "authzID")
|
||||
return &acme.Challenge{AccountID: "foo"}, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 401,
|
||||
err: acme.NewError(acme.ErrorUnauthorizedType, "accout id mismatch"),
|
||||
}
|
||||
},
|
||||
"fail/no-jwk": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
||||
assert.Equals(t, chID, "chID")
|
||||
assert.Equals(t, azID, "authzID")
|
||||
return &acme.Challenge{AccountID: "accID"}, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("missing jwk"),
|
||||
}
|
||||
},
|
||||
"fail/nil-jwk": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
||||
ctx = context.WithValue(ctx, jwkContextKey, nil)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
||||
assert.Equals(t, chID, "chID")
|
||||
assert.Equals(t, azID, "authzID")
|
||||
return &acme.Challenge{AccountID: "accID"}, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("nil jwk"),
|
||||
}
|
||||
},
|
||||
"fail/validate-challenge-error": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
||||
_jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
_pub := _jwk.Public()
|
||||
ctx = context.WithValue(ctx, jwkContextKey, &_pub)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
||||
assert.Equals(t, chID, "chID")
|
||||
assert.Equals(t, azID, "authzID")
|
||||
return &acme.Challenge{
|
||||
Status: acme.StatusPending,
|
||||
Type: acme.HTTP01,
|
||||
AccountID: "accID",
|
||||
}, nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
||||
assert.Equals(t, ch.Status, acme.StatusPending)
|
||||
assert.Equals(t, ch.Type, acme.HTTP01)
|
||||
assert.Equals(t, ch.AccountID, "accID")
|
||||
assert.Equals(t, ch.AuthorizationID, "authzID")
|
||||
assert.HasSuffix(t, ch.Error.Type, acme.ErrorConnectionType.String())
|
||||
return acme.NewErrorISE("force")
|
||||
},
|
||||
},
|
||||
vc: &mockClient{
|
||||
get: func(string) (*http.Response, error) {
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
err: acme.NewErrorISE("force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "accID"}
|
||||
ctx := acme.NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})
|
||||
_jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
_pub := _jwk.Public()
|
||||
ctx = context.WithValue(ctx, jwkContextKey, &_pub)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
db: &acme.MockDB{
|
||||
MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {
|
||||
assert.Equals(t, chID, "chID")
|
||||
assert.Equals(t, azID, "authzID")
|
||||
return &acme.Challenge{
|
||||
ID: "chID",
|
||||
Status: acme.StatusPending,
|
||||
Type: acme.HTTP01,
|
||||
AccountID: "accID",
|
||||
}, nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, ch *acme.Challenge) error {
|
||||
assert.Equals(t, ch.Status, acme.StatusPending)
|
||||
assert.Equals(t, ch.Type, acme.HTTP01)
|
||||
assert.Equals(t, ch.AccountID, "accID")
|
||||
assert.Equals(t, ch.AuthorizationID, "authzID")
|
||||
assert.HasSuffix(t, ch.Error.Type, acme.ErrorConnectionType.String())
|
||||
return nil
|
||||
},
|
||||
},
|
||||
ch: &acme.Challenge{
|
||||
ID: "chID",
|
||||
Status: acme.StatusPending,
|
||||
AuthorizationID: "authzID",
|
||||
Type: acme.HTTP01,
|
||||
AccountID: "accID",
|
||||
URL: u,
|
||||
Error: acme.NewError(acme.ErrorConnectionType, "force"),
|
||||
},
|
||||
vc: &mockClient{
|
||||
get: func(string) (*http.Response, error) {
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker("test.ca.smallstep.com", "acme"), nil)
|
||||
req := httptest.NewRequest("GET", u, nil)
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
GetChallenge(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, res.StatusCode, tc.statusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 && assert.NotNil(t, tc.err) {
|
||||
var ae acme.Error
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
assert.Equals(t, ae.Detail, tc.err.Detail)
|
||||
assert.Equals(t, ae.Subproblems, tc.err.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
expB, err := json.Marshal(tc.ch)
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, bytes.TrimSpace(body), expB)
|
||||
assert.Equals(t, res.Header["Link"], []string{fmt.Sprintf("<%s/acme/%s/authz/%s>;rel=\"up\"", baseURL, provName, "authzID")})
|
||||
assert.Equals(t, res.Header["Location"], []string{u})
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,531 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
type nextHTTP = func(http.ResponseWriter, *http.Request)
|
||||
|
||||
func logNonce(w http.ResponseWriter, nonce string) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
m := map[string]interface{}{
|
||||
"nonce": nonce,
|
||||
}
|
||||
rl.WithFields(m)
|
||||
}
|
||||
}
|
||||
|
||||
// addNonce is a middleware that adds a nonce to the response header.
|
||||
func addNonce(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
db := acme.MustDatabaseFromContext(r.Context())
|
||||
nonce, err := db.CreateNonce(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Replay-Nonce", string(nonce))
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
logNonce(w, string(nonce))
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// addDirLink is a middleware that adds a 'Link' response reader with the
|
||||
// directory index url.
|
||||
func addDirLink(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
w.Header().Add("Link", link(linker.GetLink(ctx, acme.DirectoryLinkType), "index"))
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyContentType is a middleware that verifies that content type is
|
||||
// application/jose+json.
|
||||
func verifyContentType(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
p, err := provisionerFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
Path: acme.GetUnescapedPathSuffix(acme.CertificateLinkType, p.GetName(), ""),
|
||||
}
|
||||
|
||||
var expected []string
|
||||
if strings.Contains(r.URL.String(), u.EscapedPath()) {
|
||||
// GET /certificate requests allow a greater range of content types.
|
||||
expected = []string{"application/jose+json", "application/pkix-cert", "application/pkcs7-mime"}
|
||||
} else {
|
||||
// By default every request should have content-type applictaion/jose+json.
|
||||
expected = []string{"application/jose+json"}
|
||||
}
|
||||
|
||||
ct := r.Header.Get("Content-Type")
|
||||
for _, e := range expected {
|
||||
if ct == e {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType,
|
||||
"expected content-type to be in %s, but got %s", expected, ct))
|
||||
}
|
||||
}
|
||||
|
||||
// parseJWS is a middleware that parses a request body into a JSONWebSignature struct.
|
||||
func parseJWS(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "failed to read request body"))
|
||||
return
|
||||
}
|
||||
jws, err := jose.ParseJWS(string(body))
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "failed to parse JWS from request body"))
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), jwsContextKey, jws)
|
||||
next(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
// validateJWS checks the request body for to verify that it meets ACME
|
||||
// requirements for a JWS.
|
||||
//
|
||||
// The JWS MUST NOT have multiple signatures
|
||||
// The JWS Unencoded Payload Option [RFC7797] MUST NOT be used
|
||||
// The JWS Unprotected Header [RFC7515] MUST NOT be used
|
||||
// The JWS Payload MUST NOT be detached
|
||||
// The JWS Protected Header MUST include the following fields:
|
||||
// - “alg” (Algorithm).
|
||||
// This field MUST NOT contain “none” or a Message Authentication Code
|
||||
// (MAC) algorithm (e.g. one in which the algorithm registry description
|
||||
// mentions MAC/HMAC).
|
||||
// - “nonce” (defined in Section 6.5)
|
||||
// - “url” (defined in Section 6.4)
|
||||
// - Either “jwk” (JSON Web Key) or “kid” (Key ID) as specified below<Paste>
|
||||
func validateJWS(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
|
||||
jws, err := jwsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if len(jws.Signatures) == 0 {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "request body does not contain a signature"))
|
||||
return
|
||||
}
|
||||
if len(jws.Signatures) > 1 {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "request body contains more than one signature"))
|
||||
return
|
||||
}
|
||||
|
||||
sig := jws.Signatures[0]
|
||||
uh := sig.Unprotected
|
||||
if len(uh.KeyID) > 0 ||
|
||||
uh.JSONWebKey != nil ||
|
||||
len(uh.Algorithm) > 0 ||
|
||||
len(uh.Nonce) > 0 ||
|
||||
len(uh.ExtraHeaders) > 0 {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "unprotected header must not be used"))
|
||||
return
|
||||
}
|
||||
hdr := sig.Protected
|
||||
switch hdr.Algorithm {
|
||||
case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512:
|
||||
if hdr.JSONWebKey != nil {
|
||||
switch k := hdr.JSONWebKey.Key.(type) {
|
||||
case *rsa.PublicKey:
|
||||
if k.Size() < keyutil.MinRSAKeyBytes {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType,
|
||||
"rsa keys must be at least %d bits (%d bytes) in size",
|
||||
8*keyutil.MinRSAKeyBytes, keyutil.MinRSAKeyBytes))
|
||||
return
|
||||
}
|
||||
default:
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType,
|
||||
"jws key type and algorithm do not match"))
|
||||
return
|
||||
}
|
||||
}
|
||||
case jose.ES256, jose.ES384, jose.ES512, jose.EdDSA:
|
||||
// we good
|
||||
default:
|
||||
render.Error(w, acme.NewError(acme.ErrorBadSignatureAlgorithmType, "unsuitable algorithm: %s", hdr.Algorithm))
|
||||
return
|
||||
}
|
||||
|
||||
// Check the validity/freshness of the Nonce.
|
||||
if err := db.DeleteNonce(ctx, acme.Nonce(hdr.Nonce)); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the JWS url matches the requested url.
|
||||
jwsURL, ok := hdr.ExtraHeaders["url"].(string)
|
||||
if !ok {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "jws missing url protected header"))
|
||||
return
|
||||
}
|
||||
reqURL := &url.URL{Scheme: "https", Host: r.Host, Path: r.URL.Path}
|
||||
if jwsURL != reqURL.String() {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType,
|
||||
"url header in JWS (%s) does not match request url (%s)", jwsURL, reqURL))
|
||||
return
|
||||
}
|
||||
|
||||
if hdr.JSONWebKey != nil && len(hdr.KeyID) > 0 {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "jwk and kid are mutually exclusive"))
|
||||
return
|
||||
}
|
||||
if hdr.JSONWebKey == nil && hdr.KeyID == "" {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "either jwk or kid must be defined in jws protected header"))
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// extractJWK is a middleware that extracts the JWK from the JWS and saves it
|
||||
// in the context. Make sure to parse and validate the JWS before running this
|
||||
// middleware.
|
||||
func extractJWK(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
|
||||
jws, err := jwsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
jwk := jws.Signatures[0].Protected.JSONWebKey
|
||||
if jwk == nil {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "jwk expected in protected header"))
|
||||
return
|
||||
}
|
||||
if !jwk.Valid() {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "invalid jwk in protected header"))
|
||||
return
|
||||
}
|
||||
|
||||
// Overwrite KeyID with the JWK thumbprint.
|
||||
jwk.KeyID, err = acme.KeyToID(jwk)
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error getting KeyID from JWK"))
|
||||
return
|
||||
}
|
||||
|
||||
// Store the JWK in the context.
|
||||
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||
|
||||
// Get Account OR continue to generate a new one OR continue Revoke with certificate private key
|
||||
acc, err := db.GetAccountByKeyID(ctx, jwk.KeyID)
|
||||
switch {
|
||||
case errors.Is(err, acme.ErrNotFound):
|
||||
// For NewAccount and Revoke requests ...
|
||||
break
|
||||
case err != nil:
|
||||
render.Error(w, err)
|
||||
return
|
||||
default:
|
||||
if !acc.IsValid() {
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account is not active"))
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
}
|
||||
next(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
// checkPrerequisites checks if all prerequisites for serving ACME
|
||||
// are met by the CA configuration.
|
||||
func checkPrerequisites(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
// If the function is not set assume that all prerequisites are met.
|
||||
checkFunc, ok := acme.PrerequisitesCheckerFromContext(ctx)
|
||||
if ok {
|
||||
ok, err := checkFunc(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error checking acme provisioner prerequisites"))
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
render.Error(w, acme.NewError(acme.ErrorNotImplementedType, "acme provisioner configuration lacks prerequisites"))
|
||||
return
|
||||
}
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// lookupJWK loads the JWK associated with the acme account referenced by the
|
||||
// kid parameter of the signed payload.
|
||||
// Make sure to parse and validate the JWS before running this middleware.
|
||||
func lookupJWK(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
|
||||
jws, err := jwsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
kid := jws.Signatures[0].Protected.KeyID
|
||||
if kid == "" {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "signature missing 'kid'"))
|
||||
return
|
||||
}
|
||||
|
||||
accID := path.Base(kid)
|
||||
acc, err := db.GetAccount(ctx, accID)
|
||||
switch {
|
||||
case acme.IsErrNotFound(err):
|
||||
render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType, "account with ID '%s' not found", accID))
|
||||
return
|
||||
case err != nil:
|
||||
render.Error(w, err)
|
||||
return
|
||||
default:
|
||||
if !acc.IsValid() {
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account is not active"))
|
||||
return
|
||||
}
|
||||
|
||||
if storedLocation := acc.GetLocation(); storedLocation != "" {
|
||||
if kid != storedLocation {
|
||||
// ACME accounts should have a stored location equivalent to the
|
||||
// kid in the ACME request.
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"kid does not match stored account location; expected %s, but got %s",
|
||||
storedLocation, kid))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify that the provisioner with which the account was created
|
||||
// matches the provisioner in the request URL.
|
||||
reqProv := acme.MustProvisionerFromContext(ctx)
|
||||
reqProvName := reqProv.GetName()
|
||||
accProvName := acc.ProvisionerName
|
||||
if reqProvName != accProvName {
|
||||
// Provisioner in the URL must match the provisioner with
|
||||
// which the account was created.
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s",
|
||||
accProvName, reqProvName))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// This code will only execute for old ACME accounts that do
|
||||
// not have a cached location. The following validation was
|
||||
// the original implementation of the `kid` check which has
|
||||
// since been deprecated. However, the code will remain to
|
||||
// ensure consistent behavior for old ACME accounts.
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
kidPrefix := linker.GetLink(ctx, acme.AccountLinkType, "")
|
||||
if !strings.HasPrefix(kid, kidPrefix) {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType,
|
||||
"kid does not have required prefix; expected %s, but got %s",
|
||||
kidPrefix, kid))
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, jwkContextKey, acc.Key)
|
||||
next(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractOrLookupJWK forwards handling to either extractJWK or
|
||||
// lookupJWK based on the presence of a JWK or a KID, respectively.
|
||||
func extractOrLookupJWK(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
jws, err := jwsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// at this point the JWS has already been verified (if correctly configured in middleware),
|
||||
// and it can be used to check if a JWK exists. This flow is used when the ACME client
|
||||
// signed the payload with a certificate private key.
|
||||
if canExtractJWKFrom(jws) {
|
||||
extractJWK(next)(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// default to looking up the JWK based on KeyID. This flow is used when the ACME client
|
||||
// signed the payload with an account private key.
|
||||
lookupJWK(next)(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// canExtractJWKFrom checks if the JWS has a JWK that can be extracted
|
||||
func canExtractJWKFrom(jws *jose.JSONWebSignature) bool {
|
||||
if jws == nil {
|
||||
return false
|
||||
}
|
||||
if len(jws.Signatures) == 0 {
|
||||
return false
|
||||
}
|
||||
return jws.Signatures[0].Protected.JSONWebKey != nil
|
||||
}
|
||||
|
||||
// verifyAndExtractJWSPayload extracts the JWK from the JWS and saves it in the context.
|
||||
// Make sure to parse and validate the JWS before running this middleware.
|
||||
func verifyAndExtractJWSPayload(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
jws, err := jwsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
jwk, err := jwkFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if jwk.Algorithm != "" && jwk.Algorithm != jws.Signatures[0].Protected.Algorithm {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "verifier and signature algorithm do not match"))
|
||||
return
|
||||
}
|
||||
payload, err := jws.Verify(jwk)
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "error verifying jws"))
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{
|
||||
value: payload,
|
||||
isPostAsGet: len(payload) == 0,
|
||||
isEmptyJSON: string(payload) == "{}",
|
||||
})
|
||||
next(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
// isPostAsGet asserts that the request is a PostAsGet (empty JWS payload).
|
||||
func isPostAsGet(next nextHTTP) nextHTTP {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
payload, err := payloadFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if !payload.isPostAsGet {
|
||||
render.Error(w, acme.NewError(acme.ErrorMalformedType, "expected POST-as-GET"))
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// ContextKey is the key type for storing and searching for ACME request
|
||||
// essentials in the context of a request.
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
// accContextKey account key
|
||||
accContextKey = ContextKey("acc")
|
||||
// jwsContextKey jws key
|
||||
jwsContextKey = ContextKey("jws")
|
||||
// jwkContextKey jwk key
|
||||
jwkContextKey = ContextKey("jwk")
|
||||
// payloadContextKey payload key
|
||||
payloadContextKey = ContextKey("payload")
|
||||
)
|
||||
|
||||
// accountFromContext searches the context for an ACME account. Returns the
|
||||
// account or an error.
|
||||
func accountFromContext(ctx context.Context) (*acme.Account, error) {
|
||||
val, ok := ctx.Value(accContextKey).(*acme.Account)
|
||||
if !ok || val == nil {
|
||||
return nil, acme.NewError(acme.ErrorAccountDoesNotExistType, "account not in context")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// jwkFromContext searches the context for a JWK. Returns the JWK or an error.
|
||||
func jwkFromContext(ctx context.Context) (*jose.JSONWebKey, error) {
|
||||
val, ok := ctx.Value(jwkContextKey).(*jose.JSONWebKey)
|
||||
if !ok || val == nil {
|
||||
return nil, acme.NewErrorISE("jwk expected in request context")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// jwsFromContext searches the context for a JWS. Returns the JWS or an error.
|
||||
func jwsFromContext(ctx context.Context) (*jose.JSONWebSignature, error) {
|
||||
val, ok := ctx.Value(jwsContextKey).(*jose.JSONWebSignature)
|
||||
if !ok || val == nil {
|
||||
return nil, acme.NewErrorISE("jws expected in request context")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// provisionerFromContext searches the context for a provisioner. Returns the
|
||||
// provisioner or an error.
|
||||
func provisionerFromContext(ctx context.Context) (acme.Provisioner, error) {
|
||||
p, ok := acme.ProvisionerFromContext(ctx)
|
||||
if !ok || p == nil {
|
||||
return nil, acme.NewErrorISE("provisioner expected in request context")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// acmeProvisionerFromContext searches the context for an ACME provisioner. Returns
|
||||
// pointer to an ACME provisioner or an error.
|
||||
func acmeProvisionerFromContext(ctx context.Context) (*provisioner.ACME, error) {
|
||||
p, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ap, ok := p.(*provisioner.ACME)
|
||||
if !ok {
|
||||
return nil, acme.NewErrorISE("provisioner in context is not an ACME provisioner")
|
||||
}
|
||||
|
||||
return ap, nil
|
||||
}
|
||||
|
||||
// payloadFromContext searches the context for a payload. Returns the payload
|
||||
// or an error.
|
||||
func payloadFromContext(ctx context.Context) (*payloadInfo, error) {
|
||||
val, ok := ctx.Value(payloadContextKey).(*payloadInfo)
|
||||
if !ok || val == nil {
|
||||
return nil, acme.NewErrorISE("payload expected in request context")
|
||||
}
|
||||
return val, nil
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,407 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"go.step.sm/crypto/randutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority/policy"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
// NewOrderRequest represents the body for a NewOrder request.
|
||||
type NewOrderRequest struct {
|
||||
Identifiers []acme.Identifier `json:"identifiers"`
|
||||
NotBefore time.Time `json:"notBefore,omitempty"`
|
||||
NotAfter time.Time `json:"notAfter,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates a new-order request body.
|
||||
func (n *NewOrderRequest) Validate() error {
|
||||
if len(n.Identifiers) == 0 {
|
||||
return acme.NewError(acme.ErrorMalformedType, "identifiers list cannot be empty")
|
||||
}
|
||||
for _, id := range n.Identifiers {
|
||||
switch id.Type {
|
||||
case acme.IP:
|
||||
if net.ParseIP(id.Value) == nil {
|
||||
return acme.NewError(acme.ErrorMalformedType, "invalid IP address: %s", id.Value)
|
||||
}
|
||||
case acme.DNS:
|
||||
value, _ := trimIfWildcard(id.Value)
|
||||
if _, err := x509util.SanitizeName(value); err != nil {
|
||||
return acme.NewError(acme.ErrorMalformedType, "invalid DNS name: %s", id.Value)
|
||||
}
|
||||
case acme.PermanentIdentifier:
|
||||
if id.Value == "" {
|
||||
return acme.NewError(acme.ErrorMalformedType, "permanent identifier cannot be empty")
|
||||
}
|
||||
default:
|
||||
return acme.NewError(acme.ErrorMalformedType, "identifier type unsupported: %s", id.Type)
|
||||
}
|
||||
|
||||
// TODO(hs): add some validations for DNS domains?
|
||||
// TODO(hs): combine the errors from this with allow/deny policy, like example error in https://datatracker.ietf.org/doc/html/rfc8555#section-6.7.1
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FinalizeRequest captures the body for a Finalize order request.
|
||||
type FinalizeRequest struct {
|
||||
CSR string `json:"csr"`
|
||||
csr *x509.CertificateRequest
|
||||
}
|
||||
|
||||
// Validate validates a finalize request body.
|
||||
func (f *FinalizeRequest) Validate() error {
|
||||
var err error
|
||||
// RFC 8555 isn't 100% conclusive about using raw base64-url encoding for the
|
||||
// CSR specifically, instead of "normal" base64-url encoding (incl. padding).
|
||||
// By trimming the padding from CSRs submitted by ACME clients that use
|
||||
// base64-url encoding instead of raw base64-url encoding, these are also
|
||||
// supported. This was reported in https://github.com/smallstep/certificates/issues/939
|
||||
// to be the case for a Synology DSM NAS system.
|
||||
csrBytes, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(f.CSR, "="))
|
||||
if err != nil {
|
||||
return acme.WrapError(acme.ErrorMalformedType, err, "error base64url decoding csr")
|
||||
}
|
||||
f.csr, err = x509.ParseCertificateRequest(csrBytes)
|
||||
if err != nil {
|
||||
return acme.WrapError(acme.ErrorMalformedType, err, "unable to parse csr")
|
||||
}
|
||||
if err = f.csr.CheckSignature(); err != nil {
|
||||
return acme.WrapError(acme.ErrorMalformedType, err, "csr failed signature check")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultOrderExpiry = time.Hour * 24
|
||||
var defaultOrderBackdate = time.Minute
|
||||
|
||||
// NewOrder ACME api for creating a new order.
|
||||
func NewOrder(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ca := mustAuthority(ctx)
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
payload, err := payloadFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var nor NewOrderRequest
|
||||
if err := json.Unmarshal(payload.value, &nor); err != nil {
|
||||
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
|
||||
"failed to unmarshal new-order request payload"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := nor.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(hs): gather all errors, so that we can build one response with ACME subproblems
|
||||
// include the nor.Validate() error here too, like in the example in the ACME RFC?
|
||||
|
||||
acmeProv, err := acmeProvisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var eak *acme.ExternalAccountKey
|
||||
if acmeProv.RequireEAB {
|
||||
if eak, err = db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID); err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error retrieving external account binding key"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
acmePolicy, err := newACMEPolicyEngine(eak)
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error creating ACME policy engine"))
|
||||
return
|
||||
}
|
||||
|
||||
for _, identifier := range nor.Identifiers {
|
||||
// evaluate the ACME account level policy
|
||||
if err = isIdentifierAllowed(acmePolicy, identifier); err != nil {
|
||||
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
|
||||
return
|
||||
}
|
||||
// evaluate the provisioner level policy
|
||||
orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value}
|
||||
if err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier); err != nil {
|
||||
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
|
||||
return
|
||||
}
|
||||
// evaluate the authority level policy
|
||||
if err = ca.AreSANsAllowed(ctx, []string{identifier.Value}); err != nil {
|
||||
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
now := clock.Now()
|
||||
// New order.
|
||||
o := &acme.Order{
|
||||
AccountID: acc.ID,
|
||||
ProvisionerID: prov.GetID(),
|
||||
Status: acme.StatusPending,
|
||||
Identifiers: nor.Identifiers,
|
||||
ExpiresAt: now.Add(defaultOrderExpiry),
|
||||
AuthorizationIDs: make([]string, len(nor.Identifiers)),
|
||||
NotBefore: nor.NotBefore,
|
||||
NotAfter: nor.NotAfter,
|
||||
}
|
||||
|
||||
for i, identifier := range o.Identifiers {
|
||||
az := &acme.Authorization{
|
||||
AccountID: acc.ID,
|
||||
Identifier: identifier,
|
||||
ExpiresAt: o.ExpiresAt,
|
||||
Status: acme.StatusPending,
|
||||
}
|
||||
if err := newAuthorization(ctx, az); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
o.AuthorizationIDs[i] = az.ID
|
||||
}
|
||||
|
||||
if o.NotBefore.IsZero() {
|
||||
o.NotBefore = now
|
||||
}
|
||||
if o.NotAfter.IsZero() {
|
||||
o.NotAfter = o.NotBefore.Add(prov.DefaultTLSCertDuration())
|
||||
}
|
||||
// If request NotBefore was empty then backdate the order.NotBefore (now)
|
||||
// to avoid timing issues.
|
||||
if nor.NotBefore.IsZero() {
|
||||
o.NotBefore = o.NotBefore.Add(-defaultOrderBackdate)
|
||||
}
|
||||
|
||||
if err := db.CreateOrder(ctx, o); err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error creating order"))
|
||||
return
|
||||
}
|
||||
|
||||
linker.LinkOrder(ctx, o)
|
||||
|
||||
w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID))
|
||||
render.JSONStatus(w, o, http.StatusCreated)
|
||||
}
|
||||
|
||||
func isIdentifierAllowed(acmePolicy policy.X509Policy, identifier acme.Identifier) error {
|
||||
if acmePolicy == nil {
|
||||
return nil
|
||||
}
|
||||
return acmePolicy.AreSANsAllowed([]string{identifier.Value})
|
||||
}
|
||||
|
||||
func newACMEPolicyEngine(eak *acme.ExternalAccountKey) (policy.X509Policy, error) {
|
||||
if eak == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return policy.NewX509PolicyEngine(eak.Policy)
|
||||
}
|
||||
|
||||
func trimIfWildcard(value string) (string, bool) {
|
||||
if strings.HasPrefix(value, "*.") {
|
||||
return strings.TrimPrefix(value, "*."), true
|
||||
}
|
||||
return value, false
|
||||
}
|
||||
|
||||
func newAuthorization(ctx context.Context, az *acme.Authorization) error {
|
||||
value, isWildcard := trimIfWildcard(az.Identifier.Value)
|
||||
az.Wildcard = isWildcard
|
||||
az.Identifier = acme.Identifier{
|
||||
Value: value,
|
||||
Type: az.Identifier.Type,
|
||||
}
|
||||
|
||||
chTypes := challengeTypes(az)
|
||||
|
||||
var err error
|
||||
az.Token, err = randutil.Alphanumeric(32)
|
||||
if err != nil {
|
||||
return acme.WrapErrorISE(err, "error generating random alphanumeric ID")
|
||||
}
|
||||
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
prov := acme.MustProvisionerFromContext(ctx)
|
||||
az.Challenges = make([]*acme.Challenge, 0, len(chTypes))
|
||||
for _, typ := range chTypes {
|
||||
if !prov.IsChallengeEnabled(ctx, provisioner.ACMEChallenge(typ)) {
|
||||
continue
|
||||
}
|
||||
|
||||
ch := &acme.Challenge{
|
||||
AccountID: az.AccountID,
|
||||
Value: az.Identifier.Value,
|
||||
Type: typ,
|
||||
Token: az.Token,
|
||||
Status: acme.StatusPending,
|
||||
}
|
||||
if err := db.CreateChallenge(ctx, ch); err != nil {
|
||||
return acme.WrapErrorISE(err, "error creating challenge")
|
||||
}
|
||||
az.Challenges = append(az.Challenges, ch)
|
||||
}
|
||||
if err = db.CreateAuthorization(ctx, az); err != nil {
|
||||
return acme.WrapErrorISE(err, "error creating authorization")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrder ACME api for retrieving an order.
|
||||
func GetOrder(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID"))
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error retrieving order"))
|
||||
return
|
||||
}
|
||||
if acc.ID != o.AccountID {
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"account '%s' does not own order '%s'", acc.ID, o.ID))
|
||||
return
|
||||
}
|
||||
if prov.GetID() != o.ProvisionerID {
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"provisioner '%s' does not own order '%s'", prov.GetID(), o.ID))
|
||||
return
|
||||
}
|
||||
if err = o.UpdateStatus(ctx, db); err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error updating order status"))
|
||||
return
|
||||
}
|
||||
|
||||
linker.LinkOrder(ctx, o)
|
||||
|
||||
w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID))
|
||||
render.JSON(w, o)
|
||||
}
|
||||
|
||||
// FinalizeOrder attempts to finalize an order and create a certificate.
|
||||
func FinalizeOrder(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
acc, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
payload, err := payloadFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
var fr FinalizeRequest
|
||||
if err := json.Unmarshal(payload.value, &fr); err != nil {
|
||||
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
|
||||
"failed to unmarshal finalize-order request payload"))
|
||||
return
|
||||
}
|
||||
if err := fr.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID"))
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error retrieving order"))
|
||||
return
|
||||
}
|
||||
if acc.ID != o.AccountID {
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"account '%s' does not own order '%s'", acc.ID, o.ID))
|
||||
return
|
||||
}
|
||||
if prov.GetID() != o.ProvisionerID {
|
||||
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
|
||||
"provisioner '%s' does not own order '%s'", prov.GetID(), o.ID))
|
||||
return
|
||||
}
|
||||
|
||||
ca := mustAuthority(ctx)
|
||||
if err = o.Finalize(ctx, db, fr.csr, ca, prov); err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error finalizing order"))
|
||||
return
|
||||
}
|
||||
|
||||
linker.LinkOrder(ctx, o)
|
||||
|
||||
w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID))
|
||||
render.JSON(w, o)
|
||||
}
|
||||
|
||||
// challengeTypes determines the types of challenges that should be used
|
||||
// for the ACME authorization request.
|
||||
func challengeTypes(az *acme.Authorization) []acme.ChallengeType {
|
||||
var chTypes []acme.ChallengeType
|
||||
|
||||
switch az.Identifier.Type {
|
||||
case acme.IP:
|
||||
chTypes = []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}
|
||||
case acme.DNS:
|
||||
chTypes = []acme.ChallengeType{acme.DNS01}
|
||||
// HTTP and TLS challenges can only be used for identifiers without wildcards.
|
||||
if !az.Wildcard {
|
||||
chTypes = append(chTypes, []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}...)
|
||||
}
|
||||
case acme.PermanentIdentifier:
|
||||
chTypes = []acme.ChallengeType{acme.DEVICEATTEST01}
|
||||
default:
|
||||
chTypes = []acme.ChallengeType{}
|
||||
}
|
||||
|
||||
return chTypes
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,291 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
type revokePayload struct {
|
||||
Certificate string `json:"certificate"`
|
||||
ReasonCode *int `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// RevokeCert attempts to revoke a certificate.
|
||||
func RevokeCert(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
db := acme.MustDatabaseFromContext(ctx)
|
||||
linker := acme.MustLinkerFromContext(ctx)
|
||||
|
||||
jws, err := jwsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov, err := provisionerFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := payloadFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var p revokePayload
|
||||
err = json.Unmarshal(payload.value, &p)
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error unmarshaling payload"))
|
||||
return
|
||||
}
|
||||
|
||||
certBytes, err := base64.RawURLEncoding.DecodeString(p.Certificate)
|
||||
if err != nil {
|
||||
// in this case the most likely cause is a client that didn't properly encode the certificate
|
||||
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "error base64url decoding payload certificate property"))
|
||||
return
|
||||
}
|
||||
|
||||
certToBeRevoked, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
// in this case a client may have encoded something different than a certificate
|
||||
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "error parsing certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
serial := certToBeRevoked.SerialNumber.String()
|
||||
dbCert, err := db.GetCertificateBySerial(ctx, serial)
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error retrieving certificate by serial"))
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(dbCert.Leaf.Raw, certToBeRevoked.Raw) {
|
||||
// this should never happen
|
||||
render.Error(w, acme.NewErrorISE("certificate raw bytes are not equal"))
|
||||
return
|
||||
}
|
||||
|
||||
if shouldCheckAccountFrom(jws) {
|
||||
account, err := accountFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
acmeErr := isAccountAuthorized(ctx, dbCert, certToBeRevoked, account)
|
||||
if acmeErr != nil {
|
||||
render.Error(w, acmeErr)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// if account doesn't need to be checked, the JWS should be verified to be signed by the
|
||||
// private key that belongs to the public key in the certificate to be revoked.
|
||||
_, err := jws.Verify(certToBeRevoked.PublicKey)
|
||||
if err != nil {
|
||||
// TODO(hs): possible to determine an error vs. unauthorized and thus provide an ISE vs. Unauthorized?
|
||||
render.Error(w, wrapUnauthorizedError(certToBeRevoked, nil, "verification of jws using certificate public key failed", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ca := mustAuthority(ctx)
|
||||
hasBeenRevokedBefore, err := ca.IsRevoked(serial)
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error retrieving revocation status of certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
if hasBeenRevokedBefore {
|
||||
render.Error(w, acme.NewError(acme.ErrorAlreadyRevokedType, "certificate was already revoked"))
|
||||
return
|
||||
}
|
||||
|
||||
reasonCode := p.ReasonCode
|
||||
acmeErr := validateReasonCode(reasonCode)
|
||||
if acmeErr != nil {
|
||||
render.Error(w, acmeErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Authorize revocation by ACME provisioner
|
||||
ctx = provisioner.NewContextWithMethod(ctx, provisioner.RevokeMethod)
|
||||
err = prov.AuthorizeRevoke(ctx, "")
|
||||
if err != nil {
|
||||
render.Error(w, acme.WrapErrorISE(err, "error authorizing revocation on provisioner"))
|
||||
return
|
||||
}
|
||||
|
||||
options := revokeOptions(serial, certToBeRevoked, reasonCode)
|
||||
err = ca.Revoke(ctx, options)
|
||||
if err != nil {
|
||||
render.Error(w, wrapRevokeErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
logRevoke(w, options)
|
||||
w.Header().Add("Link", link(linker.GetLink(ctx, acme.DirectoryLinkType), "index"))
|
||||
w.Write(nil)
|
||||
}
|
||||
|
||||
// isAccountAuthorized checks if an ACME account that was retrieved earlier is authorized
|
||||
// to revoke the certificate. An Account must always be valid in order to revoke a certificate.
|
||||
// In case the certificate retrieved from the database belongs to the Account, the Account is
|
||||
// authorized. If the certificate retrieved from the database doesn't belong to the Account,
|
||||
// the identifiers in the certificate are extracted and compared against the (valid) Authorizations
|
||||
// that are stored for the ACME Account. If these sets match, the Account is considered authorized
|
||||
// to revoke the certificate. If this check fails, the client will receive an unauthorized error.
|
||||
func isAccountAuthorized(_ context.Context, dbCert *acme.Certificate, certToBeRevoked *x509.Certificate, account *acme.Account) *acme.Error {
|
||||
if !account.IsValid() {
|
||||
return wrapUnauthorizedError(certToBeRevoked, nil, fmt.Sprintf("account '%s' has status '%s'", account.ID, account.Status), nil)
|
||||
}
|
||||
certificateBelongsToAccount := dbCert.AccountID == account.ID
|
||||
if certificateBelongsToAccount {
|
||||
return nil // return early
|
||||
}
|
||||
|
||||
// TODO(hs): according to RFC8555: 7.6, a server MUST consider the following accounts authorized
|
||||
// to revoke a certificate:
|
||||
//
|
||||
// o the account that issued the certificate.
|
||||
// o an account that holds authorizations for all of the identifiers in the certificate.
|
||||
//
|
||||
// We currently only support the first case. The second might result in step going OOM when
|
||||
// large numbers of Authorizations are involved when the current nosql interface is in use.
|
||||
// We want to protect users from this failure scenario, so that's why it hasn't been added yet.
|
||||
// This issue is tracked in https://github.com/smallstep/certificates/issues/767
|
||||
|
||||
// not authorized; fail closed.
|
||||
return wrapUnauthorizedError(certToBeRevoked, nil, fmt.Sprintf("account '%s' is not authorized", account.ID), nil)
|
||||
}
|
||||
|
||||
// wrapRevokeErr is a best effort implementation to transform an error during
|
||||
// revocation into an ACME error, so that clients can understand the error.
|
||||
func wrapRevokeErr(err error) *acme.Error {
|
||||
t := err.Error()
|
||||
if strings.Contains(t, "is already revoked") {
|
||||
return acme.NewError(acme.ErrorAlreadyRevokedType, t)
|
||||
}
|
||||
return acme.WrapErrorISE(err, "error when revoking certificate")
|
||||
}
|
||||
|
||||
// unauthorizedError returns an ACME error indicating the request was
|
||||
// not authorized to revoke the certificate.
|
||||
func wrapUnauthorizedError(cert *x509.Certificate, unauthorizedIdentifiers []acme.Identifier, msg string, err error) *acme.Error {
|
||||
var acmeErr *acme.Error
|
||||
if err == nil {
|
||||
acmeErr = acme.NewError(acme.ErrorUnauthorizedType, msg)
|
||||
} else {
|
||||
acmeErr = acme.WrapError(acme.ErrorUnauthorizedType, err, msg)
|
||||
}
|
||||
acmeErr.Status = http.StatusForbidden // RFC8555 7.6 shows example with 403
|
||||
|
||||
switch {
|
||||
case len(unauthorizedIdentifiers) > 0:
|
||||
identifier := unauthorizedIdentifiers[0] // picking the first; compound may be an option too?
|
||||
acmeErr.Detail = fmt.Sprintf("No authorization provided for name %s", identifier.Value)
|
||||
case cert.Subject.String() != "":
|
||||
acmeErr.Detail = fmt.Sprintf("No authorization provided for name %s", cert.Subject.CommonName)
|
||||
default:
|
||||
acmeErr.Detail = "No authorization provided"
|
||||
}
|
||||
|
||||
return acmeErr
|
||||
}
|
||||
|
||||
// logRevoke logs successful revocation of certificate
|
||||
func logRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"serial": ri.Serial,
|
||||
"reasonCode": ri.ReasonCode,
|
||||
"reason": ri.Reason,
|
||||
"passiveOnly": ri.PassiveOnly,
|
||||
"ACME": ri.ACME,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// validateReasonCode validates the revocation reason
|
||||
func validateReasonCode(reasonCode *int) *acme.Error {
|
||||
if reasonCode != nil && ((*reasonCode < ocsp.Unspecified || *reasonCode > ocsp.AACompromise) || *reasonCode == 7) {
|
||||
return acme.NewError(acme.ErrorBadRevocationReasonType, "reasonCode out of bounds")
|
||||
}
|
||||
// NOTE: it's possible to add additional requirements to the reason code:
|
||||
// The server MAY disallow a subset of reasonCodes from being
|
||||
// used by the user. If a request contains a disallowed reasonCode,
|
||||
// then the server MUST reject it with the error type
|
||||
// "urn:ietf:params:acme:error:badRevocationReason"
|
||||
// No additional checks have been implemented so far.
|
||||
return nil
|
||||
}
|
||||
|
||||
// revokeOptions determines the RevokeOptions for the Authority to use in revocation
|
||||
func revokeOptions(serial string, certToBeRevoked *x509.Certificate, reasonCode *int) *authority.RevokeOptions {
|
||||
opts := &authority.RevokeOptions{
|
||||
Serial: serial,
|
||||
ACME: true,
|
||||
Crt: certToBeRevoked,
|
||||
}
|
||||
if reasonCode != nil { // NOTE: when implementing CRL and/or OCSP, and reason code is missing, CRL entry extension should be omitted
|
||||
opts.Reason = reason(*reasonCode)
|
||||
opts.ReasonCode = *reasonCode
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// reason transforms an integer reason code to a
|
||||
// textual description of the revocation reason.
|
||||
func reason(reasonCode int) string {
|
||||
switch reasonCode {
|
||||
case ocsp.Unspecified:
|
||||
return "unspecified reason"
|
||||
case ocsp.KeyCompromise:
|
||||
return "key compromised"
|
||||
case ocsp.CACompromise:
|
||||
return "ca compromised"
|
||||
case ocsp.AffiliationChanged:
|
||||
return "affiliation changed"
|
||||
case ocsp.Superseded:
|
||||
return "superseded"
|
||||
case ocsp.CessationOfOperation:
|
||||
return "cessation of operation"
|
||||
case ocsp.CertificateHold:
|
||||
return "certificate hold"
|
||||
case ocsp.RemoveFromCRL:
|
||||
return "remove from crl"
|
||||
case ocsp.PrivilegeWithdrawn:
|
||||
return "privilege withdrawn"
|
||||
case ocsp.AACompromise:
|
||||
return "aa compromised"
|
||||
default:
|
||||
return "unspecified reason"
|
||||
}
|
||||
}
|
||||
|
||||
// shouldCheckAccountFrom indicates whether an account should be
|
||||
// retrieved from the context, so that it can be used for
|
||||
// additional checks. This should only be done when no JWK
|
||||
// can be extracted from the request, as that would indicate
|
||||
// that the revocation request was signed with a certificate
|
||||
// key pair (and not an account key pair). Looking up such
|
||||
// a JWK would result in no Account being found.
|
||||
func shouldCheckAccountFrom(jws *jose.JSONWebSignature) bool {
|
||||
return !canExtractJWKFrom(jws)
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,70 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Authorization representst an ACME Authorization.
|
||||
type Authorization struct {
|
||||
ID string `json:"-"`
|
||||
AccountID string `json:"-"`
|
||||
Token string `json:"-"`
|
||||
Fingerprint string `json:"-"`
|
||||
Identifier Identifier `json:"identifier"`
|
||||
Status Status `json:"status"`
|
||||
Challenges []*Challenge `json:"challenges"`
|
||||
Wildcard bool `json:"wildcard"`
|
||||
ExpiresAt time.Time `json:"expires"`
|
||||
Error *Error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ToLog enables response logging.
|
||||
func (az *Authorization) ToLog() (interface{}, error) {
|
||||
b, err := json.Marshal(az)
|
||||
if err != nil {
|
||||
return nil, WrapErrorISE(err, "error marshaling authz for logging")
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// UpdateStatus updates the ACME Authorization Status if necessary.
|
||||
// Changes to the Authorization are saved using the database interface.
|
||||
func (az *Authorization) UpdateStatus(ctx context.Context, db DB) error {
|
||||
now := clock.Now()
|
||||
|
||||
switch az.Status {
|
||||
case StatusInvalid:
|
||||
return nil
|
||||
case StatusValid:
|
||||
return nil
|
||||
case StatusPending:
|
||||
// check expiry
|
||||
if now.After(az.ExpiresAt) {
|
||||
az.Status = StatusInvalid
|
||||
break
|
||||
}
|
||||
|
||||
var isValid = false
|
||||
for _, ch := range az.Challenges {
|
||||
if ch.Status == StatusValid {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValid {
|
||||
return nil
|
||||
}
|
||||
az.Status = StatusValid
|
||||
az.Error = nil
|
||||
default:
|
||||
return NewErrorISE("unrecognized authorization status: %s", az.Status)
|
||||
}
|
||||
|
||||
if err := db.UpdateAuthorization(ctx, az); err != nil {
|
||||
return WrapErrorISE(err, "error updating authorization")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
)
|
||||
|
||||
func TestAuthorization_UpdateStatus(t *testing.T) {
|
||||
type test struct {
|
||||
az *Authorization
|
||||
err *Error
|
||||
db DB
|
||||
}
|
||||
tests := map[string]func(t *testing.T) test{
|
||||
"ok/already-invalid": func(t *testing.T) test {
|
||||
az := &Authorization{
|
||||
Status: StatusInvalid,
|
||||
}
|
||||
return test{
|
||||
az: az,
|
||||
}
|
||||
},
|
||||
"ok/already-valid": func(t *testing.T) test {
|
||||
az := &Authorization{
|
||||
Status: StatusInvalid,
|
||||
}
|
||||
return test{
|
||||
az: az,
|
||||
}
|
||||
},
|
||||
"fail/error-unexpected-status": func(t *testing.T) test {
|
||||
az := &Authorization{
|
||||
Status: "foo",
|
||||
}
|
||||
return test{
|
||||
az: az,
|
||||
err: NewErrorISE("unrecognized authorization status: %s", az.Status),
|
||||
}
|
||||
},
|
||||
"ok/expired": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
az := &Authorization{
|
||||
ID: "azID",
|
||||
AccountID: "accID",
|
||||
Status: StatusPending,
|
||||
ExpiresAt: now.Add(-5 * time.Minute),
|
||||
}
|
||||
return test{
|
||||
az: az,
|
||||
db: &MockDB{
|
||||
MockUpdateAuthorization: func(ctx context.Context, updaz *Authorization) error {
|
||||
assert.Equals(t, updaz.ID, az.ID)
|
||||
assert.Equals(t, updaz.AccountID, az.AccountID)
|
||||
assert.Equals(t, updaz.Status, StatusInvalid)
|
||||
assert.Equals(t, updaz.ExpiresAt, az.ExpiresAt)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"fail/db.UpdateAuthorization-error": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
az := &Authorization{
|
||||
ID: "azID",
|
||||
AccountID: "accID",
|
||||
Status: StatusPending,
|
||||
ExpiresAt: now.Add(-5 * time.Minute),
|
||||
}
|
||||
return test{
|
||||
az: az,
|
||||
db: &MockDB{
|
||||
MockUpdateAuthorization: func(ctx context.Context, updaz *Authorization) error {
|
||||
assert.Equals(t, updaz.ID, az.ID)
|
||||
assert.Equals(t, updaz.AccountID, az.AccountID)
|
||||
assert.Equals(t, updaz.Status, StatusInvalid)
|
||||
assert.Equals(t, updaz.ExpiresAt, az.ExpiresAt)
|
||||
return errors.New("force")
|
||||
},
|
||||
},
|
||||
err: NewErrorISE("error updating authorization: force"),
|
||||
}
|
||||
},
|
||||
"ok/no-valid-challenges": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
az := &Authorization{
|
||||
ID: "azID",
|
||||
AccountID: "accID",
|
||||
Status: StatusPending,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
Challenges: []*Challenge{
|
||||
{Status: StatusPending}, {Status: StatusPending}, {Status: StatusPending},
|
||||
},
|
||||
}
|
||||
return test{
|
||||
az: az,
|
||||
}
|
||||
},
|
||||
"ok/valid": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
az := &Authorization{
|
||||
ID: "azID",
|
||||
AccountID: "accID",
|
||||
Status: StatusPending,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
Challenges: []*Challenge{
|
||||
{Status: StatusPending}, {Status: StatusPending}, {Status: StatusValid},
|
||||
},
|
||||
}
|
||||
return test{
|
||||
az: az,
|
||||
db: &MockDB{
|
||||
MockUpdateAuthorization: func(ctx context.Context, updaz *Authorization) error {
|
||||
assert.Equals(t, updaz.ID, az.ID)
|
||||
assert.Equals(t, updaz.AccountID, az.AccountID)
|
||||
assert.Equals(t, updaz.Status, StatusValid)
|
||||
assert.Equals(t, updaz.ExpiresAt, az.ExpiresAt)
|
||||
assert.Equals(t, updaz.Error, nil)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run(t)
|
||||
if err := tc.az.UpdateStatus(context.Background(), tc.db); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
var k *Error
|
||||
if errors.As(err, &k) {
|
||||
assert.Equals(t, k.Type, tc.err.Type)
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
assert.Equals(t, k.Status, tc.err.Status)
|
||||
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
|
||||
assert.Equals(t, k.Detail, tc.err.Detail)
|
||||
} else {
|
||||
assert.FatalError(t, errors.New("unexpected error type"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
)
|
||||
|
||||
// Certificate options with which to create and store a cert object.
|
||||
type Certificate struct {
|
||||
ID string
|
||||
AccountID string
|
||||
OrderID string
|
||||
Leaf *x509.Certificate
|
||||
Intermediates []*x509.Certificate
|
||||
}
|
1124
acme/challenge.go
1124
acme/challenge.go
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,860 +0,0 @@
|
|||
//go:build tpmsimulator
|
||||
// +build tpmsimulator
|
||||
|
||||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/smallstep/go-attestation/attest"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/minica"
|
||||
"go.step.sm/crypto/tpm"
|
||||
"go.step.sm/crypto/tpm/simulator"
|
||||
tpmstorage "go.step.sm/crypto/tpm/storage"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
func newSimulatedTPM(t *testing.T) *tpm.TPM {
|
||||
t.Helper()
|
||||
tmpDir := t.TempDir()
|
||||
tpm, err := tpm.New(withSimulator(t), tpm.WithStore(tpmstorage.NewDirstore(tmpDir))) // TODO: provide in-memory storage implementation instead
|
||||
require.NoError(t, err)
|
||||
return tpm
|
||||
}
|
||||
|
||||
func withSimulator(t *testing.T) tpm.NewTPMOption {
|
||||
t.Helper()
|
||||
var sim simulator.Simulator
|
||||
t.Cleanup(func() {
|
||||
if sim == nil {
|
||||
return
|
||||
}
|
||||
err := sim.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
sim, err := simulator.New()
|
||||
require.NoError(t, err)
|
||||
err = sim.Open()
|
||||
require.NoError(t, err)
|
||||
return tpm.WithSimulator(sim)
|
||||
}
|
||||
|
||||
func generateKeyID(t *testing.T, pub crypto.PublicKey) []byte {
|
||||
t.Helper()
|
||||
b, err := x509.MarshalPKIXPublicKey(pub)
|
||||
require.NoError(t, err)
|
||||
hash := sha256.Sum256(b)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func mustAttestTPM(t *testing.T, keyAuthorization string, permanentIdentifiers []string) ([]byte, crypto.Signer, *x509.Certificate) {
|
||||
t.Helper()
|
||||
aca, err := minica.New(
|
||||
minica.WithName("TPM Testing"),
|
||||
minica.WithGetSignerFunc(
|
||||
func() (crypto.Signer, error) {
|
||||
return keyutil.GenerateSigner("RSA", "", 2048)
|
||||
},
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// prepare simulated TPM and create an AK
|
||||
stpm := newSimulatedTPM(t)
|
||||
eks, err := stpm.GetEKs(context.Background())
|
||||
require.NoError(t, err)
|
||||
ak, err := stpm.CreateAK(context.Background(), "first-ak")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ak)
|
||||
|
||||
// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
|
||||
ap, err := ak.AttestationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create template and sign certificate for the AK public key
|
||||
keyID := generateKeyID(t, eks[0].Public())
|
||||
template := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
sans := []x509util.SubjectAlternativeName{}
|
||||
uris := []*url.URL{{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)}}
|
||||
for _, pi := range permanentIdentifiers {
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.PermanentIdentifierType,
|
||||
Value: pi,
|
||||
})
|
||||
}
|
||||
asn1Value := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.DirectoryNameType,
|
||||
ASN1Value: asn1Value,
|
||||
})
|
||||
ext, err := createSubjectAltNameExtension(nil, nil, nil, uris, sans, true)
|
||||
require.NoError(t, err)
|
||||
ext.Set(template)
|
||||
akCert, err := aca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, akCert)
|
||||
|
||||
// create a new key attested by the AK, while including
|
||||
// the key authorization bytes as qualifying data.
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
||||
config := tpm.AttestKeyConfig{
|
||||
Algorithm: "RSA",
|
||||
Size: 2048,
|
||||
QualifyingData: keyAuthSum[:],
|
||||
}
|
||||
key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
require.Equal(t, "first-key", key.Name())
|
||||
require.NotEqual(t, 0, len(key.Data()))
|
||||
require.Equal(t, "first-ak", key.AttestedBy())
|
||||
require.True(t, key.WasAttested())
|
||||
require.True(t, key.WasAttestedBy(ak))
|
||||
|
||||
signer, err := key.Signer(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// prepare the attestation object with the AK certificate chain,
|
||||
// the attested key, its metadata and the signature signed by the
|
||||
// AK.
|
||||
params, err := key.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
attObj, err := cbor.Marshal(struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// marshal the ACME payload
|
||||
payload, err := json.Marshal(struct {
|
||||
AttObj string `json:"attObj"`
|
||||
}{
|
||||
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return payload, signer, aca.Root
|
||||
}
|
||||
|
||||
func Test_deviceAttest01ValidateWithTPMSimulator(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
ch *Challenge
|
||||
db DB
|
||||
jwk *jose.JSONWebKey
|
||||
payload []byte
|
||||
}
|
||||
type test struct {
|
||||
args args
|
||||
wantErr *Error
|
||||
}
|
||||
tests := map[string]func(t *testing.T) test{
|
||||
"ok/doTPMAttestationFormat-storeError": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, _, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
|
||||
// parse payload, set invalid "ver", remarshal
|
||||
var p payloadType
|
||||
err := json.Unmarshal(payload, &p)
|
||||
require.NoError(t, err)
|
||||
attObj, err := base64.RawURLEncoding.DecodeString(p.AttObj)
|
||||
require.NoError(t, err)
|
||||
att := attestationObject{}
|
||||
err = cbor.Unmarshal(attObj, &att)
|
||||
require.NoError(t, err)
|
||||
att.AttStatement["ver"] = "bogus"
|
||||
attObj, err = cbor.Marshal(struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}{
|
||||
Format: "tpm",
|
||||
AttStatement: att.AttStatement,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
payload, err = json.Marshal(struct {
|
||||
AttObj string `json:"attObj"`
|
||||
}{
|
||||
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusInvalid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
|
||||
err := NewError(ErrorBadAttestationStatementType, `version "bogus" is not supported`)
|
||||
|
||||
assert.EqualError(t, updch.Error.Err, err.Err.Error())
|
||||
assert.Equal(t, err.Type, updch.Error.Type)
|
||||
assert.Equal(t, err.Detail, updch.Error.Detail)
|
||||
assert.Equal(t, err.Status, updch.Error.Status)
|
||||
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok with invalid PermanentIdentifier SAN": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, _, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.99999999",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusInvalid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.99999999", updch.Value)
|
||||
|
||||
err := NewError(ErrorRejectedIdentifierType, `permanent identifier does not match`).
|
||||
AddSubproblems(NewSubproblemWithIdentifier(
|
||||
ErrorMalformedType,
|
||||
Identifier{Type: "permanent-identifier", Value: "device.id.99999999"},
|
||||
`challenge identifier "device.id.99999999" doesn't match any of the attested hardware identifiers ["device.id.12345678"]`,
|
||||
))
|
||||
|
||||
assert.EqualError(t, updch.Error.Err, err.Err.Error())
|
||||
assert.Equal(t, err.Type, updch.Error.Type)
|
||||
assert.Equal(t, err.Detail, updch.Error.Detail)
|
||||
assert.Equal(t, err.Status, updch.Error.Status)
|
||||
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, signer, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "azID", az.ID)
|
||||
assert.Equal(t, fingerprint, az.Fingerprint)
|
||||
return nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusValid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok with PermanentIdentifier SAN": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, signer, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "azID", az.ID)
|
||||
assert.Equal(t, fingerprint, az.Fingerprint)
|
||||
return nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusValid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run(t)
|
||||
|
||||
if err := deviceAttest01Validate(tc.args.ctx, tc.args.ch, tc.args.db, tc.args.jwk, tc.args.payload); err != nil {
|
||||
assert.Error(t, tc.wantErr)
|
||||
assert.EqualError(t, err, tc.wantErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Nil(t, tc.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newBadAttestationStatementError(msg string) *Error {
|
||||
return &Error{
|
||||
Type: "urn:ietf:params:acme:error:badAttestationStatement",
|
||||
Status: 400,
|
||||
Err: errors.New(msg),
|
||||
}
|
||||
}
|
||||
|
||||
func newInternalServerError(msg string) *Error {
|
||||
return &Error{
|
||||
Type: "urn:ietf:params:acme:error:serverInternal",
|
||||
Status: 500,
|
||||
Err: errors.New(msg),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
oidPermanentIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}
|
||||
oidHardwareModuleNameIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 4}
|
||||
)
|
||||
|
||||
func Test_doTPMAttestationFormat(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
aca, err := minica.New(
|
||||
minica.WithName("TPM Testing"),
|
||||
minica.WithGetSignerFunc(
|
||||
func() (crypto.Signer, error) {
|
||||
return keyutil.GenerateSigner("RSA", "", 2048)
|
||||
},
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
acaRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: aca.Root.Raw})
|
||||
|
||||
// prepare simulated TPM and create an AK
|
||||
stpm := newSimulatedTPM(t)
|
||||
eks, err := stpm.GetEKs(context.Background())
|
||||
require.NoError(t, err)
|
||||
ak, err := stpm.CreateAK(context.Background(), "first-ak")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ak)
|
||||
|
||||
// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
|
||||
ap, err := ak.AttestationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create template and sign certificate for the AK public key
|
||||
keyID := generateKeyID(t, eks[0].Public())
|
||||
template := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
sans := []x509util.SubjectAlternativeName{}
|
||||
uris := []*url.URL{{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)}}
|
||||
asn1Value := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.DirectoryNameType,
|
||||
ASN1Value: asn1Value,
|
||||
})
|
||||
ext, err := createSubjectAltNameExtension(nil, nil, nil, uris, sans, true)
|
||||
require.NoError(t, err)
|
||||
ext.Set(template)
|
||||
akCert, err := aca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, akCert)
|
||||
|
||||
invalidTemplate := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
invalidAKCert, err := aca.Sign(invalidTemplate)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, invalidAKCert)
|
||||
|
||||
// generate a JWK and the key authorization value
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
keyAuthorization, err := KeyAuthorization("token", jwk)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a new key attested by the AK, while including
|
||||
// the key authorization bytes as qualifying data.
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
||||
config := tpm.AttestKeyConfig{
|
||||
Algorithm: "RSA",
|
||||
Size: 2048,
|
||||
QualifyingData: keyAuthSum[:],
|
||||
}
|
||||
key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
params, err := key.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
signer, err := key.Signer(context.Background())
|
||||
require.NoError(t, err)
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
// attest another key and get its certification parameters
|
||||
anotherKey, err := stpm.AttestKey(context.Background(), "first-ak", "another-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
anotherKeyParams, err := anotherKey.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
prov Provisioner
|
||||
ch *Challenge
|
||||
jwk *jose.JSONWebKey
|
||||
att *attestationObject
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *tpmAttestationData
|
||||
expErr *Error
|
||||
}{
|
||||
{"ok", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, nil},
|
||||
{"fail ver not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("ver not present")},
|
||||
{"fail ver type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": []interface{}{},
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("ver not present")},
|
||||
{"fail bogus ver", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "bogus",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError(`version "bogus" is not supported`)},
|
||||
{"fail x5c not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c not present")},
|
||||
{"fail x5c type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": [][]byte{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c not present")},
|
||||
{"fail x5c empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is empty")},
|
||||
{"fail leaf type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{"leaf", aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed")},
|
||||
{"fail leaf parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw[:100], aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
|
||||
{"fail intermediate type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, "intermediate"},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed")},
|
||||
{"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw[:100]},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
|
||||
{"fail roots", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newInternalServerError("no root CA bundle available to verify the attestation certificate")},
|
||||
{"fail verify", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is not valid: x509: certificate signed by unknown authority")},
|
||||
{"fail validateAKCertificate", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{invalidAKCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("AK certificate is not valid: missing TPM manufacturer")},
|
||||
{"fail pubArea not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
|
||||
{"fail pubArea type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": []interface{}{},
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
|
||||
{"fail pubArea empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": []byte{},
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("pubArea is empty")},
|
||||
{"fail sig not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
|
||||
{"fail sig type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": []interface{}{},
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
|
||||
{"fail sig empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": []byte{},
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("sig is empty")},
|
||||
{"fail certInfo not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
|
||||
{"fail certInfo type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": []interface{}{},
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
|
||||
{"fail certInfo empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": []byte{},
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("certInfo is empty")},
|
||||
{"fail alg not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid alg in attestation statement")},
|
||||
{"fail alg type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(0), // invalid alg
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid alg 0 in attestation statement")},
|
||||
{"fail attestation verification", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": anotherKeyParams.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certification parameters: certification refers to a different key")},
|
||||
{"fail keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, &jose.JSONWebKey{Key: []byte("not an asymmetric key")}, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newInternalServerError("failed creating key auth digest: error generating JWK thumbprint: square/go-jose: unknown key type '[]uint8'")},
|
||||
{"fail different keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "aDifferentToken"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), //
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("key authorization does not match")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := doTPMAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
|
||||
if tt.expErr != nil {
|
||||
var ae *Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.EqualError(t, err, tt.expErr.Error())
|
||||
assert.Equal(t, ae.StatusCode(), tt.expErr.StatusCode())
|
||||
assert.Equal(t, ae.Type, tt.expErr.Type)
|
||||
}
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, got) {
|
||||
assert.Equal(t, akCert, got.Certificate)
|
||||
assert.Equal(t, [][]*x509.Certificate{
|
||||
{
|
||||
akCert, aca.Intermediate, aca.Root,
|
||||
},
|
||||
}, got.VerifiedChains)
|
||||
assert.Equal(t, fingerprint, got.Fingerprint)
|
||||
assert.Empty(t, got.PermanentIdentifiers) // currently expected to be always empty
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client is the interface used to verify ACME challenges.
|
||||
type Client interface {
|
||||
// Get issues an HTTP GET to the specified URL.
|
||||
Get(url string) (*http.Response, error)
|
||||
|
||||
// LookupTXT returns the DNS TXT records for the given domain name.
|
||||
LookupTxt(name string) ([]string, error)
|
||||
|
||||
// TLSDial connects to the given network address using net.Dialer and then
|
||||
// initiates a TLS handshake, returning the resulting TLS connection.
|
||||
TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error)
|
||||
}
|
||||
|
||||
type clientKey struct{}
|
||||
|
||||
// NewClientContext adds the given client to the context.
|
||||
func NewClientContext(ctx context.Context, c Client) context.Context {
|
||||
return context.WithValue(ctx, clientKey{}, c)
|
||||
}
|
||||
|
||||
// ClientFromContext returns the current client from the given context.
|
||||
func ClientFromContext(ctx context.Context) (c Client, ok bool) {
|
||||
c, ok = ctx.Value(clientKey{}).(Client)
|
||||
return
|
||||
}
|
||||
|
||||
// MustClientFromContext returns the current client from the given context. It will
|
||||
// return a new instance of the client if it does not exist.
|
||||
func MustClientFromContext(ctx context.Context) Client {
|
||||
c, ok := ClientFromContext(ctx)
|
||||
if !ok {
|
||||
return NewClient()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type client struct {
|
||||
http *http.Client
|
||||
dialer *net.Dialer
|
||||
}
|
||||
|
||||
// NewClient returns an implementation of Client for verifying ACME challenges.
|
||||
func NewClient() Client {
|
||||
return &client{
|
||||
http: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
//nolint:gosec // used on tls-alpn-01 challenge
|
||||
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
|
||||
},
|
||||
},
|
||||
},
|
||||
dialer: &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) Get(url string) (*http.Response, error) {
|
||||
return c.http.Get(url)
|
||||
}
|
||||
|
||||
func (c *client) LookupTxt(name string) ([]string, error) {
|
||||
return net.LookupTXT(name)
|
||||
}
|
||||
|
||||
func (c *client) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) {
|
||||
return tls.DialWithDialer(c.dialer, network, addr, config)
|
||||
}
|
199
acme/common.go
199
acme/common.go
|
@ -1,199 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
// Clock that returns time in UTC rounded to seconds.
|
||||
type Clock struct{}
|
||||
|
||||
// Now returns the UTC time rounded to seconds.
|
||||
func (c *Clock) Now() time.Time {
|
||||
return time.Now().UTC().Truncate(time.Second)
|
||||
}
|
||||
|
||||
var clock Clock
|
||||
|
||||
// CertificateAuthority is the interface implemented by a CA authority.
|
||||
type CertificateAuthority interface {
|
||||
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
AreSANsAllowed(ctx context.Context, sans []string) error
|
||||
IsRevoked(sn string) (bool, error)
|
||||
Revoke(context.Context, *authority.RevokeOptions) error
|
||||
LoadProvisionerByName(string) (provisioner.Interface, error)
|
||||
}
|
||||
|
||||
// NewContext adds the given acme components to the context.
|
||||
func NewContext(ctx context.Context, db DB, client Client, linker Linker, fn PrerequisitesChecker) context.Context {
|
||||
ctx = NewDatabaseContext(ctx, db)
|
||||
ctx = NewClientContext(ctx, client)
|
||||
ctx = NewLinkerContext(ctx, linker)
|
||||
// Prerequisite checker is optional.
|
||||
if fn != nil {
|
||||
ctx = NewPrerequisitesCheckerContext(ctx, fn)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// PrerequisitesChecker is a function that checks if all prerequisites for
|
||||
// serving ACME are met by the CA configuration.
|
||||
type PrerequisitesChecker func(ctx context.Context) (bool, error)
|
||||
|
||||
// DefaultPrerequisitesChecker is the default PrerequisiteChecker and returns
|
||||
// always true.
|
||||
func DefaultPrerequisitesChecker(context.Context) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type prerequisitesKey struct{}
|
||||
|
||||
// NewPrerequisitesCheckerContext adds the given PrerequisitesChecker to the
|
||||
// context.
|
||||
func NewPrerequisitesCheckerContext(ctx context.Context, fn PrerequisitesChecker) context.Context {
|
||||
return context.WithValue(ctx, prerequisitesKey{}, fn)
|
||||
}
|
||||
|
||||
// PrerequisitesCheckerFromContext returns the PrerequisitesChecker in the
|
||||
// context.
|
||||
func PrerequisitesCheckerFromContext(ctx context.Context) (PrerequisitesChecker, bool) {
|
||||
fn, ok := ctx.Value(prerequisitesKey{}).(PrerequisitesChecker)
|
||||
return fn, ok && fn != nil
|
||||
}
|
||||
|
||||
// Provisioner is an interface that implements a subset of the provisioner.Interface --
|
||||
// only those methods required by the ACME api/authority.
|
||||
type Provisioner interface {
|
||||
AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error
|
||||
AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error)
|
||||
AuthorizeRevoke(ctx context.Context, token string) error
|
||||
IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool
|
||||
IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool
|
||||
GetAttestationRoots() (*x509.CertPool, bool)
|
||||
GetID() string
|
||||
GetName() string
|
||||
DefaultTLSCertDuration() time.Duration
|
||||
GetOptions() *provisioner.Options
|
||||
}
|
||||
|
||||
type provisionerKey struct{}
|
||||
|
||||
// NewProvisionerContext adds the given provisioner to the context.
|
||||
func NewProvisionerContext(ctx context.Context, v Provisioner) context.Context {
|
||||
return context.WithValue(ctx, provisionerKey{}, v)
|
||||
}
|
||||
|
||||
// ProvisionerFromContext returns the current provisioner from the given context.
|
||||
func ProvisionerFromContext(ctx context.Context) (v Provisioner, ok bool) {
|
||||
v, ok = ctx.Value(provisionerKey{}).(Provisioner)
|
||||
return
|
||||
}
|
||||
|
||||
// MustLinkerFromContext returns the current provisioner from the given context.
|
||||
// It will panic if it's not in the context.
|
||||
func MustProvisionerFromContext(ctx context.Context) Provisioner {
|
||||
if v, ok := ProvisionerFromContext(ctx); !ok {
|
||||
panic("acme provisioner is not the context")
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// MockProvisioner for testing
|
||||
type MockProvisioner struct {
|
||||
Mret1 interface{}
|
||||
Merr error
|
||||
MgetID func() string
|
||||
MgetName func() string
|
||||
MauthorizeOrderIdentifier func(ctx context.Context, identifier provisioner.ACMEIdentifier) error
|
||||
MauthorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error)
|
||||
MauthorizeRevoke func(ctx context.Context, token string) error
|
||||
MisChallengeEnabled func(ctx context.Context, challenge provisioner.ACMEChallenge) bool
|
||||
MisAttFormatEnabled func(ctx context.Context, format provisioner.ACMEAttestationFormat) bool
|
||||
MgetAttestationRoots func() (*x509.CertPool, bool)
|
||||
MdefaultTLSCertDuration func() time.Duration
|
||||
MgetOptions func() *provisioner.Options
|
||||
}
|
||||
|
||||
// GetName mock
|
||||
func (m *MockProvisioner) GetName() string {
|
||||
if m.MgetName != nil {
|
||||
return m.MgetName()
|
||||
}
|
||||
return m.Mret1.(string)
|
||||
}
|
||||
|
||||
// AuthorizeOrderIdentifiers mock
|
||||
func (m *MockProvisioner) AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error {
|
||||
if m.MauthorizeOrderIdentifier != nil {
|
||||
return m.MauthorizeOrderIdentifier(ctx, identifier)
|
||||
}
|
||||
return m.Merr
|
||||
}
|
||||
|
||||
// AuthorizeSign mock
|
||||
func (m *MockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||
if m.MauthorizeSign != nil {
|
||||
return m.MauthorizeSign(ctx, ott)
|
||||
}
|
||||
return m.Mret1.([]provisioner.SignOption), m.Merr
|
||||
}
|
||||
|
||||
// AuthorizeRevoke mock
|
||||
func (m *MockProvisioner) AuthorizeRevoke(ctx context.Context, token string) error {
|
||||
if m.MauthorizeRevoke != nil {
|
||||
return m.MauthorizeRevoke(ctx, token)
|
||||
}
|
||||
return m.Merr
|
||||
}
|
||||
|
||||
// IsChallengeEnabled mock
|
||||
func (m *MockProvisioner) IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool {
|
||||
if m.MisChallengeEnabled != nil {
|
||||
return m.MisChallengeEnabled(ctx, challenge)
|
||||
}
|
||||
return m.Merr == nil
|
||||
}
|
||||
|
||||
// IsAttestationFormatEnabled mock
|
||||
func (m *MockProvisioner) IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool {
|
||||
if m.MisAttFormatEnabled != nil {
|
||||
return m.MisAttFormatEnabled(ctx, format)
|
||||
}
|
||||
return m.Merr == nil
|
||||
}
|
||||
|
||||
func (m *MockProvisioner) GetAttestationRoots() (*x509.CertPool, bool) {
|
||||
if m.MgetAttestationRoots != nil {
|
||||
return m.MgetAttestationRoots()
|
||||
}
|
||||
return m.Mret1.(*x509.CertPool), m.Mret1 != nil
|
||||
}
|
||||
|
||||
// DefaultTLSCertDuration mock
|
||||
func (m *MockProvisioner) DefaultTLSCertDuration() time.Duration {
|
||||
if m.MdefaultTLSCertDuration != nil {
|
||||
return m.MdefaultTLSCertDuration()
|
||||
}
|
||||
return m.Mret1.(time.Duration)
|
||||
}
|
||||
|
||||
// GetOptions mock
|
||||
func (m *MockProvisioner) GetOptions() *provisioner.Options {
|
||||
if m.MgetOptions != nil {
|
||||
return m.MgetOptions()
|
||||
}
|
||||
return m.Mret1.(*provisioner.Options)
|
||||
}
|
||||
|
||||
// GetID mock
|
||||
func (m *MockProvisioner) GetID() string {
|
||||
if m.MgetID != nil {
|
||||
return m.MgetID()
|
||||
}
|
||||
return m.Mret1.(string)
|
||||
}
|
390
acme/db.go
390
acme/db.go
|
@ -1,390 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrNotFound is an error that should be used by the acme.DB interface to
|
||||
// indicate that an entity does not exist. For example, in the new-account
|
||||
// endpoint, if GetAccountByKeyID returns ErrNotFound we will create the new
|
||||
// account.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// IsErrNotFound returns true if the error is a "not found" error. Returns false
|
||||
// otherwise.
|
||||
func IsErrNotFound(err error) bool {
|
||||
return errors.Is(err, ErrNotFound)
|
||||
}
|
||||
|
||||
// DB is the DB interface expected by the step-ca ACME API.
|
||||
type DB interface {
|
||||
CreateAccount(ctx context.Context, acc *Account) error
|
||||
GetAccount(ctx context.Context, id string) (*Account, error)
|
||||
GetAccountByKeyID(ctx context.Context, kid string) (*Account, error)
|
||||
UpdateAccount(ctx context.Context, acc *Account) error
|
||||
|
||||
CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
||||
GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)
|
||||
GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error)
|
||||
GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
||||
GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error)
|
||||
DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error
|
||||
UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
|
||||
|
||||
CreateNonce(ctx context.Context) (Nonce, error)
|
||||
DeleteNonce(ctx context.Context, nonce Nonce) error
|
||||
|
||||
CreateAuthorization(ctx context.Context, az *Authorization) error
|
||||
GetAuthorization(ctx context.Context, id string) (*Authorization, error)
|
||||
UpdateAuthorization(ctx context.Context, az *Authorization) error
|
||||
GetAuthorizationsByAccountID(ctx context.Context, accountID string) ([]*Authorization, error)
|
||||
|
||||
CreateCertificate(ctx context.Context, cert *Certificate) error
|
||||
GetCertificate(ctx context.Context, id string) (*Certificate, error)
|
||||
GetCertificateBySerial(ctx context.Context, serial string) (*Certificate, error)
|
||||
|
||||
CreateChallenge(ctx context.Context, ch *Challenge) error
|
||||
GetChallenge(ctx context.Context, id, authzID string) (*Challenge, error)
|
||||
UpdateChallenge(ctx context.Context, ch *Challenge) error
|
||||
|
||||
CreateOrder(ctx context.Context, o *Order) error
|
||||
GetOrder(ctx context.Context, id string) (*Order, error)
|
||||
GetOrdersByAccountID(ctx context.Context, accountID string) ([]string, error)
|
||||
UpdateOrder(ctx context.Context, o *Order) error
|
||||
}
|
||||
|
||||
type dbKey struct{}
|
||||
|
||||
// NewDatabaseContext adds the given acme database to the context.
|
||||
func NewDatabaseContext(ctx context.Context, db DB) context.Context {
|
||||
return context.WithValue(ctx, dbKey{}, db)
|
||||
}
|
||||
|
||||
// DatabaseFromContext returns the current acme database from the given context.
|
||||
func DatabaseFromContext(ctx context.Context) (db DB, ok bool) {
|
||||
db, ok = ctx.Value(dbKey{}).(DB)
|
||||
return
|
||||
}
|
||||
|
||||
// MustDatabaseFromContext returns the current database from the given context.
|
||||
// It will panic if it's not in the context.
|
||||
func MustDatabaseFromContext(ctx context.Context) DB {
|
||||
if db, ok := DatabaseFromContext(ctx); !ok {
|
||||
panic("acme database is not in the context")
|
||||
} else {
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
// MockDB is an implementation of the DB interface that should only be used as
|
||||
// a mock in tests.
|
||||
type MockDB struct {
|
||||
MockCreateAccount func(ctx context.Context, acc *Account) error
|
||||
MockGetAccount func(ctx context.Context, id string) (*Account, error)
|
||||
MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error)
|
||||
MockUpdateAccount func(ctx context.Context, acc *Account) error
|
||||
|
||||
MockCreateExternalAccountKey func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
||||
MockGetExternalAccountKey func(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)
|
||||
MockGetExternalAccountKeys func(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error)
|
||||
MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)
|
||||
MockGetExternalAccountKeyByAccountID func(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error)
|
||||
MockDeleteExternalAccountKey func(ctx context.Context, provisionerID, keyID string) error
|
||||
MockUpdateExternalAccountKey func(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error
|
||||
|
||||
MockCreateNonce func(ctx context.Context) (Nonce, error)
|
||||
MockDeleteNonce func(ctx context.Context, nonce Nonce) error
|
||||
|
||||
MockCreateAuthorization func(ctx context.Context, az *Authorization) error
|
||||
MockGetAuthorization func(ctx context.Context, id string) (*Authorization, error)
|
||||
MockUpdateAuthorization func(ctx context.Context, az *Authorization) error
|
||||
MockGetAuthorizationsByAccountID func(ctx context.Context, accountID string) ([]*Authorization, error)
|
||||
|
||||
MockCreateCertificate func(ctx context.Context, cert *Certificate) error
|
||||
MockGetCertificate func(ctx context.Context, id string) (*Certificate, error)
|
||||
MockGetCertificateBySerial func(ctx context.Context, serial string) (*Certificate, error)
|
||||
|
||||
MockCreateChallenge func(ctx context.Context, ch *Challenge) error
|
||||
MockGetChallenge func(ctx context.Context, id, authzID string) (*Challenge, error)
|
||||
MockUpdateChallenge func(ctx context.Context, ch *Challenge) error
|
||||
|
||||
MockCreateOrder func(ctx context.Context, o *Order) error
|
||||
MockGetOrder func(ctx context.Context, id string) (*Order, error)
|
||||
MockGetOrdersByAccountID func(ctx context.Context, accountID string) ([]string, error)
|
||||
MockUpdateOrder func(ctx context.Context, o *Order) error
|
||||
|
||||
MockRet1 interface{}
|
||||
MockError error
|
||||
}
|
||||
|
||||
// CreateAccount mock.
|
||||
func (m *MockDB) CreateAccount(ctx context.Context, acc *Account) error {
|
||||
if m.MockCreateAccount != nil {
|
||||
return m.MockCreateAccount(ctx, acc)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetAccount mock.
|
||||
func (m *MockDB) GetAccount(ctx context.Context, id string) (*Account, error) {
|
||||
if m.MockGetAccount != nil {
|
||||
return m.MockGetAccount(ctx, id)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*Account), m.MockError
|
||||
}
|
||||
|
||||
// GetAccountByKeyID mock
|
||||
func (m *MockDB) GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) {
|
||||
if m.MockGetAccountByKeyID != nil {
|
||||
return m.MockGetAccountByKeyID(ctx, kid)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*Account), m.MockError
|
||||
}
|
||||
|
||||
// UpdateAccount mock
|
||||
func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error {
|
||||
if m.MockUpdateAccount != nil {
|
||||
return m.MockUpdateAccount(ctx, acc)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// CreateExternalAccountKey mock
|
||||
func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) {
|
||||
if m.MockCreateExternalAccountKey != nil {
|
||||
return m.MockCreateExternalAccountKey(ctx, provisionerID, reference)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||
}
|
||||
|
||||
// GetExternalAccountKey mock
|
||||
func (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) {
|
||||
if m.MockGetExternalAccountKey != nil {
|
||||
return m.MockGetExternalAccountKey(ctx, provisionerID, keyID)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||
}
|
||||
|
||||
// GetExternalAccountKeys mock
|
||||
func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) {
|
||||
if m.MockGetExternalAccountKeys != nil {
|
||||
return m.MockGetExternalAccountKeys(ctx, provisionerID, cursor, limit)
|
||||
} else if m.MockError != nil {
|
||||
return nil, "", m.MockError
|
||||
}
|
||||
return m.MockRet1.([]*ExternalAccountKey), "", m.MockError
|
||||
}
|
||||
|
||||
// GetExternalAccountKeyByReference mock
|
||||
func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) {
|
||||
if m.MockGetExternalAccountKeyByReference != nil {
|
||||
return m.MockGetExternalAccountKeyByReference(ctx, provisionerID, reference)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||
}
|
||||
|
||||
// GetExternalAccountKeyByAccountID mock
|
||||
func (m *MockDB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error) {
|
||||
if m.MockGetExternalAccountKeyByAccountID != nil {
|
||||
return m.MockGetExternalAccountKeyByAccountID(ctx, provisionerID, accountID)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*ExternalAccountKey), m.MockError
|
||||
}
|
||||
|
||||
// DeleteExternalAccountKey mock
|
||||
func (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error {
|
||||
if m.MockDeleteExternalAccountKey != nil {
|
||||
return m.MockDeleteExternalAccountKey(ctx, provisionerID, keyID)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// UpdateExternalAccountKey mock
|
||||
func (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error {
|
||||
if m.MockUpdateExternalAccountKey != nil {
|
||||
return m.MockUpdateExternalAccountKey(ctx, provisionerID, eak)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// CreateNonce mock
|
||||
func (m *MockDB) CreateNonce(ctx context.Context) (Nonce, error) {
|
||||
if m.MockCreateNonce != nil {
|
||||
return m.MockCreateNonce(ctx)
|
||||
} else if m.MockError != nil {
|
||||
return Nonce(""), m.MockError
|
||||
}
|
||||
return m.MockRet1.(Nonce), m.MockError
|
||||
}
|
||||
|
||||
// DeleteNonce mock
|
||||
func (m *MockDB) DeleteNonce(ctx context.Context, nonce Nonce) error {
|
||||
if m.MockDeleteNonce != nil {
|
||||
return m.MockDeleteNonce(ctx, nonce)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// CreateAuthorization mock
|
||||
func (m *MockDB) CreateAuthorization(ctx context.Context, az *Authorization) error {
|
||||
if m.MockCreateAuthorization != nil {
|
||||
return m.MockCreateAuthorization(ctx, az)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetAuthorization mock
|
||||
func (m *MockDB) GetAuthorization(ctx context.Context, id string) (*Authorization, error) {
|
||||
if m.MockGetAuthorization != nil {
|
||||
return m.MockGetAuthorization(ctx, id)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*Authorization), m.MockError
|
||||
}
|
||||
|
||||
// UpdateAuthorization mock
|
||||
func (m *MockDB) UpdateAuthorization(ctx context.Context, az *Authorization) error {
|
||||
if m.MockUpdateAuthorization != nil {
|
||||
return m.MockUpdateAuthorization(ctx, az)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetAuthorizationsByAccountID mock
|
||||
func (m *MockDB) GetAuthorizationsByAccountID(ctx context.Context, accountID string) ([]*Authorization, error) {
|
||||
if m.MockGetAuthorizationsByAccountID != nil {
|
||||
return m.MockGetAuthorizationsByAccountID(ctx, accountID)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return nil, m.MockError
|
||||
}
|
||||
|
||||
// CreateCertificate mock
|
||||
func (m *MockDB) CreateCertificate(ctx context.Context, cert *Certificate) error {
|
||||
if m.MockCreateCertificate != nil {
|
||||
return m.MockCreateCertificate(ctx, cert)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetCertificate mock
|
||||
func (m *MockDB) GetCertificate(ctx context.Context, id string) (*Certificate, error) {
|
||||
if m.MockGetCertificate != nil {
|
||||
return m.MockGetCertificate(ctx, id)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*Certificate), m.MockError
|
||||
}
|
||||
|
||||
// GetCertificateBySerial mock
|
||||
func (m *MockDB) GetCertificateBySerial(ctx context.Context, serial string) (*Certificate, error) {
|
||||
if m.MockGetCertificateBySerial != nil {
|
||||
return m.MockGetCertificateBySerial(ctx, serial)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*Certificate), m.MockError
|
||||
}
|
||||
|
||||
// CreateChallenge mock
|
||||
func (m *MockDB) CreateChallenge(ctx context.Context, ch *Challenge) error {
|
||||
if m.MockCreateChallenge != nil {
|
||||
return m.MockCreateChallenge(ctx, ch)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetChallenge mock
|
||||
func (m *MockDB) GetChallenge(ctx context.Context, chID, azID string) (*Challenge, error) {
|
||||
if m.MockGetChallenge != nil {
|
||||
return m.MockGetChallenge(ctx, chID, azID)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*Challenge), m.MockError
|
||||
}
|
||||
|
||||
// UpdateChallenge mock
|
||||
func (m *MockDB) UpdateChallenge(ctx context.Context, ch *Challenge) error {
|
||||
if m.MockUpdateChallenge != nil {
|
||||
return m.MockUpdateChallenge(ctx, ch)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// CreateOrder mock
|
||||
func (m *MockDB) CreateOrder(ctx context.Context, o *Order) error {
|
||||
if m.MockCreateOrder != nil {
|
||||
return m.MockCreateOrder(ctx, o)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetOrder mock
|
||||
func (m *MockDB) GetOrder(ctx context.Context, id string) (*Order, error) {
|
||||
if m.MockGetOrder != nil {
|
||||
return m.MockGetOrder(ctx, id)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*Order), m.MockError
|
||||
}
|
||||
|
||||
// UpdateOrder mock
|
||||
func (m *MockDB) UpdateOrder(ctx context.Context, o *Order) error {
|
||||
if m.MockUpdateOrder != nil {
|
||||
return m.MockUpdateOrder(ctx, o)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetOrdersByAccountID mock
|
||||
func (m *MockDB) GetOrdersByAccountID(ctx context.Context, accID string) ([]string, error) {
|
||||
if m.MockGetOrdersByAccountID != nil {
|
||||
return m.MockGetOrdersByAccountID(ctx, accID)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.([]string), m.MockError
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
nosqlDB "github.com/smallstep/nosql"
|
||||
"go.step.sm/crypto/jose"
|
||||
)
|
||||
|
||||
// dbAccount represents an ACME account.
|
||||
type dbAccount struct {
|
||||
ID string `json:"id"`
|
||||
Key *jose.JSONWebKey `json:"key"`
|
||||
Contact []string `json:"contact,omitempty"`
|
||||
Status acme.Status `json:"status"`
|
||||
LocationPrefix string `json:"locationPrefix"`
|
||||
ProvisionerName string `json:"provisionerName"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeactivatedAt time.Time `json:"deactivatedAt"`
|
||||
}
|
||||
|
||||
func (dba *dbAccount) clone() *dbAccount {
|
||||
nu := *dba
|
||||
return &nu
|
||||
}
|
||||
|
||||
func (db *DB) getAccountIDByKeyID(_ context.Context, kid string) (string, error) {
|
||||
id, err := db.db.Get(accountByKeyIDTable, []byte(kid))
|
||||
if err != nil {
|
||||
if nosqlDB.IsErrNotFound(err) {
|
||||
return "", acme.ErrNotFound
|
||||
}
|
||||
return "", errors.Wrapf(err, "error loading key-account index for key %s", kid)
|
||||
}
|
||||
return string(id), nil
|
||||
}
|
||||
|
||||
// getDBAccount retrieves and unmarshals dbAccount.
|
||||
func (db *DB) getDBAccount(_ context.Context, id string) (*dbAccount, error) {
|
||||
data, err := db.db.Get(accountTable, []byte(id))
|
||||
if err != nil {
|
||||
if nosqlDB.IsErrNotFound(err) {
|
||||
return nil, acme.ErrNotFound
|
||||
}
|
||||
return nil, errors.Wrapf(err, "error loading account %s", id)
|
||||
}
|
||||
|
||||
dbacc := new(dbAccount)
|
||||
if err = json.Unmarshal(data, dbacc); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling account %s into dbAccount", id)
|
||||
}
|
||||
return dbacc, nil
|
||||
}
|
||||
|
||||
// GetAccount retrieves an ACME account by ID.
|
||||
func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error) {
|
||||
dbacc, err := db.getDBAccount(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &acme.Account{
|
||||
Status: dbacc.Status,
|
||||
Contact: dbacc.Contact,
|
||||
Key: dbacc.Key,
|
||||
ID: dbacc.ID,
|
||||
LocationPrefix: dbacc.LocationPrefix,
|
||||
ProvisionerName: dbacc.ProvisionerName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAccountByKeyID retrieves an ACME account by KeyID (thumbprint of the Account Key -- JWK).
|
||||
func (db *DB) GetAccountByKeyID(ctx context.Context, kid string) (*acme.Account, error) {
|
||||
id, err := db.getAccountIDByKeyID(ctx, kid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db.GetAccount(ctx, id)
|
||||
}
|
||||
|
||||
// CreateAccount imlements the AcmeDB.CreateAccount interface.
|
||||
func (db *DB) CreateAccount(ctx context.Context, acc *acme.Account) error {
|
||||
var err error
|
||||
acc.ID, err = randID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dba := &dbAccount{
|
||||
ID: acc.ID,
|
||||
Key: acc.Key,
|
||||
Contact: acc.Contact,
|
||||
Status: acc.Status,
|
||||
CreatedAt: clock.Now(),
|
||||
LocationPrefix: acc.LocationPrefix,
|
||||
ProvisionerName: acc.ProvisionerName,
|
||||
}
|
||||
|
||||
kid, err := acme.KeyToID(dba.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kidB := []byte(kid)
|
||||
|
||||
// Set the jwkID -> acme account ID index
|
||||
_, swapped, err := db.db.CmpAndSwap(accountByKeyIDTable, kidB, nil, []byte(acc.ID))
|
||||
switch {
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error storing keyID to accountID index")
|
||||
case !swapped:
|
||||
return errors.Errorf("key-id to account-id index already exists")
|
||||
default:
|
||||
if err = db.save(ctx, acc.ID, dba, nil, "account", accountTable); err != nil {
|
||||
db.db.Del(accountByKeyIDTable, kidB)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateAccount imlements the AcmeDB.UpdateAccount interface.
|
||||
func (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error {
|
||||
old, err := db.getDBAccount(ctx, acc.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nu := old.clone()
|
||||
nu.Contact = acc.Contact
|
||||
nu.Status = acc.Status
|
||||
|
||||
// If the status has changed to 'deactivated', then set deactivatedAt timestamp.
|
||||
if acc.Status == acme.StatusDeactivated && old.Status != acme.StatusDeactivated {
|
||||
nu.DeactivatedAt = clock.Now()
|
||||
}
|
||||
|
||||
return db.save(ctx, old.ID, nu, old, "account", accountTable)
|
||||
}
|
|
@ -1,715 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
nosqldb "github.com/smallstep/nosql/database"
|
||||
"go.step.sm/crypto/jose"
|
||||
)
|
||||
|
||||
func TestDB_getDBAccount(t *testing.T) {
|
||||
accID := "accID"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
dbacc *dbAccount
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/not-found": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, string(key), accID)
|
||||
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
err: acme.ErrNotFound,
|
||||
}
|
||||
},
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, string(key), accID)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading account accID: force"),
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, string(key), accID)
|
||||
|
||||
return []byte("foo"), nil
|
||||
},
|
||||
},
|
||||
err: errors.New("error unmarshaling account accID into dbAccount"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
dbacc := &dbAccount{
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
CreatedAt: now,
|
||||
DeactivatedAt: now,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
}
|
||||
b, err := json.Marshal(dbacc)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, string(key), accID)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
},
|
||||
dbacc: dbacc,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if dbacc, err := d.getDBAccount(context.Background(), accID); err != nil {
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, dbacc.ID, tc.dbacc.ID)
|
||||
assert.Equals(t, dbacc.Status, tc.dbacc.Status)
|
||||
assert.Equals(t, dbacc.CreatedAt, tc.dbacc.CreatedAt)
|
||||
assert.Equals(t, dbacc.DeactivatedAt, tc.dbacc.DeactivatedAt)
|
||||
assert.Equals(t, dbacc.Contact, tc.dbacc.Contact)
|
||||
assert.Equals(t, dbacc.Key.KeyID, tc.dbacc.Key.KeyID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_getAccountIDByKeyID(t *testing.T) {
|
||||
accID := "accID"
|
||||
kid := "kid"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/not-found": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountByKeyIDTable)
|
||||
assert.Equals(t, string(key), kid)
|
||||
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
err: acme.ErrNotFound,
|
||||
}
|
||||
},
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountByKeyIDTable)
|
||||
assert.Equals(t, string(key), kid)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading key-account index for key kid: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountByKeyIDTable)
|
||||
assert.Equals(t, string(key), kid)
|
||||
|
||||
return []byte(accID), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if retAccID, err := d.getAccountIDByKeyID(context.Background(), kid); err != nil {
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, retAccID, accID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_GetAccount(t *testing.T) {
|
||||
accID := "accID"
|
||||
locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/"
|
||||
provisionerName := "foo"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
dbacc *dbAccount
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, string(key), accID)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading account accID: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
dbacc := &dbAccount{
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
CreatedAt: now,
|
||||
DeactivatedAt: now,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
LocationPrefix: locationPrefix,
|
||||
ProvisionerName: provisionerName,
|
||||
}
|
||||
b, err := json.Marshal(dbacc)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, string(key), accID)
|
||||
return b, nil
|
||||
},
|
||||
},
|
||||
dbacc: dbacc,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if acc, err := d.GetAccount(context.Background(), accID); err != nil {
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, acc.ID, tc.dbacc.ID)
|
||||
assert.Equals(t, acc.Status, tc.dbacc.Status)
|
||||
assert.Equals(t, acc.Contact, tc.dbacc.Contact)
|
||||
assert.Equals(t, acc.LocationPrefix, tc.dbacc.LocationPrefix)
|
||||
assert.Equals(t, acc.ProvisionerName, tc.dbacc.ProvisionerName)
|
||||
assert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_GetAccountByKeyID(t *testing.T) {
|
||||
accID := "accID"
|
||||
kid := "kid"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
dbacc *dbAccount
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.getAccountIDByKeyID-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, string(bucket), string(accountByKeyIDTable))
|
||||
assert.Equals(t, string(key), kid)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading key-account index for key kid: force"),
|
||||
}
|
||||
},
|
||||
"fail/db.GetAccount-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
switch string(bucket) {
|
||||
case string(accountByKeyIDTable):
|
||||
assert.Equals(t, string(key), kid)
|
||||
return []byte(accID), nil
|
||||
case string(accountTable):
|
||||
assert.Equals(t, string(key), accID)
|
||||
return nil, errors.New("force")
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading account accID: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
dbacc := &dbAccount{
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
CreatedAt: now,
|
||||
DeactivatedAt: now,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
}
|
||||
b, err := json.Marshal(dbacc)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
switch string(bucket) {
|
||||
case string(accountByKeyIDTable):
|
||||
assert.Equals(t, string(key), kid)
|
||||
return []byte(accID), nil
|
||||
case string(accountTable):
|
||||
assert.Equals(t, string(key), accID)
|
||||
return b, nil
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
},
|
||||
dbacc: dbacc,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if acc, err := d.GetAccountByKeyID(context.Background(), kid); err != nil {
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, acc.ID, tc.dbacc.ID)
|
||||
assert.Equals(t, acc.Status, tc.dbacc.Status)
|
||||
assert.Equals(t, acc.Contact, tc.dbacc.Contact)
|
||||
assert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_CreateAccount(t *testing.T) {
|
||||
locationPrefix := "https://test.ca.smallstep.com/acme/foo/account/"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
acc *acme.Account
|
||||
err error
|
||||
_id *string
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/keyID-cmpAndSwap-error": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
acc := &acme.Account{
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
LocationPrefix: locationPrefix,
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, accountByKeyIDTable)
|
||||
assert.Equals(t, string(key), jwk.KeyID)
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
assert.Equals(t, nu, []byte(acc.ID))
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
},
|
||||
acc: acc,
|
||||
err: errors.New("error storing keyID to accountID index: force"),
|
||||
}
|
||||
},
|
||||
"fail/keyID-cmpAndSwap-false": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
acc := &acme.Account{
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
LocationPrefix: locationPrefix,
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, accountByKeyIDTable)
|
||||
assert.Equals(t, string(key), jwk.KeyID)
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
assert.Equals(t, nu, []byte(acc.ID))
|
||||
return nil, false, nil
|
||||
},
|
||||
},
|
||||
acc: acc,
|
||||
err: errors.New("key-id to account-id index already exists"),
|
||||
}
|
||||
},
|
||||
"fail/account-save-error": func(t *testing.T) test {
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
acc := &acme.Account{
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
LocationPrefix: locationPrefix,
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
switch string(bucket) {
|
||||
case string(accountByKeyIDTable):
|
||||
assert.Equals(t, string(key), jwk.KeyID)
|
||||
assert.Equals(t, old, nil)
|
||||
return nu, true, nil
|
||||
case string(accountTable):
|
||||
assert.Equals(t, string(key), acc.ID)
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbacc := new(dbAccount)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbacc))
|
||||
assert.Equals(t, dbacc.ID, string(key))
|
||||
assert.Equals(t, dbacc.Contact, acc.Contact)
|
||||
assert.Equals(t, dbacc.LocationPrefix, acc.LocationPrefix)
|
||||
assert.Equals(t, dbacc.ProvisionerName, acc.ProvisionerName)
|
||||
assert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID)
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt))
|
||||
assert.True(t, dbacc.DeactivatedAt.IsZero())
|
||||
return nil, false, errors.New("force")
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
|
||||
return nil, false, errors.New("force")
|
||||
}
|
||||
},
|
||||
},
|
||||
acc: acc,
|
||||
err: errors.New("error saving acme account: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
var (
|
||||
id string
|
||||
idPtr = &id
|
||||
)
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
acc := &acme.Account{
|
||||
Status: acme.StatusValid,
|
||||
Contact: []string{"foo", "bar"},
|
||||
Key: jwk,
|
||||
LocationPrefix: locationPrefix,
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
id = string(key)
|
||||
switch string(bucket) {
|
||||
case string(accountByKeyIDTable):
|
||||
assert.Equals(t, string(key), jwk.KeyID)
|
||||
assert.Equals(t, old, nil)
|
||||
return nu, true, nil
|
||||
case string(accountTable):
|
||||
assert.Equals(t, string(key), acc.ID)
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbacc := new(dbAccount)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbacc))
|
||||
assert.Equals(t, dbacc.ID, string(key))
|
||||
assert.Equals(t, dbacc.Contact, acc.Contact)
|
||||
assert.Equals(t, dbacc.LocationPrefix, acc.LocationPrefix)
|
||||
assert.Equals(t, dbacc.ProvisionerName, acc.ProvisionerName)
|
||||
assert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID)
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt))
|
||||
assert.True(t, dbacc.DeactivatedAt.IsZero())
|
||||
return nu, true, nil
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket %s", string(bucket)))
|
||||
return nil, false, errors.New("force")
|
||||
}
|
||||
},
|
||||
},
|
||||
acc: acc,
|
||||
_id: idPtr,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.CreateAccount(context.Background(), tc.acc); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, tc.acc.ID, *tc._id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_UpdateAccount(t *testing.T) {
|
||||
accID := "accID"
|
||||
now := clock.Now()
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
dbacc := &dbAccount{
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
CreatedAt: now,
|
||||
DeactivatedAt: now,
|
||||
Contact: []string{"foo", "bar"},
|
||||
LocationPrefix: "foo",
|
||||
ProvisionerName: "alpha",
|
||||
Key: jwk,
|
||||
}
|
||||
b, err := json.Marshal(dbacc)
|
||||
assert.FatalError(t, err)
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
acc *acme.Account
|
||||
err error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
acc: &acme.Account{
|
||||
ID: accID,
|
||||
},
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, string(key), accID)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading account accID: force"),
|
||||
}
|
||||
},
|
||||
"fail/already-deactivated": func(t *testing.T) test {
|
||||
clone := dbacc.clone()
|
||||
clone.Status = acme.StatusDeactivated
|
||||
clone.DeactivatedAt = now
|
||||
dbaccb, err := json.Marshal(clone)
|
||||
assert.FatalError(t, err)
|
||||
acc := &acme.Account{
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
Contact: []string{"foo", "bar"},
|
||||
}
|
||||
return test{
|
||||
acc: acc,
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, string(key), accID)
|
||||
|
||||
return dbaccb, nil
|
||||
},
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, old, b)
|
||||
|
||||
dbNew := new(dbAccount)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbNew))
|
||||
assert.Equals(t, dbNew.ID, clone.ID)
|
||||
assert.Equals(t, dbNew.Status, clone.Status)
|
||||
assert.Equals(t, dbNew.Contact, clone.Contact)
|
||||
assert.Equals(t, dbNew.Key.KeyID, clone.Key.KeyID)
|
||||
assert.Equals(t, dbNew.CreatedAt, clone.CreatedAt)
|
||||
assert.Equals(t, dbNew.DeactivatedAt, clone.DeactivatedAt)
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error saving acme account: force"),
|
||||
}
|
||||
},
|
||||
"fail/db.CmpAndSwap-error": func(t *testing.T) test {
|
||||
acc := &acme.Account{
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
Contact: []string{"foo", "bar"},
|
||||
}
|
||||
return test{
|
||||
acc: acc,
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, string(key), accID)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, old, b)
|
||||
|
||||
dbNew := new(dbAccount)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbNew))
|
||||
assert.Equals(t, dbNew.ID, dbacc.ID)
|
||||
assert.Equals(t, dbNew.Status, acc.Status)
|
||||
assert.Equals(t, dbNew.Contact, dbacc.Contact)
|
||||
assert.Equals(t, dbNew.Key.KeyID, dbacc.Key.KeyID)
|
||||
assert.Equals(t, dbNew.CreatedAt, dbacc.CreatedAt)
|
||||
assert.True(t, dbNew.DeactivatedAt.Add(-time.Minute).Before(now))
|
||||
assert.True(t, dbNew.DeactivatedAt.Add(time.Minute).After(now))
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error saving acme account: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
acc := &acme.Account{
|
||||
ID: accID,
|
||||
Status: acme.StatusDeactivated,
|
||||
Contact: []string{"baz", "zap"},
|
||||
LocationPrefix: "bar",
|
||||
ProvisionerName: "beta",
|
||||
Key: jwk,
|
||||
}
|
||||
return test{
|
||||
acc: acc,
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, string(key), accID)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, accountTable)
|
||||
assert.Equals(t, old, b)
|
||||
|
||||
dbNew := new(dbAccount)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbNew))
|
||||
assert.Equals(t, dbNew.ID, dbacc.ID)
|
||||
assert.Equals(t, dbNew.Status, acc.Status)
|
||||
assert.Equals(t, dbNew.Contact, acc.Contact)
|
||||
// LocationPrefix should not change.
|
||||
assert.Equals(t, dbNew.LocationPrefix, dbacc.LocationPrefix)
|
||||
assert.Equals(t, dbNew.ProvisionerName, dbacc.ProvisionerName)
|
||||
assert.Equals(t, dbNew.Key.KeyID, dbacc.Key.KeyID)
|
||||
assert.Equals(t, dbNew.CreatedAt, dbacc.CreatedAt)
|
||||
assert.True(t, dbNew.DeactivatedAt.Add(-time.Minute).Before(now))
|
||||
assert.True(t, dbNew.DeactivatedAt.Add(time.Minute).After(now))
|
||||
return nu, true, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.UpdateAccount(context.Background(), tc.acc); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
// dbAuthz is the base authz type that others build from.
|
||||
type dbAuthz struct {
|
||||
ID string `json:"id"`
|
||||
AccountID string `json:"accountID"`
|
||||
Identifier acme.Identifier `json:"identifier"`
|
||||
Status acme.Status `json:"status"`
|
||||
Token string `json:"token"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
ChallengeIDs []string `json:"challengeIDs"`
|
||||
Wildcard bool `json:"wildcard"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
Error *acme.Error `json:"error"`
|
||||
}
|
||||
|
||||
func (ba *dbAuthz) clone() *dbAuthz {
|
||||
u := *ba
|
||||
return &u
|
||||
}
|
||||
|
||||
// getDBAuthz retrieves and unmarshals a database representation of the
|
||||
// ACME Authorization type.
|
||||
func (db *DB) getDBAuthz(_ context.Context, id string) (*dbAuthz, error) {
|
||||
data, err := db.db.Get(authzTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "authz %s not found", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading authz %s", id)
|
||||
}
|
||||
|
||||
var dbaz dbAuthz
|
||||
if err = json.Unmarshal(data, &dbaz); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling authz %s into dbAuthz", id)
|
||||
}
|
||||
return &dbaz, nil
|
||||
}
|
||||
|
||||
// GetAuthorization retrieves and unmarshals an ACME authz type from the database.
|
||||
// Implements acme.DB GetAuthorization interface.
|
||||
func (db *DB) GetAuthorization(ctx context.Context, id string) (*acme.Authorization, error) {
|
||||
dbaz, err := db.getDBAuthz(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var chs = make([]*acme.Challenge, len(dbaz.ChallengeIDs))
|
||||
for i, chID := range dbaz.ChallengeIDs {
|
||||
chs[i], err = db.GetChallenge(ctx, chID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &acme.Authorization{
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: chs,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Token: dbaz.Token,
|
||||
Fingerprint: dbaz.Fingerprint,
|
||||
Error: dbaz.Error,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateAuthorization creates an entry in the database for the Authorization.
|
||||
// Implements the acme.DB.CreateAuthorization interface.
|
||||
func (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) error {
|
||||
var err error
|
||||
az.ID, err = randID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chIDs := make([]string, len(az.Challenges))
|
||||
for i, ch := range az.Challenges {
|
||||
chIDs[i] = ch.ID
|
||||
}
|
||||
|
||||
now := clock.Now()
|
||||
dbaz := &dbAuthz{
|
||||
ID: az.ID,
|
||||
AccountID: az.AccountID,
|
||||
Status: az.Status,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: az.ExpiresAt,
|
||||
Identifier: az.Identifier,
|
||||
ChallengeIDs: chIDs,
|
||||
Token: az.Token,
|
||||
Fingerprint: az.Fingerprint,
|
||||
Wildcard: az.Wildcard,
|
||||
}
|
||||
|
||||
return db.save(ctx, az.ID, dbaz, nil, "authz", authzTable)
|
||||
}
|
||||
|
||||
// UpdateAuthorization saves an updated ACME Authorization to the database.
|
||||
func (db *DB) UpdateAuthorization(ctx context.Context, az *acme.Authorization) error {
|
||||
old, err := db.getDBAuthz(ctx, az.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nu := old.clone()
|
||||
nu.Status = az.Status
|
||||
nu.Fingerprint = az.Fingerprint
|
||||
nu.Error = az.Error
|
||||
return db.save(ctx, old.ID, nu, old, "authz", authzTable)
|
||||
}
|
||||
|
||||
// GetAuthorizationsByAccountID retrieves and unmarshals ACME authz types from the database.
|
||||
func (db *DB) GetAuthorizationsByAccountID(_ context.Context, accountID string) ([]*acme.Authorization, error) {
|
||||
entries, err := db.db.List(authzTable)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error listing authz")
|
||||
}
|
||||
authzs := []*acme.Authorization{}
|
||||
for _, entry := range entries {
|
||||
dbaz := new(dbAuthz)
|
||||
if err = json.Unmarshal(entry.Value, dbaz); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling dbAuthz key '%s' into dbAuthz struct", string(entry.Key))
|
||||
}
|
||||
// Filter out all dbAuthzs that don't belong to the accountID. This
|
||||
// could be made more efficient with additional data structures mapping the
|
||||
// Account ID to authorizations. Not trivial to do, though.
|
||||
if dbaz.AccountID != accountID {
|
||||
continue
|
||||
}
|
||||
authzs = append(authzs, &acme.Authorization{
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: nil, // challenges not required for current use case
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Token: dbaz.Token,
|
||||
Fingerprint: dbaz.Fingerprint,
|
||||
Error: dbaz.Error,
|
||||
})
|
||||
}
|
||||
|
||||
return authzs, nil
|
||||
}
|
|
@ -1,772 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
nosqldb "github.com/smallstep/nosql/database"
|
||||
)
|
||||
|
||||
func TestDB_getDBAuthz(t *testing.T) {
|
||||
azID := "azID"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
dbaz *dbAuthz
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/not-found": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), azID)
|
||||
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
acmeErr: acme.NewError(acme.ErrorMalformedType, "authz azID not found"),
|
||||
}
|
||||
},
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), azID)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading authz azID: force"),
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), azID)
|
||||
|
||||
return []byte("foo"), nil
|
||||
},
|
||||
},
|
||||
err: errors.New("error unmarshaling authz azID into dbAuthz"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
dbaz := &dbAuthz{
|
||||
ID: azID,
|
||||
AccountID: "accountID",
|
||||
Identifier: acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
},
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
Error: acme.NewErrorISE("The server experienced an internal error"),
|
||||
ChallengeIDs: []string{"foo", "bar"},
|
||||
Wildcard: true,
|
||||
}
|
||||
b, err := json.Marshal(dbaz)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), azID)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
},
|
||||
dbaz: dbaz,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if dbaz, err := d.getDBAuthz(context.Background(), azID); err != nil {
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, dbaz.ID, tc.dbaz.ID)
|
||||
assert.Equals(t, dbaz.AccountID, tc.dbaz.AccountID)
|
||||
assert.Equals(t, dbaz.Identifier, tc.dbaz.Identifier)
|
||||
assert.Equals(t, dbaz.Status, tc.dbaz.Status)
|
||||
assert.Equals(t, dbaz.Token, tc.dbaz.Token)
|
||||
assert.Equals(t, dbaz.CreatedAt, tc.dbaz.CreatedAt)
|
||||
assert.Equals(t, dbaz.ExpiresAt, tc.dbaz.ExpiresAt)
|
||||
assert.Equals(t, dbaz.Error.Error(), tc.dbaz.Error.Error())
|
||||
assert.Equals(t, dbaz.Wildcard, tc.dbaz.Wildcard)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_GetAuthorization(t *testing.T) {
|
||||
azID := "azID"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
dbaz *dbAuthz
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), azID)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading authz azID: force"),
|
||||
}
|
||||
},
|
||||
"fail/forward-acme-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), azID)
|
||||
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
acmeErr: acme.NewError(acme.ErrorMalformedType, "authz azID not found"),
|
||||
}
|
||||
},
|
||||
"fail/db.GetChallenge-error": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
dbaz := &dbAuthz{
|
||||
ID: azID,
|
||||
AccountID: "accountID",
|
||||
Identifier: acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
},
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
Error: acme.NewErrorISE("force"),
|
||||
ChallengeIDs: []string{"foo", "bar"},
|
||||
Wildcard: true,
|
||||
}
|
||||
b, err := json.Marshal(dbaz)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
switch string(bucket) {
|
||||
case string(authzTable):
|
||||
assert.Equals(t, string(key), azID)
|
||||
return b, nil
|
||||
case string(challengeTable):
|
||||
assert.Equals(t, string(key), "foo")
|
||||
return nil, errors.New("force")
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket '%s'", string(bucket)))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading acme challenge foo: force"),
|
||||
}
|
||||
},
|
||||
"fail/db.GetChallenge-not-found": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
dbaz := &dbAuthz{
|
||||
ID: azID,
|
||||
AccountID: "accountID",
|
||||
Identifier: acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
},
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
Error: acme.NewErrorISE("force"),
|
||||
ChallengeIDs: []string{"foo", "bar"},
|
||||
Wildcard: true,
|
||||
}
|
||||
b, err := json.Marshal(dbaz)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
switch string(bucket) {
|
||||
case string(authzTable):
|
||||
assert.Equals(t, string(key), azID)
|
||||
return b, nil
|
||||
case string(challengeTable):
|
||||
assert.Equals(t, string(key), "foo")
|
||||
return nil, nosqldb.ErrNotFound
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket '%s'", string(bucket)))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
},
|
||||
acmeErr: acme.NewError(acme.ErrorMalformedType, "challenge foo not found"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
dbaz := &dbAuthz{
|
||||
ID: azID,
|
||||
AccountID: "accountID",
|
||||
Identifier: acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
},
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
Error: acme.NewErrorISE("The server experienced an internal error"),
|
||||
ChallengeIDs: []string{"foo", "bar"},
|
||||
Wildcard: true,
|
||||
}
|
||||
b, err := json.Marshal(dbaz)
|
||||
assert.FatalError(t, err)
|
||||
chCount := 0
|
||||
fooChb, err := json.Marshal(&dbChallenge{ID: "foo"})
|
||||
assert.FatalError(t, err)
|
||||
barChb, err := json.Marshal(&dbChallenge{ID: "bar"})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
switch string(bucket) {
|
||||
case string(authzTable):
|
||||
assert.Equals(t, string(key), azID)
|
||||
return b, nil
|
||||
case string(challengeTable):
|
||||
if chCount == 0 {
|
||||
chCount++
|
||||
assert.Equals(t, string(key), "foo")
|
||||
return fooChb, nil
|
||||
}
|
||||
assert.Equals(t, string(key), "bar")
|
||||
return barChb, nil
|
||||
default:
|
||||
assert.FatalError(t, errors.Errorf("unexpected bucket '%s'", string(bucket)))
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
},
|
||||
},
|
||||
dbaz: dbaz,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if az, err := d.GetAuthorization(context.Background(), azID); err != nil {
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, az.ID, tc.dbaz.ID)
|
||||
assert.Equals(t, az.AccountID, tc.dbaz.AccountID)
|
||||
assert.Equals(t, az.Identifier, tc.dbaz.Identifier)
|
||||
assert.Equals(t, az.Status, tc.dbaz.Status)
|
||||
assert.Equals(t, az.Token, tc.dbaz.Token)
|
||||
assert.Equals(t, az.Wildcard, tc.dbaz.Wildcard)
|
||||
assert.Equals(t, az.ExpiresAt, tc.dbaz.ExpiresAt)
|
||||
assert.Equals(t, az.Challenges, []*acme.Challenge{
|
||||
{ID: "foo"},
|
||||
{ID: "bar"},
|
||||
})
|
||||
assert.Equals(t, az.Error.Error(), tc.dbaz.Error.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_CreateAuthorization(t *testing.T) {
|
||||
azID := "azID"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
az *acme.Authorization
|
||||
err error
|
||||
_id *string
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/cmpAndSwap-error": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
az := &acme.Authorization{
|
||||
ID: azID,
|
||||
AccountID: "accountID",
|
||||
Identifier: acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
},
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
Challenges: []*acme.Challenge{
|
||||
{ID: "foo"},
|
||||
{ID: "bar"},
|
||||
},
|
||||
Wildcard: true,
|
||||
Error: acme.NewErrorISE("force"),
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), az.ID)
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbaz := new(dbAuthz)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbaz))
|
||||
assert.Equals(t, dbaz.ID, string(key))
|
||||
assert.Equals(t, dbaz.AccountID, az.AccountID)
|
||||
assert.Equals(t, dbaz.Identifier, acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
})
|
||||
assert.Equals(t, dbaz.Status, az.Status)
|
||||
assert.Equals(t, dbaz.Token, az.Token)
|
||||
assert.Equals(t, dbaz.ChallengeIDs, []string{"foo", "bar"})
|
||||
assert.Equals(t, dbaz.Wildcard, az.Wildcard)
|
||||
assert.Equals(t, dbaz.ExpiresAt, az.ExpiresAt)
|
||||
assert.Nil(t, dbaz.Error)
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbaz.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbaz.CreatedAt))
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
},
|
||||
az: az,
|
||||
err: errors.New("error saving acme authz: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
var (
|
||||
id string
|
||||
idPtr = &id
|
||||
now = clock.Now()
|
||||
az = &acme.Authorization{
|
||||
ID: azID,
|
||||
AccountID: "accountID",
|
||||
Identifier: acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
},
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
Challenges: []*acme.Challenge{
|
||||
{ID: "foo"},
|
||||
{ID: "bar"},
|
||||
},
|
||||
Wildcard: true,
|
||||
Error: acme.NewErrorISE("force"),
|
||||
}
|
||||
)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
*idPtr = string(key)
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), az.ID)
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbaz := new(dbAuthz)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbaz))
|
||||
assert.Equals(t, dbaz.ID, string(key))
|
||||
assert.Equals(t, dbaz.AccountID, az.AccountID)
|
||||
assert.Equals(t, dbaz.Identifier, acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
})
|
||||
assert.Equals(t, dbaz.Status, az.Status)
|
||||
assert.Equals(t, dbaz.Token, az.Token)
|
||||
assert.Equals(t, dbaz.ChallengeIDs, []string{"foo", "bar"})
|
||||
assert.Equals(t, dbaz.Wildcard, az.Wildcard)
|
||||
assert.Equals(t, dbaz.ExpiresAt, az.ExpiresAt)
|
||||
assert.Nil(t, dbaz.Error)
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbaz.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbaz.CreatedAt))
|
||||
return nu, true, nil
|
||||
},
|
||||
},
|
||||
az: az,
|
||||
_id: idPtr,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.CreateAuthorization(context.Background(), tc.az); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, tc.az.ID, *tc._id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_UpdateAuthorization(t *testing.T) {
|
||||
azID := "azID"
|
||||
now := clock.Now()
|
||||
dbaz := &dbAuthz{
|
||||
ID: azID,
|
||||
AccountID: "accountID",
|
||||
Identifier: acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
},
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
ChallengeIDs: []string{"foo", "bar"},
|
||||
Wildcard: true,
|
||||
Fingerprint: "fingerprint",
|
||||
}
|
||||
b, err := json.Marshal(dbaz)
|
||||
assert.FatalError(t, err)
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
az *acme.Authorization
|
||||
err error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
az: &acme.Authorization{
|
||||
ID: azID,
|
||||
},
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), azID)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading authz azID: force"),
|
||||
}
|
||||
},
|
||||
"fail/db.CmpAndSwap-error": func(t *testing.T) test {
|
||||
updAz := &acme.Authorization{
|
||||
ID: azID,
|
||||
Status: acme.StatusValid,
|
||||
Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
|
||||
}
|
||||
return test{
|
||||
az: updAz,
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), azID)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, old, b)
|
||||
|
||||
dbOld := new(dbAuthz)
|
||||
assert.FatalError(t, json.Unmarshal(old, dbOld))
|
||||
assert.Equals(t, dbaz, dbOld)
|
||||
|
||||
dbNew := new(dbAuthz)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbNew))
|
||||
assert.Equals(t, dbNew.ID, dbaz.ID)
|
||||
assert.Equals(t, dbNew.AccountID, dbaz.AccountID)
|
||||
assert.Equals(t, dbNew.Identifier, dbaz.Identifier)
|
||||
assert.Equals(t, dbNew.Status, acme.StatusValid)
|
||||
assert.Equals(t, dbNew.Token, dbaz.Token)
|
||||
assert.Equals(t, dbNew.ChallengeIDs, dbaz.ChallengeIDs)
|
||||
assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard)
|
||||
assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt)
|
||||
assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt)
|
||||
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error saving acme authz: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
updAz := &acme.Authorization{
|
||||
ID: azID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Status: acme.StatusValid,
|
||||
Identifier: dbaz.Identifier,
|
||||
Challenges: []*acme.Challenge{
|
||||
{ID: "foo"},
|
||||
{ID: "bar"},
|
||||
},
|
||||
Token: dbaz.Token,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Fingerprint: "fingerprint",
|
||||
Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
|
||||
}
|
||||
return test{
|
||||
az: updAz,
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, string(key), azID)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
assert.Equals(t, old, b)
|
||||
|
||||
dbOld := new(dbAuthz)
|
||||
assert.FatalError(t, json.Unmarshal(old, dbOld))
|
||||
assert.Equals(t, dbaz, dbOld)
|
||||
|
||||
dbNew := new(dbAuthz)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbNew))
|
||||
assert.Equals(t, dbNew.ID, dbaz.ID)
|
||||
assert.Equals(t, dbNew.AccountID, dbaz.AccountID)
|
||||
assert.Equals(t, dbNew.Identifier, dbaz.Identifier)
|
||||
assert.Equals(t, dbNew.Status, acme.StatusValid)
|
||||
assert.Equals(t, dbNew.Token, dbaz.Token)
|
||||
assert.Equals(t, dbNew.ChallengeIDs, dbaz.ChallengeIDs)
|
||||
assert.Equals(t, dbNew.Wildcard, dbaz.Wildcard)
|
||||
assert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt)
|
||||
assert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt)
|
||||
assert.Equals(t, dbNew.Fingerprint, dbaz.Fingerprint)
|
||||
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
|
||||
return nu, true, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.UpdateAuthorization(context.Background(), tc.az); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, tc.az.ID, dbaz.ID)
|
||||
assert.Equals(t, tc.az.AccountID, dbaz.AccountID)
|
||||
assert.Equals(t, tc.az.Identifier, dbaz.Identifier)
|
||||
assert.Equals(t, tc.az.Status, acme.StatusValid)
|
||||
assert.Equals(t, tc.az.Wildcard, dbaz.Wildcard)
|
||||
assert.Equals(t, tc.az.Token, dbaz.Token)
|
||||
assert.Equals(t, tc.az.ExpiresAt, dbaz.ExpiresAt)
|
||||
assert.Equals(t, tc.az.Challenges, []*acme.Challenge{
|
||||
{ID: "foo"},
|
||||
{ID: "bar"},
|
||||
})
|
||||
assert.Equals(t, tc.az.Error.Error(), acme.NewError(acme.ErrorMalformedType, "malformed").Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_GetAuthorizationsByAccountID(t *testing.T) {
|
||||
azID := "azID"
|
||||
accountID := "accountID"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
authzs []*acme.Authorization
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.List-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error listing authz: force"),
|
||||
}
|
||||
},
|
||||
"fail/unmarshal": func(t *testing.T) test {
|
||||
b := []byte(`{malformed}`)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
return []*nosqldb.Entry{
|
||||
{
|
||||
Bucket: bucket,
|
||||
Key: []byte(azID),
|
||||
Value: b,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
authzs: nil,
|
||||
err: fmt.Errorf("error unmarshaling dbAuthz key '%s' into dbAuthz struct", azID),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
dbaz := &dbAuthz{
|
||||
ID: azID,
|
||||
AccountID: accountID,
|
||||
Identifier: acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
},
|
||||
Status: acme.StatusValid,
|
||||
Token: "token",
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
ChallengeIDs: []string{"foo", "bar"},
|
||||
Wildcard: true,
|
||||
}
|
||||
b, err := json.Marshal(dbaz)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
return []*nosqldb.Entry{
|
||||
{
|
||||
Bucket: bucket,
|
||||
Key: []byte(azID),
|
||||
Value: b,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
authzs: []*acme.Authorization{
|
||||
{
|
||||
ID: dbaz.ID,
|
||||
AccountID: dbaz.AccountID,
|
||||
Token: dbaz.Token,
|
||||
Identifier: dbaz.Identifier,
|
||||
Status: dbaz.Status,
|
||||
Challenges: nil,
|
||||
Wildcard: dbaz.Wildcard,
|
||||
ExpiresAt: dbaz.ExpiresAt,
|
||||
Error: dbaz.Error,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/skip-different-account": func(t *testing.T) test {
|
||||
now := clock.Now()
|
||||
dbaz := &dbAuthz{
|
||||
ID: azID,
|
||||
AccountID: "differentAccountID",
|
||||
Identifier: acme.Identifier{
|
||||
Type: "dns",
|
||||
Value: "test.ca.smallstep.com",
|
||||
},
|
||||
Status: acme.StatusValid,
|
||||
Token: "token",
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(5 * time.Minute),
|
||||
ChallengeIDs: []string{"foo", "bar"},
|
||||
Wildcard: true,
|
||||
}
|
||||
b, err := json.Marshal(dbaz)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MList: func(bucket []byte) ([]*nosqldb.Entry, error) {
|
||||
assert.Equals(t, bucket, authzTable)
|
||||
return []*nosqldb.Entry{
|
||||
{
|
||||
Bucket: bucket,
|
||||
Key: []byte(azID),
|
||||
Value: b,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
authzs: []*acme.Authorization{},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if azs, err := d.GetAuthorizationsByAccountID(context.Background(), accountID); err != nil {
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
if !cmp.Equal(azs, tc.authzs) {
|
||||
t.Errorf("db.GetAuthorizationsByAccountID() diff =\n%s", cmp.Diff(azs, tc.authzs))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
type dbCert struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
AccountID string `json:"accountID"`
|
||||
OrderID string `json:"orderID"`
|
||||
Leaf []byte `json:"leaf"`
|
||||
Intermediates []byte `json:"intermediates"`
|
||||
}
|
||||
|
||||
type dbSerial struct {
|
||||
Serial string `json:"serial"`
|
||||
CertificateID string `json:"certificateID"`
|
||||
}
|
||||
|
||||
// CreateCertificate creates and stores an ACME certificate type.
|
||||
func (db *DB) CreateCertificate(ctx context.Context, cert *acme.Certificate) error {
|
||||
var err error
|
||||
cert.ID, err = randID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
leaf := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Leaf.Raw,
|
||||
})
|
||||
var intermediates []byte
|
||||
for _, cert := range cert.Intermediates {
|
||||
intermediates = append(intermediates, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
})...)
|
||||
}
|
||||
|
||||
dbch := &dbCert{
|
||||
ID: cert.ID,
|
||||
AccountID: cert.AccountID,
|
||||
OrderID: cert.OrderID,
|
||||
Leaf: leaf,
|
||||
Intermediates: intermediates,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
err = db.save(ctx, cert.ID, dbch, nil, "certificate", certTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serial := cert.Leaf.SerialNumber.String()
|
||||
dbSerial := &dbSerial{
|
||||
Serial: serial,
|
||||
CertificateID: cert.ID,
|
||||
}
|
||||
return db.save(ctx, serial, dbSerial, nil, "serial", certBySerialTable)
|
||||
}
|
||||
|
||||
// GetCertificate retrieves and unmarshals an ACME certificate type from the
|
||||
// datastore.
|
||||
func (db *DB) GetCertificate(_ context.Context, id string) (*acme.Certificate, error) {
|
||||
b, err := db.db.Get(certTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "certificate %s not found", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading certificate %s", id)
|
||||
}
|
||||
dbC := new(dbCert)
|
||||
if err := json.Unmarshal(b, dbC); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling certificate %s", id)
|
||||
}
|
||||
|
||||
certs, err := parseBundle(append(dbC.Leaf, dbC.Intermediates...))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing certificate chain for ACME certificate with ID %s", id)
|
||||
}
|
||||
|
||||
return &acme.Certificate{
|
||||
ID: dbC.ID,
|
||||
AccountID: dbC.AccountID,
|
||||
OrderID: dbC.OrderID,
|
||||
Leaf: certs[0],
|
||||
Intermediates: certs[1:],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCertificateBySerial retrieves and unmarshals an ACME certificate type from the
|
||||
// datastore based on a certificate serial number.
|
||||
func (db *DB) GetCertificateBySerial(ctx context.Context, serial string) (*acme.Certificate, error) {
|
||||
b, err := db.db.Get(certBySerialTable, []byte(serial))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "certificate with serial %s not found", serial)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading certificate ID for serial %s", serial)
|
||||
}
|
||||
|
||||
dbSerial := new(dbSerial)
|
||||
if err := json.Unmarshal(b, dbSerial); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling certificate with serial %s", serial)
|
||||
}
|
||||
|
||||
return db.GetCertificate(ctx, dbSerial.CertificateID)
|
||||
}
|
||||
|
||||
func parseBundle(b []byte) ([]*x509.Certificate, error) {
|
||||
var (
|
||||
err error
|
||||
block *pem.Block
|
||||
bundle []*x509.Certificate
|
||||
)
|
||||
for len(b) > 0 {
|
||||
block, b = pem.Decode(b)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type != "CERTIFICATE" {
|
||||
return nil, errors.New("error decoding PEM: data contains block that is not a certificate")
|
||||
}
|
||||
var crt *x509.Certificate
|
||||
crt, err = x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing x509 certificate")
|
||||
}
|
||||
bundle = append(bundle, crt)
|
||||
}
|
||||
if len(b) > 0 {
|
||||
return nil, errors.New("error decoding PEM: unexpected data")
|
||||
}
|
||||
return bundle, nil
|
||||
}
|
|
@ -1,470 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
nosqldb "github.com/smallstep/nosql/database"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
)
|
||||
|
||||
func TestDB_CreateCertificate(t *testing.T) {
|
||||
leaf, err := pemutil.ReadCertificate("../../../authority/testdata/certs/foo.crt")
|
||||
assert.FatalError(t, err)
|
||||
inter, err := pemutil.ReadCertificate("../../../authority/testdata/certs/intermediate_ca.crt")
|
||||
assert.FatalError(t, err)
|
||||
root, err := pemutil.ReadCertificate("../../../authority/testdata/certs/root_ca.crt")
|
||||
assert.FatalError(t, err)
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
cert *acme.Certificate
|
||||
err error
|
||||
_id *string
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/cmpAndSwap-error": func(t *testing.T) test {
|
||||
cert := &acme.Certificate{
|
||||
AccountID: "accountID",
|
||||
OrderID: "orderID",
|
||||
Leaf: leaf,
|
||||
Intermediates: []*x509.Certificate{inter, root},
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, certTable)
|
||||
assert.Equals(t, key, []byte(cert.ID))
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbc := new(dbCert)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbc))
|
||||
assert.Equals(t, dbc.ID, string(key))
|
||||
assert.Equals(t, dbc.ID, cert.ID)
|
||||
assert.Equals(t, dbc.AccountID, cert.AccountID)
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbc.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbc.CreatedAt))
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
},
|
||||
cert: cert,
|
||||
err: errors.New("error saving acme certificate: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
cert := &acme.Certificate{
|
||||
AccountID: "accountID",
|
||||
OrderID: "orderID",
|
||||
Leaf: leaf,
|
||||
Intermediates: []*x509.Certificate{inter, root},
|
||||
}
|
||||
var (
|
||||
id string
|
||||
idPtr = &id
|
||||
)
|
||||
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
if !bytes.Equal(bucket, certTable) && !bytes.Equal(bucket, certBySerialTable) {
|
||||
t.Fail()
|
||||
}
|
||||
if bytes.Equal(bucket, certTable) {
|
||||
*idPtr = string(key)
|
||||
assert.Equals(t, bucket, certTable)
|
||||
assert.Equals(t, key, []byte(cert.ID))
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbc := new(dbCert)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbc))
|
||||
assert.Equals(t, dbc.ID, string(key))
|
||||
assert.Equals(t, dbc.ID, cert.ID)
|
||||
assert.Equals(t, dbc.AccountID, cert.AccountID)
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbc.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbc.CreatedAt))
|
||||
}
|
||||
if bytes.Equal(bucket, certBySerialTable) {
|
||||
assert.Equals(t, bucket, certBySerialTable)
|
||||
assert.Equals(t, key, []byte(cert.Leaf.SerialNumber.String()))
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbs := new(dbSerial)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbs))
|
||||
assert.Equals(t, dbs.Serial, string(key))
|
||||
assert.Equals(t, dbs.CertificateID, cert.ID)
|
||||
|
||||
*idPtr = cert.ID
|
||||
}
|
||||
|
||||
return nil, true, nil
|
||||
},
|
||||
},
|
||||
_id: idPtr,
|
||||
cert: cert,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.CreateCertificate(context.Background(), tc.cert); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, tc.cert.ID, *tc._id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_GetCertificate(t *testing.T) {
|
||||
leaf, err := pemutil.ReadCertificate("../../../authority/testdata/certs/foo.crt")
|
||||
assert.FatalError(t, err)
|
||||
inter, err := pemutil.ReadCertificate("../../../authority/testdata/certs/intermediate_ca.crt")
|
||||
assert.FatalError(t, err)
|
||||
root, err := pemutil.ReadCertificate("../../../authority/testdata/certs/root_ca.crt")
|
||||
assert.FatalError(t, err)
|
||||
|
||||
certID := "certID"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/not-found": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, certTable)
|
||||
assert.Equals(t, string(key), certID)
|
||||
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
acmeErr: acme.NewError(acme.ErrorMalformedType, "certificate certID not found"),
|
||||
}
|
||||
},
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, certTable)
|
||||
assert.Equals(t, string(key), certID)
|
||||
|
||||
return nil, errors.Errorf("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading certificate certID: force"),
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, certTable)
|
||||
assert.Equals(t, string(key), certID)
|
||||
|
||||
return []byte("foobar"), nil
|
||||
},
|
||||
},
|
||||
err: errors.New("error unmarshaling certificate certID"),
|
||||
}
|
||||
},
|
||||
"fail/parseBundle-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, certTable)
|
||||
assert.Equals(t, string(key), certID)
|
||||
|
||||
cert := dbCert{
|
||||
ID: certID,
|
||||
AccountID: "accountID",
|
||||
OrderID: "orderID",
|
||||
Leaf: pem.EncodeToMemory(&pem.Block{
|
||||
Type: "Public Key",
|
||||
Bytes: leaf.Raw,
|
||||
}),
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
b, err := json.Marshal(cert)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
},
|
||||
err: errors.Errorf("error parsing certificate chain for ACME certificate with ID certID"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, certTable)
|
||||
assert.Equals(t, string(key), certID)
|
||||
|
||||
cert := dbCert{
|
||||
ID: certID,
|
||||
AccountID: "accountID",
|
||||
OrderID: "orderID",
|
||||
Leaf: pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: leaf.Raw,
|
||||
}),
|
||||
Intermediates: append(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: inter.Raw,
|
||||
}), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: root.Raw,
|
||||
})...),
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
b, err := json.Marshal(cert)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
cert, err := d.GetCertificate(context.Background(), certID)
|
||||
if err != nil {
|
||||
var acmeErr *acme.Error
|
||||
if errors.As(err, &acmeErr) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, acmeErr.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, acmeErr.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, cert.ID, certID)
|
||||
assert.Equals(t, cert.AccountID, "accountID")
|
||||
assert.Equals(t, cert.OrderID, "orderID")
|
||||
assert.Equals(t, cert.Leaf, leaf)
|
||||
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseBundle(t *testing.T) {
|
||||
leaf, err := pemutil.ReadCertificate("../../../authority/testdata/certs/foo.crt")
|
||||
assert.FatalError(t, err)
|
||||
inter, err := pemutil.ReadCertificate("../../../authority/testdata/certs/intermediate_ca.crt")
|
||||
assert.FatalError(t, err)
|
||||
root, err := pemutil.ReadCertificate("../../../authority/testdata/certs/root_ca.crt")
|
||||
assert.FatalError(t, err)
|
||||
|
||||
var certs []byte
|
||||
for _, cert := range []*x509.Certificate{leaf, inter, root} {
|
||||
certs = append(certs, pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
})...)
|
||||
}
|
||||
|
||||
type test struct {
|
||||
b []byte
|
||||
err error
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"fail/bad-type-error": {
|
||||
b: pem.EncodeToMemory(&pem.Block{
|
||||
Type: "Public Key",
|
||||
Bytes: leaf.Raw,
|
||||
}),
|
||||
err: errors.Errorf("error decoding PEM: data contains block that is not a certificate"),
|
||||
},
|
||||
"fail/bad-pem-error": {
|
||||
b: pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: []byte("foo"),
|
||||
}),
|
||||
err: errors.Errorf("error parsing x509 certificate"),
|
||||
},
|
||||
"fail/unexpected-data": {
|
||||
b: append(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: leaf.Raw,
|
||||
}), []byte("foo")...),
|
||||
err: errors.Errorf("error decoding PEM: unexpected data"),
|
||||
},
|
||||
"ok": {
|
||||
b: certs,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ret, err := parseBundle(tc.b)
|
||||
if err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, ret, []*x509.Certificate{leaf, inter, root})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_GetCertificateBySerial(t *testing.T) {
|
||||
leaf, err := pemutil.ReadCertificate("../../../authority/testdata/certs/foo.crt")
|
||||
assert.FatalError(t, err)
|
||||
inter, err := pemutil.ReadCertificate("../../../authority/testdata/certs/intermediate_ca.crt")
|
||||
assert.FatalError(t, err)
|
||||
root, err := pemutil.ReadCertificate("../../../authority/testdata/certs/root_ca.crt")
|
||||
assert.FatalError(t, err)
|
||||
|
||||
certID := "certID"
|
||||
serial := ""
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/not-found": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
if bytes.Equal(bucket, certBySerialTable) {
|
||||
return nil, nosqldb.ErrNotFound
|
||||
}
|
||||
return nil, errors.New("wrong table")
|
||||
},
|
||||
},
|
||||
acmeErr: acme.NewError(acme.ErrorMalformedType, "certificate with serial %s not found", serial),
|
||||
}
|
||||
},
|
||||
"fail/db-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
if bytes.Equal(bucket, certBySerialTable) {
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
return nil, errors.New("wrong table")
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf("error loading certificate ID for serial %s", serial),
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-dbSerial": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
if bytes.Equal(bucket, certBySerialTable) {
|
||||
return []byte(`{"serial":malformed!}`), nil
|
||||
}
|
||||
return nil, errors.New("wrong table")
|
||||
},
|
||||
},
|
||||
err: fmt.Errorf("error unmarshaling certificate with serial %s", serial),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
|
||||
if bytes.Equal(bucket, certBySerialTable) {
|
||||
certSerial := dbSerial{
|
||||
Serial: serial,
|
||||
CertificateID: certID,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(certSerial)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
if bytes.Equal(bucket, certTable) {
|
||||
cert := dbCert{
|
||||
ID: certID,
|
||||
AccountID: "accountID",
|
||||
OrderID: "orderID",
|
||||
Leaf: pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: leaf.Raw,
|
||||
}),
|
||||
Intermediates: append(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: inter.Raw,
|
||||
}), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: root.Raw,
|
||||
})...),
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
b, err := json.Marshal(cert)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
return nil, errors.New("wrong table")
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
cert, err := d.GetCertificateBySerial(context.Background(), serial)
|
||||
if err != nil {
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, cert.ID, certID)
|
||||
assert.Equals(t, cert.AccountID, "accountID")
|
||||
assert.Equals(t, cert.OrderID, "orderID")
|
||||
assert.Equals(t, cert.Leaf, leaf)
|
||||
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/smallstep/nosql"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
)
|
||||
|
||||
type dbChallenge struct {
|
||||
ID string `json:"id"`
|
||||
AccountID string `json:"accountID"`
|
||||
Type acme.ChallengeType `json:"type"`
|
||||
Status acme.Status `json:"status"`
|
||||
Token string `json:"token"`
|
||||
Value string `json:"value"`
|
||||
ValidatedAt string `json:"validatedAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Error *acme.Error `json:"error"` // TODO(hs): a bit dangerous; should become db-specific type
|
||||
}
|
||||
|
||||
func (dbc *dbChallenge) clone() *dbChallenge {
|
||||
u := *dbc
|
||||
return &u
|
||||
}
|
||||
|
||||
func (db *DB) getDBChallenge(_ context.Context, id string) (*dbChallenge, error) {
|
||||
data, err := db.db.Get(challengeTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "challenge %s not found", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading acme challenge %s", id)
|
||||
}
|
||||
|
||||
dbch := new(dbChallenge)
|
||||
if err := json.Unmarshal(data, dbch); err != nil {
|
||||
return nil, errors.Wrap(err, "error unmarshaling dbChallenge")
|
||||
}
|
||||
return dbch, nil
|
||||
}
|
||||
|
||||
// CreateChallenge creates a new ACME challenge data structure in the database.
|
||||
// Implements acme.DB.CreateChallenge interface.
|
||||
func (db *DB) CreateChallenge(ctx context.Context, ch *acme.Challenge) error {
|
||||
var err error
|
||||
ch.ID, err = randID()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error generating random id for ACME challenge")
|
||||
}
|
||||
|
||||
dbch := &dbChallenge{
|
||||
ID: ch.ID,
|
||||
AccountID: ch.AccountID,
|
||||
Value: ch.Value,
|
||||
Status: acme.StatusPending,
|
||||
Token: ch.Token,
|
||||
CreatedAt: clock.Now(),
|
||||
Type: ch.Type,
|
||||
}
|
||||
|
||||
return db.save(ctx, ch.ID, dbch, nil, "challenge", challengeTable)
|
||||
}
|
||||
|
||||
// GetChallenge retrieves and unmarshals an ACME challenge type from the database.
|
||||
// Implements the acme.DB GetChallenge interface.
|
||||
func (db *DB) GetChallenge(ctx context.Context, id, authzID string) (*acme.Challenge, error) {
|
||||
_ = authzID // unused input
|
||||
dbch, err := db.getDBChallenge(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := &acme.Challenge{
|
||||
ID: dbch.ID,
|
||||
AccountID: dbch.AccountID,
|
||||
Type: dbch.Type,
|
||||
Value: dbch.Value,
|
||||
Status: dbch.Status,
|
||||
Token: dbch.Token,
|
||||
Error: dbch.Error,
|
||||
ValidatedAt: dbch.ValidatedAt,
|
||||
}
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// UpdateChallenge updates an ACME challenge type in the database.
|
||||
func (db *DB) UpdateChallenge(ctx context.Context, ch *acme.Challenge) error {
|
||||
old, err := db.getDBChallenge(ctx, ch.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nu := old.clone()
|
||||
|
||||
// These should be the only values changing in an Update request.
|
||||
nu.Status = ch.Status
|
||||
nu.Error = ch.Error
|
||||
nu.ValidatedAt = ch.ValidatedAt
|
||||
|
||||
return db.save(ctx, old.ID, nu, old, "challenge", challengeTable)
|
||||
}
|
|
@ -1,460 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
nosqldb "github.com/smallstep/nosql/database"
|
||||
)
|
||||
|
||||
func TestDB_getDBChallenge(t *testing.T) {
|
||||
chID := "chID"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
dbc *dbChallenge
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/not-found": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), chID)
|
||||
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
acmeErr: acme.NewError(acme.ErrorMalformedType, "challenge chID not found"),
|
||||
}
|
||||
},
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), chID)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading acme challenge chID: force"),
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), chID)
|
||||
|
||||
return []byte("foo"), nil
|
||||
},
|
||||
},
|
||||
err: errors.New("error unmarshaling dbChallenge"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
dbc := &dbChallenge{
|
||||
ID: chID,
|
||||
AccountID: "accountID",
|
||||
Type: "dns-01",
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
Value: "test.ca.smallstep.com",
|
||||
CreatedAt: clock.Now(),
|
||||
ValidatedAt: "foobar",
|
||||
Error: acme.NewErrorISE("The server experienced an internal error"),
|
||||
}
|
||||
b, err := json.Marshal(dbc)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), chID)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
},
|
||||
dbc: dbc,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if ch, err := d.getDBChallenge(context.Background(), chID); err != nil {
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, ch.ID, tc.dbc.ID)
|
||||
assert.Equals(t, ch.AccountID, tc.dbc.AccountID)
|
||||
assert.Equals(t, ch.Type, tc.dbc.Type)
|
||||
assert.Equals(t, ch.Status, tc.dbc.Status)
|
||||
assert.Equals(t, ch.Token, tc.dbc.Token)
|
||||
assert.Equals(t, ch.Value, tc.dbc.Value)
|
||||
assert.Equals(t, ch.ValidatedAt, tc.dbc.ValidatedAt)
|
||||
assert.Equals(t, ch.Error.Error(), tc.dbc.Error.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_CreateChallenge(t *testing.T) {
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
ch *acme.Challenge
|
||||
err error
|
||||
_id *string
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/cmpAndSwap-error": func(t *testing.T) test {
|
||||
ch := &acme.Challenge{
|
||||
AccountID: "accountID",
|
||||
Type: "dns-01",
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
Value: "test.ca.smallstep.com",
|
||||
}
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), ch.ID)
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbc := new(dbChallenge)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbc))
|
||||
assert.Equals(t, dbc.ID, string(key))
|
||||
assert.Equals(t, dbc.AccountID, ch.AccountID)
|
||||
assert.Equals(t, dbc.Type, ch.Type)
|
||||
assert.Equals(t, dbc.Status, ch.Status)
|
||||
assert.Equals(t, dbc.Token, ch.Token)
|
||||
assert.Equals(t, dbc.Value, ch.Value)
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbc.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbc.CreatedAt))
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
},
|
||||
ch: ch,
|
||||
err: errors.New("error saving acme challenge: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
var (
|
||||
id string
|
||||
idPtr = &id
|
||||
ch = &acme.Challenge{
|
||||
AccountID: "accountID",
|
||||
Type: "dns-01",
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
Value: "test.ca.smallstep.com",
|
||||
}
|
||||
)
|
||||
|
||||
return test{
|
||||
ch: ch,
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
*idPtr = string(key)
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), ch.ID)
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbc := new(dbChallenge)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbc))
|
||||
assert.Equals(t, dbc.ID, string(key))
|
||||
assert.Equals(t, dbc.AccountID, ch.AccountID)
|
||||
assert.Equals(t, dbc.Type, ch.Type)
|
||||
assert.Equals(t, dbc.Status, ch.Status)
|
||||
assert.Equals(t, dbc.Token, ch.Token)
|
||||
assert.Equals(t, dbc.Value, ch.Value)
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbc.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbc.CreatedAt))
|
||||
return nil, true, nil
|
||||
},
|
||||
},
|
||||
_id: idPtr,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.CreateChallenge(context.Background(), tc.ch); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, tc.ch.ID, *tc._id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_GetChallenge(t *testing.T) {
|
||||
chID := "chID"
|
||||
azID := "azID"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
dbc *dbChallenge
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), chID)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading acme challenge chID: force"),
|
||||
}
|
||||
},
|
||||
"fail/forward-acme-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), chID)
|
||||
|
||||
return nil, nosqldb.ErrNotFound
|
||||
},
|
||||
},
|
||||
acmeErr: acme.NewError(acme.ErrorMalformedType, "challenge chID not found"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
dbc := &dbChallenge{
|
||||
ID: chID,
|
||||
AccountID: "accountID",
|
||||
Type: "dns-01",
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
Value: "test.ca.smallstep.com",
|
||||
CreatedAt: clock.Now(),
|
||||
ValidatedAt: "foobar",
|
||||
Error: acme.NewErrorISE("The server experienced an internal error"),
|
||||
}
|
||||
b, err := json.Marshal(dbc)
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), chID)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
},
|
||||
dbc: dbc,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if ch, err := d.GetChallenge(context.Background(), chID, azID); err != nil {
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, ch.ID, tc.dbc.ID)
|
||||
assert.Equals(t, ch.AccountID, tc.dbc.AccountID)
|
||||
assert.Equals(t, ch.Type, tc.dbc.Type)
|
||||
assert.Equals(t, ch.Status, tc.dbc.Status)
|
||||
assert.Equals(t, ch.Token, tc.dbc.Token)
|
||||
assert.Equals(t, ch.Value, tc.dbc.Value)
|
||||
assert.Equals(t, ch.ValidatedAt, tc.dbc.ValidatedAt)
|
||||
assert.Equals(t, ch.Error.Error(), tc.dbc.Error.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_UpdateChallenge(t *testing.T) {
|
||||
chID := "chID"
|
||||
dbc := &dbChallenge{
|
||||
ID: chID,
|
||||
AccountID: "accountID",
|
||||
Type: "dns-01",
|
||||
Status: acme.StatusPending,
|
||||
Token: "token",
|
||||
Value: "test.ca.smallstep.com",
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
b, err := json.Marshal(dbc)
|
||||
assert.FatalError(t, err)
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
ch *acme.Challenge
|
||||
err error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/db.Get-error": func(t *testing.T) test {
|
||||
return test{
|
||||
ch: &acme.Challenge{
|
||||
ID: chID,
|
||||
},
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), chID)
|
||||
|
||||
return nil, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error loading acme challenge chID: force"),
|
||||
}
|
||||
},
|
||||
"fail/db.CmpAndSwap-error": func(t *testing.T) test {
|
||||
updCh := &acme.Challenge{
|
||||
ID: chID,
|
||||
Status: acme.StatusValid,
|
||||
ValidatedAt: "foobar",
|
||||
Error: acme.NewError(acme.ErrorMalformedType, "The request message was malformed"),
|
||||
}
|
||||
return test{
|
||||
ch: updCh,
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), chID)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, old, b)
|
||||
|
||||
dbOld := new(dbChallenge)
|
||||
assert.FatalError(t, json.Unmarshal(old, dbOld))
|
||||
assert.Equals(t, dbc, dbOld)
|
||||
|
||||
dbNew := new(dbChallenge)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbNew))
|
||||
assert.Equals(t, dbNew.ID, dbc.ID)
|
||||
assert.Equals(t, dbNew.AccountID, dbc.AccountID)
|
||||
assert.Equals(t, dbNew.Type, dbc.Type)
|
||||
assert.Equals(t, dbNew.Status, updCh.Status)
|
||||
assert.Equals(t, dbNew.Token, dbc.Token)
|
||||
assert.Equals(t, dbNew.Value, dbc.Value)
|
||||
assert.Equals(t, dbNew.Error.Error(), updCh.Error.Error())
|
||||
assert.Equals(t, dbNew.CreatedAt, dbc.CreatedAt)
|
||||
assert.Equals(t, dbNew.ValidatedAt, updCh.ValidatedAt)
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error saving acme challenge: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
updCh := &acme.Challenge{
|
||||
ID: dbc.ID,
|
||||
AccountID: dbc.AccountID,
|
||||
Type: dbc.Type,
|
||||
Token: dbc.Token,
|
||||
Value: dbc.Value,
|
||||
Status: acme.StatusValid,
|
||||
ValidatedAt: "foobar",
|
||||
Error: acme.NewError(acme.ErrorMalformedType, "malformed"),
|
||||
}
|
||||
return test{
|
||||
ch: updCh,
|
||||
db: &db.MockNoSQLDB{
|
||||
MGet: func(bucket, key []byte) ([]byte, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), chID)
|
||||
|
||||
return b, nil
|
||||
},
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, old, b)
|
||||
|
||||
dbOld := new(dbChallenge)
|
||||
assert.FatalError(t, json.Unmarshal(old, dbOld))
|
||||
assert.Equals(t, dbc, dbOld)
|
||||
|
||||
dbNew := new(dbChallenge)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbNew))
|
||||
assert.Equals(t, dbNew.ID, dbc.ID)
|
||||
assert.Equals(t, dbNew.AccountID, dbc.AccountID)
|
||||
assert.Equals(t, dbNew.Type, dbc.Type)
|
||||
assert.Equals(t, dbNew.Token, dbc.Token)
|
||||
assert.Equals(t, dbNew.Value, dbc.Value)
|
||||
assert.Equals(t, dbNew.CreatedAt, dbc.CreatedAt)
|
||||
assert.Equals(t, dbNew.Status, acme.StatusValid)
|
||||
assert.Equals(t, dbNew.ValidatedAt, "foobar")
|
||||
assert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, "The request message was malformed").Error())
|
||||
return nu, true, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.UpdateChallenge(context.Background(), tc.ch); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, tc.ch.ID, dbc.ID)
|
||||
assert.Equals(t, tc.ch.AccountID, dbc.AccountID)
|
||||
assert.Equals(t, tc.ch.Type, dbc.Type)
|
||||
assert.Equals(t, tc.ch.Token, dbc.Token)
|
||||
assert.Equals(t, tc.ch.Value, dbc.Value)
|
||||
assert.Equals(t, tc.ch.ValidatedAt, "foobar")
|
||||
assert.Equals(t, tc.ch.Status, acme.StatusValid)
|
||||
assert.Equals(t, tc.ch.Error.Error(), acme.NewError(acme.ErrorMalformedType, "malformed").Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,387 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
nosqlDB "github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
// externalAccountKeyMutex for read/write locking of EAK operations.
|
||||
var externalAccountKeyMutex sync.RWMutex
|
||||
|
||||
// referencesByProvisionerIndexMutex for locking referencesByProvisioner index operations.
|
||||
var referencesByProvisionerIndexMutex sync.Mutex
|
||||
|
||||
type dbExternalAccountKey struct {
|
||||
ID string `json:"id"`
|
||||
ProvisionerID string `json:"provisionerID"`
|
||||
Reference string `json:"reference"`
|
||||
AccountID string `json:"accountID,omitempty"`
|
||||
HmacKey []byte `json:"key"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
BoundAt time.Time `json:"boundAt"`
|
||||
}
|
||||
|
||||
type dbExternalAccountKeyReference struct {
|
||||
Reference string `json:"reference"`
|
||||
ExternalAccountKeyID string `json:"externalAccountKeyID"`
|
||||
}
|
||||
|
||||
// getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey.
|
||||
func (db *DB) getDBExternalAccountKey(_ context.Context, id string) (*dbExternalAccountKey, error) {
|
||||
data, err := db.db.Get(externalAccountKeyTable, []byte(id))
|
||||
if err != nil {
|
||||
if nosqlDB.IsErrNotFound(err) {
|
||||
return nil, acme.ErrNotFound
|
||||
}
|
||||
return nil, errors.Wrapf(err, "error loading external account key %s", id)
|
||||
}
|
||||
|
||||
dbeak := new(dbExternalAccountKey)
|
||||
if err = json.Unmarshal(data, dbeak); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", id)
|
||||
}
|
||||
|
||||
return dbeak, nil
|
||||
}
|
||||
|
||||
// CreateExternalAccountKey creates a new External Account Binding key with a name
|
||||
func (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
externalAccountKeyMutex.Lock()
|
||||
defer externalAccountKeyMutex.Unlock()
|
||||
|
||||
keyID, err := randID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
random := make([]byte, 32)
|
||||
_, err = rand.Read(random)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbeak := &dbExternalAccountKey{
|
||||
ID: keyID,
|
||||
ProvisionerID: provisionerID,
|
||||
Reference: reference,
|
||||
HmacKey: random,
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
|
||||
if err := db.save(ctx, keyID, dbeak, nil, "external_account_key", externalAccountKeyTable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := db.addEAKID(ctx, provisionerID, dbeak.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dbeak.Reference != "" {
|
||||
dbExternalAccountKeyReference := &dbExternalAccountKeyReference{
|
||||
Reference: dbeak.Reference,
|
||||
ExternalAccountKeyID: dbeak.ID,
|
||||
}
|
||||
if err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, "external_account_key_reference", externalAccountKeyIDsByReferenceTable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &acme.ExternalAccountKey{
|
||||
ID: dbeak.ID,
|
||||
ProvisionerID: dbeak.ProvisionerID,
|
||||
Reference: dbeak.Reference,
|
||||
AccountID: dbeak.AccountID,
|
||||
HmacKey: dbeak.HmacKey,
|
||||
CreatedAt: dbeak.CreatedAt,
|
||||
BoundAt: dbeak.BoundAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetExternalAccountKey retrieves an External Account Binding key by KeyID
|
||||
func (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) {
|
||||
externalAccountKeyMutex.RLock()
|
||||
defer externalAccountKeyMutex.RUnlock()
|
||||
|
||||
dbeak, err := db.getDBExternalAccountKey(ctx, keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dbeak.ProvisionerID != provisionerID {
|
||||
return nil, acme.NewError(acme.ErrorUnauthorizedType, "provisioner does not match provisioner for which the EAB key was created")
|
||||
}
|
||||
|
||||
return &acme.ExternalAccountKey{
|
||||
ID: dbeak.ID,
|
||||
ProvisionerID: dbeak.ProvisionerID,
|
||||
Reference: dbeak.Reference,
|
||||
AccountID: dbeak.AccountID,
|
||||
HmacKey: dbeak.HmacKey,
|
||||
CreatedAt: dbeak.CreatedAt,
|
||||
BoundAt: dbeak.BoundAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error {
|
||||
externalAccountKeyMutex.Lock()
|
||||
defer externalAccountKeyMutex.Unlock()
|
||||
|
||||
dbeak, err := db.getDBExternalAccountKey(ctx, keyID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID)
|
||||
}
|
||||
|
||||
if dbeak.ProvisionerID != provisionerID {
|
||||
return errors.New("provisioner does not match provisioner for which the EAB key was created")
|
||||
}
|
||||
|
||||
if dbeak.Reference != "" {
|
||||
if err := db.db.Del(externalAccountKeyIDsByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil {
|
||||
return errors.Wrapf(err, "error deleting ACME EAB Key reference with Key ID %s and reference %s", keyID, dbeak.Reference)
|
||||
}
|
||||
}
|
||||
if err := db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil {
|
||||
return errors.Wrapf(err, "error deleting ACME EAB Key with Key ID %s", keyID)
|
||||
}
|
||||
if err := db.deleteEAKID(ctx, provisionerID, keyID); err != nil {
|
||||
return errors.Wrapf(err, "error removing ACME EAB Key ID %s", keyID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner
|
||||
func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {
|
||||
_, _ = cursor, limit // unused input
|
||||
|
||||
externalAccountKeyMutex.RLock()
|
||||
defer externalAccountKeyMutex.RUnlock()
|
||||
|
||||
// cursor and limit are ignored in open source, at least for now.
|
||||
|
||||
var eakIDs []string
|
||||
r, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID))
|
||||
if err != nil {
|
||||
if !nosqlDB.IsErrNotFound(err) {
|
||||
return nil, "", errors.Wrapf(err, "error loading ACME EAB Key IDs for provisioner %s", provisionerID)
|
||||
}
|
||||
// it may happen that no record is found; we'll continue with an empty slice
|
||||
} else {
|
||||
if err := json.Unmarshal(r, &eakIDs); err != nil {
|
||||
return nil, "", errors.Wrapf(err, "error unmarshaling ACME EAB Key IDs for provisioner %s", provisionerID)
|
||||
}
|
||||
}
|
||||
|
||||
keys := []*acme.ExternalAccountKey{}
|
||||
for _, eakID := range eakIDs {
|
||||
if eakID == "" {
|
||||
continue // shouldn't happen; just in case
|
||||
}
|
||||
eak, err := db.getDBExternalAccountKey(ctx, eakID)
|
||||
if err != nil {
|
||||
if !nosqlDB.IsErrNotFound(err) {
|
||||
return nil, "", errors.Wrapf(err, "error retrieving ACME EAB Key for provisioner %s and keyID %s", provisionerID, eakID)
|
||||
}
|
||||
}
|
||||
keys = append(keys, &acme.ExternalAccountKey{
|
||||
ID: eak.ID,
|
||||
HmacKey: eak.HmacKey,
|
||||
ProvisionerID: eak.ProvisionerID,
|
||||
Reference: eak.Reference,
|
||||
AccountID: eak.AccountID,
|
||||
CreatedAt: eak.CreatedAt,
|
||||
BoundAt: eak.BoundAt,
|
||||
})
|
||||
}
|
||||
|
||||
return keys, "", nil
|
||||
}
|
||||
|
||||
// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference
|
||||
func (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
|
||||
externalAccountKeyMutex.RLock()
|
||||
defer externalAccountKeyMutex.RUnlock()
|
||||
|
||||
if reference == "" {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
k, err := db.db.Get(externalAccountKeyIDsByReferenceTable, []byte(referenceKey(provisionerID, reference)))
|
||||
if nosqlDB.IsErrNotFound(err) {
|
||||
return nil, acme.ErrNotFound
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading ACME EAB key for reference %s", reference)
|
||||
}
|
||||
dbExternalAccountKeyReference := new(dbExternalAccountKeyReference)
|
||||
if err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling ACME EAB key for reference %s", reference)
|
||||
}
|
||||
|
||||
return db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID)
|
||||
}
|
||||
|
||||
func (db *DB) GetExternalAccountKeyByAccountID(context.Context, string, string) (*acme.ExternalAccountKey, error) {
|
||||
//nolint:nilnil // legacy
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {
|
||||
externalAccountKeyMutex.Lock()
|
||||
defer externalAccountKeyMutex.Unlock()
|
||||
|
||||
old, err := db.getDBExternalAccountKey(ctx, eak.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if old.ProvisionerID != provisionerID {
|
||||
return errors.New("provisioner does not match provisioner for which the EAB key was created")
|
||||
}
|
||||
|
||||
if old.ProvisionerID != eak.ProvisionerID {
|
||||
return errors.New("cannot change provisioner for an existing ACME EAB Key")
|
||||
}
|
||||
|
||||
if old.Reference != eak.Reference {
|
||||
return errors.New("cannot change reference for an existing ACME EAB Key")
|
||||
}
|
||||
|
||||
nu := dbExternalAccountKey{
|
||||
ID: eak.ID,
|
||||
ProvisionerID: eak.ProvisionerID,
|
||||
Reference: eak.Reference,
|
||||
AccountID: eak.AccountID,
|
||||
HmacKey: eak.HmacKey,
|
||||
CreatedAt: eak.CreatedAt,
|
||||
BoundAt: eak.BoundAt,
|
||||
}
|
||||
|
||||
return db.save(ctx, nu.ID, nu, old, "external_account_key", externalAccountKeyTable)
|
||||
}
|
||||
|
||||
func (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error {
|
||||
referencesByProvisionerIndexMutex.Lock()
|
||||
defer referencesByProvisionerIndexMutex.Unlock()
|
||||
|
||||
if eakID == "" {
|
||||
return errors.Errorf("can't add empty eakID for provisioner %s", provisionerID)
|
||||
}
|
||||
|
||||
var eakIDs []string
|
||||
b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID))
|
||||
if err != nil {
|
||||
if !nosqlDB.IsErrNotFound(err) {
|
||||
return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID)
|
||||
}
|
||||
// it may happen that no record is found; we'll continue with an empty slice
|
||||
} else {
|
||||
if err := json.Unmarshal(b, &eakIDs); err != nil {
|
||||
return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID)
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range eakIDs {
|
||||
if id == eakID {
|
||||
// return an error when a duplicate ID is found
|
||||
return errors.Errorf("eakID %s already exists for provisioner %s", eakID, provisionerID)
|
||||
}
|
||||
}
|
||||
|
||||
var newEAKIDs []string
|
||||
newEAKIDs = append(newEAKIDs, eakIDs...)
|
||||
newEAKIDs = append(newEAKIDs, eakID)
|
||||
|
||||
var (
|
||||
_old interface{} = eakIDs
|
||||
_new interface{} = newEAKIDs
|
||||
)
|
||||
|
||||
// ensure that the DB gets the expected value when the slice is empty; otherwise
|
||||
// it'll return with an error that indicates that the DBs view of the data is
|
||||
// different from the last read (i.e. _old is different from what the DB has).
|
||||
if len(eakIDs) == 0 {
|
||||
_old = nil
|
||||
}
|
||||
|
||||
if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil {
|
||||
return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error {
|
||||
referencesByProvisionerIndexMutex.Lock()
|
||||
defer referencesByProvisionerIndexMutex.Unlock()
|
||||
|
||||
var eakIDs []string
|
||||
b, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID))
|
||||
if err != nil {
|
||||
if !nosqlDB.IsErrNotFound(err) {
|
||||
return errors.Wrapf(err, "error loading eakIDs for provisioner %s", provisionerID)
|
||||
}
|
||||
// it may happen that no record is found; we'll continue with an empty slice
|
||||
} else {
|
||||
if err := json.Unmarshal(b, &eakIDs); err != nil {
|
||||
return errors.Wrapf(err, "error unmarshaling eakIDs for provisioner %s", provisionerID)
|
||||
}
|
||||
}
|
||||
|
||||
newEAKIDs := removeElement(eakIDs, eakID)
|
||||
var (
|
||||
_old interface{} = eakIDs
|
||||
_new interface{} = newEAKIDs
|
||||
)
|
||||
|
||||
// ensure that the DB gets the expected value when the slice is empty; otherwise
|
||||
// it'll return with an error that indicates that the DBs view of the data is
|
||||
// different from the last read (i.e. _old is different from what the DB has).
|
||||
if len(eakIDs) == 0 {
|
||||
_old = nil
|
||||
}
|
||||
|
||||
if err = db.save(ctx, provisionerID, _new, _old, "externalAccountKeyIDsByProvisionerID", externalAccountKeyIDsByProvisionerIDTable); err != nil {
|
||||
return errors.Wrapf(err, "error saving eakIDs index for provisioner %s", provisionerID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// referenceKey returns a unique key for a reference per provisioner
|
||||
func referenceKey(provisionerID, reference string) string {
|
||||
return provisionerID + "." + reference
|
||||
}
|
||||
|
||||
// sliceIndex finds the index of item in slice
|
||||
func sliceIndex(slice []string, item string) int {
|
||||
for i := range slice {
|
||||
if slice[i] == item {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// removeElement deletes the item if it exists in the
|
||||
// slice. It returns a new slice, keeping the old one intact.
|
||||
func removeElement(slice []string, item string) []string {
|
||||
newSlice := make([]string, 0)
|
||||
index := sliceIndex(slice, item)
|
||||
if index < 0 {
|
||||
newSlice = append(newSlice, slice...)
|
||||
return newSlice
|
||||
}
|
||||
|
||||
newSlice = append(newSlice, slice[:index]...)
|
||||
|
||||
return append(newSlice, slice[index+1:]...)
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,66 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/nosql"
|
||||
"github.com/smallstep/nosql/database"
|
||||
)
|
||||
|
||||
// dbNonce contains nonce metadata used in the ACME protocol.
|
||||
type dbNonce struct {
|
||||
ID string
|
||||
CreatedAt time.Time
|
||||
DeletedAt time.Time
|
||||
}
|
||||
|
||||
// CreateNonce creates, stores, and returns an ACME replay-nonce.
|
||||
// Implements the acme.DB interface.
|
||||
func (db *DB) CreateNonce(ctx context.Context) (acme.Nonce, error) {
|
||||
_id, err := randID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id := base64.RawURLEncoding.EncodeToString([]byte(_id))
|
||||
n := &dbNonce{
|
||||
ID: id,
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
if err := db.save(ctx, id, n, nil, "nonce", nonceTable); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return acme.Nonce(id), nil
|
||||
}
|
||||
|
||||
// DeleteNonce verifies that the nonce is valid (by checking if it exists),
|
||||
// and if so, consumes the nonce resource by deleting it from the database.
|
||||
func (db *DB) DeleteNonce(_ context.Context, nonce acme.Nonce) error {
|
||||
err := db.db.Update(&database.Tx{
|
||||
Operations: []*database.TxEntry{
|
||||
{
|
||||
Bucket: nonceTable,
|
||||
Key: []byte(nonce),
|
||||
Cmd: database.Get,
|
||||
},
|
||||
{
|
||||
Bucket: nonceTable,
|
||||
Key: []byte(nonce),
|
||||
Cmd: database.Delete,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
switch {
|
||||
case nosql.IsErrNotFound(err):
|
||||
return acme.NewError(acme.ErrorBadNonceType, "nonce %s not found", string(nonce))
|
||||
case err != nil:
|
||||
return errors.Wrapf(err, "error deleting nonce %s", string(nonce))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -1,168 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
"github.com/smallstep/nosql/database"
|
||||
)
|
||||
|
||||
func TestDB_CreateNonce(t *testing.T) {
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
_id *string
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/cmpAndSwap-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, nonceTable)
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbn := new(dbNonce)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbn))
|
||||
assert.Equals(t, dbn.ID, string(key))
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbn.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbn.CreatedAt))
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error saving acme nonce: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
var (
|
||||
id string
|
||||
idPtr = &id
|
||||
)
|
||||
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
*idPtr = string(key)
|
||||
assert.Equals(t, bucket, nonceTable)
|
||||
assert.Equals(t, old, nil)
|
||||
|
||||
dbn := new(dbNonce)
|
||||
assert.FatalError(t, json.Unmarshal(nu, dbn))
|
||||
assert.Equals(t, dbn.ID, string(key))
|
||||
assert.True(t, clock.Now().Add(-time.Minute).Before(dbn.CreatedAt))
|
||||
assert.True(t, clock.Now().Add(time.Minute).After(dbn.CreatedAt))
|
||||
return nil, true, nil
|
||||
},
|
||||
},
|
||||
_id: idPtr,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if n, err := d.CreateNonce(context.Background()); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
if assert.Nil(t, tc.err) {
|
||||
assert.Equals(t, string(n), *tc._id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDB_DeleteNonce(t *testing.T) {
|
||||
|
||||
nonceID := "nonceID"
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
acmeErr *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/not-found": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MUpdate: func(tx *database.Tx) error {
|
||||
assert.Equals(t, tx.Operations[0].Bucket, nonceTable)
|
||||
assert.Equals(t, tx.Operations[0].Key, []byte(nonceID))
|
||||
assert.Equals(t, tx.Operations[0].Cmd, database.Get)
|
||||
|
||||
assert.Equals(t, tx.Operations[1].Bucket, nonceTable)
|
||||
assert.Equals(t, tx.Operations[1].Key, []byte(nonceID))
|
||||
assert.Equals(t, tx.Operations[1].Cmd, database.Delete)
|
||||
return database.ErrNotFound
|
||||
},
|
||||
},
|
||||
acmeErr: acme.NewError(acme.ErrorBadNonceType, "nonce %s not found", nonceID),
|
||||
}
|
||||
},
|
||||
"fail/db.Update-error": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MUpdate: func(tx *database.Tx) error {
|
||||
assert.Equals(t, tx.Operations[0].Bucket, nonceTable)
|
||||
assert.Equals(t, tx.Operations[0].Key, []byte(nonceID))
|
||||
assert.Equals(t, tx.Operations[0].Cmd, database.Get)
|
||||
|
||||
assert.Equals(t, tx.Operations[1].Bucket, nonceTable)
|
||||
assert.Equals(t, tx.Operations[1].Key, []byte(nonceID))
|
||||
assert.Equals(t, tx.Operations[1].Cmd, database.Delete)
|
||||
return errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error deleting nonce nonceID: force"),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
return test{
|
||||
db: &db.MockNoSQLDB{
|
||||
MUpdate: func(tx *database.Tx) error {
|
||||
assert.Equals(t, tx.Operations[0].Bucket, nonceTable)
|
||||
assert.Equals(t, tx.Operations[0].Key, []byte(nonceID))
|
||||
assert.Equals(t, tx.Operations[0].Cmd, database.Get)
|
||||
|
||||
assert.Equals(t, tx.Operations[1].Bucket, nonceTable)
|
||||
assert.Equals(t, tx.Operations[1].Key, []byte(nonceID))
|
||||
assert.Equals(t, tx.Operations[1].Cmd, database.Delete)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := DB{db: tc.db}
|
||||
if err := d.DeleteNonce(context.Background(), acme.Nonce(nonceID)); err != nil {
|
||||
var ae *acme.Error
|
||||
if errors.As(err, &ae) {
|
||||
if assert.NotNil(t, tc.acmeErr) {
|
||||
assert.Equals(t, ae.Type, tc.acmeErr.Type)
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
assert.Equals(t, ae.Status, tc.acmeErr.Status)
|
||||
assert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())
|
||||
assert.Equals(t, ae.Detail, tc.acmeErr.Detail)
|
||||
}
|
||||
} else {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
nosqlDB "github.com/smallstep/nosql"
|
||||
"go.step.sm/crypto/randutil"
|
||||
)
|
||||
|
||||
var (
|
||||
accountTable = []byte("acme_accounts")
|
||||
accountByKeyIDTable = []byte("acme_keyID_accountID_index")
|
||||
authzTable = []byte("acme_authzs")
|
||||
challengeTable = []byte("acme_challenges")
|
||||
nonceTable = []byte("nonces")
|
||||
orderTable = []byte("acme_orders")
|
||||
ordersByAccountIDTable = []byte("acme_account_orders_index")
|
||||
certTable = []byte("acme_certs")
|
||||
certBySerialTable = []byte("acme_serial_certs_index")
|
||||
externalAccountKeyTable = []byte("acme_external_account_keys")
|
||||
externalAccountKeyIDsByReferenceTable = []byte("acme_external_account_keyID_reference_index")
|
||||
externalAccountKeyIDsByProvisionerIDTable = []byte("acme_external_account_keyID_provisionerID_index")
|
||||
)
|
||||
|
||||
// DB is a struct that implements the AcmeDB interface.
|
||||
type DB struct {
|
||||
db nosqlDB.DB
|
||||
}
|
||||
|
||||
// New configures and returns a new ACME DB backend implemented using a nosql DB.
|
||||
func New(db nosqlDB.DB) (*DB, error) {
|
||||
tables := [][]byte{accountTable, accountByKeyIDTable, authzTable,
|
||||
challengeTable, nonceTable, orderTable, ordersByAccountIDTable,
|
||||
certTable, certBySerialTable, externalAccountKeyTable,
|
||||
externalAccountKeyIDsByReferenceTable, externalAccountKeyIDsByProvisionerIDTable,
|
||||
}
|
||||
for _, b := range tables {
|
||||
if err := db.CreateTable(b); err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating table %s",
|
||||
string(b))
|
||||
}
|
||||
}
|
||||
return &DB{db}, nil
|
||||
}
|
||||
|
||||
// save writes the new data to the database, overwriting the old data if it
|
||||
// existed.
|
||||
func (db *DB) save(_ context.Context, id string, nu, old interface{}, typ string, table []byte) error {
|
||||
var (
|
||||
err error
|
||||
newB []byte
|
||||
)
|
||||
if nu == nil {
|
||||
newB = nil
|
||||
} else {
|
||||
newB, err = json.Marshal(nu)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling acme type: %s, value: %v", typ, nu)
|
||||
}
|
||||
}
|
||||
var oldB []byte
|
||||
if old == nil {
|
||||
oldB = nil
|
||||
} else {
|
||||
oldB, err = json.Marshal(old)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling acme type: %s, value: %v", typ, old)
|
||||
}
|
||||
}
|
||||
|
||||
_, swapped, err := db.db.CmpAndSwap(table, []byte(id), oldB, newB)
|
||||
switch {
|
||||
case err != nil:
|
||||
return errors.Wrapf(err, "error saving acme %s", typ)
|
||||
case !swapped:
|
||||
return errors.Errorf("error saving acme %s; changed since last read", typ)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var idLen = 32
|
||||
|
||||
func randID() (val string, err error) {
|
||||
val, err = randutil.Alphanumeric(idLen)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error generating random alphanumeric ID")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Clock that returns time in UTC rounded to seconds.
|
||||
type Clock struct{}
|
||||
|
||||
// Now returns the UTC time rounded to seconds.
|
||||
func (c *Clock) Now() time.Time {
|
||||
return time.Now().UTC().Truncate(time.Second)
|
||||
}
|
||||
|
||||
var clock = new(Clock)
|
|
@ -1,139 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
err error
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"fail/db.CreateTable-error": {
|
||||
db: &db.MockNoSQLDB{
|
||||
MCreateTable: func(bucket []byte) error {
|
||||
assert.Equals(t, string(bucket), string(accountTable))
|
||||
return errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.Errorf("error creating table %s: force", string(accountTable)),
|
||||
},
|
||||
"ok": {
|
||||
db: &db.MockNoSQLDB{
|
||||
MCreateTable: func(bucket []byte) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := New(tc.db); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type errorThrower string
|
||||
|
||||
func (et errorThrower) MarshalJSON() ([]byte, error) {
|
||||
return nil, errors.New("force")
|
||||
}
|
||||
|
||||
func TestDB_save(t *testing.T) {
|
||||
type test struct {
|
||||
db nosql.DB
|
||||
nu interface{}
|
||||
old interface{}
|
||||
err error
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"fail/error-marshaling-new": {
|
||||
nu: errorThrower("foo"),
|
||||
err: errors.New("error marshaling acme type: challenge"),
|
||||
},
|
||||
"fail/error-marshaling-old": {
|
||||
nu: "new",
|
||||
old: errorThrower("foo"),
|
||||
err: errors.New("error marshaling acme type: challenge"),
|
||||
},
|
||||
"fail/db.CmpAndSwap-error": {
|
||||
nu: "new",
|
||||
old: "old",
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), "id")
|
||||
assert.Equals(t, string(old), "\"old\"")
|
||||
assert.Equals(t, string(nu), "\"new\"")
|
||||
return nil, false, errors.New("force")
|
||||
},
|
||||
},
|
||||
err: errors.New("error saving acme challenge: force"),
|
||||
},
|
||||
"fail/db.CmpAndSwap-false-marshaling-old": {
|
||||
nu: "new",
|
||||
old: "old",
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), "id")
|
||||
assert.Equals(t, string(old), "\"old\"")
|
||||
assert.Equals(t, string(nu), "\"new\"")
|
||||
return nil, false, nil
|
||||
},
|
||||
},
|
||||
err: errors.New("error saving acme challenge; changed since last read"),
|
||||
},
|
||||
"ok": {
|
||||
nu: "new",
|
||||
old: "old",
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), "id")
|
||||
assert.Equals(t, string(old), "\"old\"")
|
||||
assert.Equals(t, string(nu), "\"new\"")
|
||||
return nu, true, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
"ok/nils": {
|
||||
nu: nil,
|
||||
old: nil,
|
||||
db: &db.MockNoSQLDB{
|
||||
MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {
|
||||
assert.Equals(t, bucket, challengeTable)
|
||||
assert.Equals(t, string(key), "id")
|
||||
assert.Equals(t, old, nil)
|
||||
assert.Equals(t, nu, nil)
|
||||
return nu, true, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
d := &DB{db: tc.db}
|
||||
if err := d.save(context.Background(), "id", tc.nu, tc.old, "challenge", challengeTable); err != nil {
|
||||
if assert.NotNil(t, tc.err) {
|
||||
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
// Mutex for locking ordersByAccount index operations.
|
||||
var ordersByAccountMux sync.Mutex
|
||||
|
||||
type dbOrder struct {
|
||||
ID string `json:"id"`
|
||||
AccountID string `json:"accountID"`
|
||||
ProvisionerID string `json:"provisionerID"`
|
||||
Identifiers []acme.Identifier `json:"identifiers"`
|
||||
AuthorizationIDs []string `json:"authorizationIDs"`
|
||||
Status acme.Status `json:"status"`
|
||||
NotBefore time.Time `json:"notBefore,omitempty"`
|
||||
NotAfter time.Time `json:"notAfter,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ExpiresAt time.Time `json:"expiresAt,omitempty"`
|
||||
CertificateID string `json:"certificate,omitempty"`
|
||||
Error *acme.Error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (a *dbOrder) clone() *dbOrder {
|
||||
b := *a
|
||||
return &b
|
||||
}
|
||||
|
||||
// getDBOrder retrieves and unmarshals an ACME Order type from the database.
|
||||
func (db *DB) getDBOrder(_ context.Context, id string) (*dbOrder, error) {
|
||||
b, err := db.db.Get(orderTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, acme.NewError(acme.ErrorMalformedType, "order %s not found", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading order %s", id)
|
||||
}
|
||||
o := new(dbOrder)
|
||||
if err := json.Unmarshal(b, &o); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling order %s into dbOrder", id)
|
||||
}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// GetOrder retrieves an ACME Order from the database.
|
||||
func (db *DB) GetOrder(ctx context.Context, id string) (*acme.Order, error) {
|
||||
dbo, err := db.getDBOrder(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o := &acme.Order{
|
||||
ID: dbo.ID,
|
||||
AccountID: dbo.AccountID,
|
||||
ProvisionerID: dbo.ProvisionerID,
|
||||
CertificateID: dbo.CertificateID,
|
||||
Status: dbo.Status,
|
||||
ExpiresAt: dbo.ExpiresAt,
|
||||
Identifiers: dbo.Identifiers,
|
||||
NotBefore: dbo.NotBefore,
|
||||
NotAfter: dbo.NotAfter,
|
||||
AuthorizationIDs: dbo.AuthorizationIDs,
|
||||
Error: dbo.Error,
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// CreateOrder creates ACME Order resources and saves them to the DB.
|
||||
func (db *DB) CreateOrder(ctx context.Context, o *acme.Order) error {
|
||||
var err error
|
||||
o.ID, err = randID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := clock.Now()
|
||||
dbo := &dbOrder{
|
||||
ID: o.ID,
|
||||
AccountID: o.AccountID,
|
||||
ProvisionerID: o.ProvisionerID,
|
||||
Status: o.Status,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: o.ExpiresAt,
|
||||
Identifiers: o.Identifiers,
|
||||
NotBefore: o.NotBefore,
|
||||
NotAfter: o.NotAfter,
|
||||
AuthorizationIDs: o.AuthorizationIDs,
|
||||
}
|
||||
if err := db.save(ctx, o.ID, dbo, nil, "order", orderTable); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.updateAddOrderIDs(ctx, o.AccountID, o.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateOrder saves an updated ACME Order to the database.
|
||||
func (db *DB) UpdateOrder(ctx context.Context, o *acme.Order) error {
|
||||
old, err := db.getDBOrder(ctx, o.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nu := old.clone()
|
||||
|
||||
nu.Status = o.Status
|
||||
nu.Error = o.Error
|
||||
nu.CertificateID = o.CertificateID
|
||||
return db.save(ctx, old.ID, nu, old, "order", orderTable)
|
||||
}
|
||||
|
||||
func (db *DB) updateAddOrderIDs(ctx context.Context, accID string, addOids ...string) ([]string, error) {
|
||||
ordersByAccountMux.Lock()
|
||||
defer ordersByAccountMux.Unlock()
|
||||
|
||||
var oldOids []string
|
||||
b, err := db.db.Get(ordersByAccountIDTable, []byte(accID))
|
||||
if err != nil {
|
||||
if !nosql.IsErrNotFound(err) {
|
||||
return nil, errors.Wrapf(err, "error loading orderIDs for account %s", accID)
|
||||
}
|
||||
} else {
|
||||
if err := json.Unmarshal(b, &oldOids); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling orderIDs for account %s", accID)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any order that is not in PENDING state and update the stored list
|
||||
// before returning.
|
||||
//
|
||||
// According to RFC 8555:
|
||||
// The server SHOULD include pending orders and SHOULD NOT include orders
|
||||
// that are invalid in the array of URLs.
|
||||
pendOids := []string{}
|
||||
for _, oid := range oldOids {
|
||||
o, err := db.GetOrder(ctx, oid)
|
||||
if err != nil {
|
||||
return nil, acme.WrapErrorISE(err, "error loading order %s for account %s", oid, accID)
|
||||
}
|
||||
if err = o.UpdateStatus(ctx, db); err != nil {
|
||||
return nil, acme.WrapErrorISE(err, "error updating order %s for account %s", oid, accID)
|
||||
}
|
||||
if o.Status == acme.StatusPending {
|
||||
pendOids = append(pendOids, oid)
|
||||
}
|
||||
}
|
||||
pendOids = append(pendOids, addOids...)
|
||||
var (
|
||||
_old interface{} = oldOids
|
||||
_new interface{} = pendOids
|
||||
)
|
||||
switch {
|
||||
case len(oldOids) == 0 && len(pendOids) == 0:
|
||||
// If list has not changed from empty, then no need to write the DB.
|
||||
return []string{}, nil
|
||||
case len(oldOids) == 0:
|
||||
_old = nil
|
||||
case len(pendOids) == 0:
|
||||
_new = nil
|
||||
}
|
||||
if err = db.save(ctx, accID, _new, _old, "orderIDsByAccountID", ordersByAccountIDTable); err != nil {
|
||||
// Delete all orders that may have been previously stored if orderIDsByAccountID update fails.
|
||||
for _, oid := range addOids {
|
||||
// Ignore error from delete -- we tried our best.
|
||||
// TODO when we have logging w/ request ID tracking, logging this error.
|
||||
db.db.Del(orderTable, []byte(oid))
|
||||
}
|
||||
return nil, errors.Wrapf(err, "error saving orderIDs index for account %s", accID)
|
||||
}
|
||||
return pendOids, nil
|
||||
}
|
||||
|
||||
// GetOrdersByAccountID returns a list of order IDs owned by the account.
|
||||
func (db *DB) GetOrdersByAccountID(ctx context.Context, accID string) ([]string, error) {
|
||||
return db.updateAddOrderIDs(ctx, accID)
|
||||
}
|
File diff suppressed because it is too large
Load diff
410
acme/errors.go
410
acme/errors.go
|
@ -1,410 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
)
|
||||
|
||||
// ProblemType is the type of the ACME problem.
|
||||
type ProblemType int
|
||||
|
||||
const (
|
||||
// ErrorAccountDoesNotExistType request specified an account that does not exist
|
||||
ErrorAccountDoesNotExistType ProblemType = iota
|
||||
// ErrorAlreadyRevokedType request specified a certificate to be revoked that has already been revoked
|
||||
ErrorAlreadyRevokedType
|
||||
// ErrorBadAttestationStatementType WebAuthn attestation statement could not be verified
|
||||
ErrorBadAttestationStatementType
|
||||
// ErrorBadCSRType CSR is unacceptable (e.g., due to a short key)
|
||||
ErrorBadCSRType
|
||||
// ErrorBadNonceType client sent an unacceptable anti-replay nonce
|
||||
ErrorBadNonceType
|
||||
// ErrorBadPublicKeyType JWS was signed by a public key the server does not support
|
||||
ErrorBadPublicKeyType
|
||||
// ErrorBadRevocationReasonType revocation reason provided is not allowed by the server
|
||||
ErrorBadRevocationReasonType
|
||||
// ErrorBadSignatureAlgorithmType JWS was signed with an algorithm the server does not support
|
||||
ErrorBadSignatureAlgorithmType
|
||||
// ErrorCaaType Authority Authorization (CAA) records forbid the CA from issuing a certificate
|
||||
ErrorCaaType
|
||||
// ErrorCompoundType error conditions are indicated in the “subproblems” array.
|
||||
ErrorCompoundType
|
||||
// ErrorConnectionType server could not connect to validation target
|
||||
ErrorConnectionType
|
||||
// ErrorDNSType was a problem with a DNS query during identifier validation
|
||||
ErrorDNSType
|
||||
// ErrorExternalAccountRequiredType request must include a value for the “externalAccountBinding” field
|
||||
ErrorExternalAccountRequiredType
|
||||
// ErrorIncorrectResponseType received didn’t match the challenge’s requirements
|
||||
ErrorIncorrectResponseType
|
||||
// ErrorInvalidContactType URL for an account was invalid
|
||||
ErrorInvalidContactType
|
||||
// ErrorMalformedType request message was malformed
|
||||
ErrorMalformedType
|
||||
// ErrorOrderNotReadyType request attempted to finalize an order that is not ready to be finalized
|
||||
ErrorOrderNotReadyType
|
||||
// ErrorRateLimitedType request exceeds a rate limit
|
||||
ErrorRateLimitedType
|
||||
// ErrorRejectedIdentifierType server will not issue certificates for the identifier
|
||||
ErrorRejectedIdentifierType
|
||||
// ErrorServerInternalType server experienced an internal error
|
||||
ErrorServerInternalType
|
||||
// ErrorTLSType server received a TLS error during validation
|
||||
ErrorTLSType
|
||||
// ErrorUnauthorizedType client lacks sufficient authorization
|
||||
ErrorUnauthorizedType
|
||||
// ErrorUnsupportedContactType URL for an account used an unsupported protocol scheme
|
||||
ErrorUnsupportedContactType
|
||||
// ErrorUnsupportedIdentifierType identifier is of an unsupported type
|
||||
ErrorUnsupportedIdentifierType
|
||||
// ErrorUserActionRequiredType the “instance” URL and take actions specified there
|
||||
ErrorUserActionRequiredType
|
||||
// ErrorNotImplementedType operation is not implemented
|
||||
ErrorNotImplementedType
|
||||
)
|
||||
|
||||
// String returns the string representation of the acme problem type,
|
||||
// fulfilling the Stringer interface.
|
||||
func (ap ProblemType) String() string {
|
||||
switch ap {
|
||||
case ErrorAccountDoesNotExistType:
|
||||
return "accountDoesNotExist"
|
||||
case ErrorAlreadyRevokedType:
|
||||
return "alreadyRevoked"
|
||||
case ErrorBadAttestationStatementType:
|
||||
return "badAttestationStatement"
|
||||
case ErrorBadCSRType:
|
||||
return "badCSR"
|
||||
case ErrorBadNonceType:
|
||||
return "badNonce"
|
||||
case ErrorBadPublicKeyType:
|
||||
return "badPublicKey"
|
||||
case ErrorBadRevocationReasonType:
|
||||
return "badRevocationReason"
|
||||
case ErrorBadSignatureAlgorithmType:
|
||||
return "badSignatureAlgorithm"
|
||||
case ErrorCaaType:
|
||||
return "caa"
|
||||
case ErrorCompoundType:
|
||||
return "compound"
|
||||
case ErrorConnectionType:
|
||||
return "connection"
|
||||
case ErrorDNSType:
|
||||
return "dns"
|
||||
case ErrorExternalAccountRequiredType:
|
||||
return "externalAccountRequired"
|
||||
case ErrorInvalidContactType:
|
||||
return "incorrectResponse"
|
||||
case ErrorMalformedType:
|
||||
return "malformed"
|
||||
case ErrorOrderNotReadyType:
|
||||
return "orderNotReady"
|
||||
case ErrorRateLimitedType:
|
||||
return "rateLimited"
|
||||
case ErrorRejectedIdentifierType:
|
||||
return "rejectedIdentifier"
|
||||
case ErrorServerInternalType:
|
||||
return "serverInternal"
|
||||
case ErrorTLSType:
|
||||
return "tls"
|
||||
case ErrorUnauthorizedType:
|
||||
return "unauthorized"
|
||||
case ErrorUnsupportedContactType:
|
||||
return "unsupportedContact"
|
||||
case ErrorUnsupportedIdentifierType:
|
||||
return "unsupportedIdentifier"
|
||||
case ErrorUserActionRequiredType:
|
||||
return "userActionRequired"
|
||||
case ErrorNotImplementedType:
|
||||
return "notImplemented"
|
||||
default:
|
||||
return fmt.Sprintf("unsupported type ACME error type '%d'", int(ap))
|
||||
}
|
||||
}
|
||||
|
||||
type errorMetadata struct {
|
||||
details string
|
||||
status int
|
||||
typ string
|
||||
String string
|
||||
}
|
||||
|
||||
var (
|
||||
officialACMEPrefix = "urn:ietf:params:acme:error:"
|
||||
errorServerInternalMetadata = errorMetadata{
|
||||
typ: officialACMEPrefix + ErrorServerInternalType.String(),
|
||||
details: "The server experienced an internal error",
|
||||
status: 500,
|
||||
}
|
||||
errorMap = map[ProblemType]errorMetadata{
|
||||
ErrorAccountDoesNotExistType: {
|
||||
typ: officialACMEPrefix + ErrorAccountDoesNotExistType.String(),
|
||||
details: "Account does not exist",
|
||||
status: 400,
|
||||
},
|
||||
ErrorAlreadyRevokedType: {
|
||||
typ: officialACMEPrefix + ErrorAlreadyRevokedType.String(),
|
||||
details: "Certificate already revoked",
|
||||
status: 400,
|
||||
},
|
||||
ErrorBadCSRType: {
|
||||
typ: officialACMEPrefix + ErrorBadCSRType.String(),
|
||||
details: "The CSR is unacceptable",
|
||||
status: 400,
|
||||
},
|
||||
ErrorBadNonceType: {
|
||||
typ: officialACMEPrefix + ErrorBadNonceType.String(),
|
||||
details: "Unacceptable anti-replay nonce",
|
||||
status: 400,
|
||||
},
|
||||
ErrorBadPublicKeyType: {
|
||||
typ: officialACMEPrefix + ErrorBadPublicKeyType.String(),
|
||||
details: "The jws was signed by a public key the server does not support",
|
||||
status: 400,
|
||||
},
|
||||
ErrorBadRevocationReasonType: {
|
||||
typ: officialACMEPrefix + ErrorBadRevocationReasonType.String(),
|
||||
details: "The revocation reason provided is not allowed by the server",
|
||||
status: 400,
|
||||
},
|
||||
ErrorBadSignatureAlgorithmType: {
|
||||
typ: officialACMEPrefix + ErrorBadSignatureAlgorithmType.String(),
|
||||
details: "The JWS was signed with an algorithm the server does not support",
|
||||
status: 400,
|
||||
},
|
||||
ErrorBadAttestationStatementType: {
|
||||
typ: officialACMEPrefix + ErrorBadAttestationStatementType.String(),
|
||||
details: "Attestation statement cannot be verified",
|
||||
status: 400,
|
||||
},
|
||||
ErrorCaaType: {
|
||||
typ: officialACMEPrefix + ErrorCaaType.String(),
|
||||
details: "Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate",
|
||||
status: 400,
|
||||
},
|
||||
ErrorCompoundType: {
|
||||
typ: officialACMEPrefix + ErrorCompoundType.String(),
|
||||
details: "Specific error conditions are indicated in the “subproblems” array",
|
||||
status: 400,
|
||||
},
|
||||
ErrorConnectionType: {
|
||||
typ: officialACMEPrefix + ErrorConnectionType.String(),
|
||||
details: "The server could not connect to validation target",
|
||||
status: 400,
|
||||
},
|
||||
ErrorDNSType: {
|
||||
typ: officialACMEPrefix + ErrorDNSType.String(),
|
||||
details: "There was a problem with a DNS query during identifier validation",
|
||||
status: 400,
|
||||
},
|
||||
ErrorExternalAccountRequiredType: {
|
||||
typ: officialACMEPrefix + ErrorExternalAccountRequiredType.String(),
|
||||
details: "The request must include a value for the \"externalAccountBinding\" field",
|
||||
status: 400,
|
||||
},
|
||||
ErrorIncorrectResponseType: {
|
||||
typ: officialACMEPrefix + ErrorIncorrectResponseType.String(),
|
||||
details: "Response received didn't match the challenge's requirements",
|
||||
status: 400,
|
||||
},
|
||||
ErrorInvalidContactType: {
|
||||
typ: officialACMEPrefix + ErrorInvalidContactType.String(),
|
||||
details: "A contact URL for an account was invalid",
|
||||
status: 400,
|
||||
},
|
||||
ErrorMalformedType: {
|
||||
typ: officialACMEPrefix + ErrorMalformedType.String(),
|
||||
details: "The request message was malformed",
|
||||
status: 400,
|
||||
},
|
||||
ErrorOrderNotReadyType: {
|
||||
typ: officialACMEPrefix + ErrorOrderNotReadyType.String(),
|
||||
details: "The request attempted to finalize an order that is not ready to be finalized",
|
||||
status: 400,
|
||||
},
|
||||
ErrorRateLimitedType: {
|
||||
typ: officialACMEPrefix + ErrorRateLimitedType.String(),
|
||||
details: "The request exceeds a rate limit",
|
||||
status: 400,
|
||||
},
|
||||
ErrorRejectedIdentifierType: {
|
||||
typ: officialACMEPrefix + ErrorRejectedIdentifierType.String(),
|
||||
details: "The server will not issue certificates for the identifier",
|
||||
status: 400,
|
||||
},
|
||||
ErrorNotImplementedType: {
|
||||
typ: officialACMEPrefix + ErrorRejectedIdentifierType.String(),
|
||||
details: "The requested operation is not implemented",
|
||||
status: 501,
|
||||
},
|
||||
ErrorTLSType: {
|
||||
typ: officialACMEPrefix + ErrorTLSType.String(),
|
||||
details: "The server received a TLS error during validation",
|
||||
status: 400,
|
||||
},
|
||||
ErrorUnauthorizedType: {
|
||||
typ: officialACMEPrefix + ErrorUnauthorizedType.String(),
|
||||
details: "The client lacks sufficient authorization",
|
||||
status: 401,
|
||||
},
|
||||
ErrorUnsupportedContactType: {
|
||||
typ: officialACMEPrefix + ErrorUnsupportedContactType.String(),
|
||||
details: "A contact URL for an account used an unsupported protocol scheme",
|
||||
status: 400,
|
||||
},
|
||||
ErrorUnsupportedIdentifierType: {
|
||||
typ: officialACMEPrefix + ErrorUnsupportedIdentifierType.String(),
|
||||
details: "An identifier is of an unsupported type",
|
||||
status: 400,
|
||||
},
|
||||
ErrorUserActionRequiredType: {
|
||||
typ: officialACMEPrefix + ErrorUserActionRequiredType.String(),
|
||||
details: "Visit the “instance” URL and take actions specified there",
|
||||
status: 400,
|
||||
},
|
||||
ErrorServerInternalType: errorServerInternalMetadata,
|
||||
}
|
||||
)
|
||||
|
||||
// Error represents an ACME Error
|
||||
type Error struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
Subproblems []Subproblem `json:"subproblems,omitempty"`
|
||||
Err error `json:"-"`
|
||||
Status int `json:"-"`
|
||||
}
|
||||
|
||||
// Subproblem represents an ACME subproblem. It's fairly
|
||||
// similar to an ACME error, but differs in that it can't
|
||||
// include subproblems itself, the error is reflected
|
||||
// in the Detail property and doesn't have a Status.
|
||||
type Subproblem struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
// The "identifier" field MUST NOT be present at the top level in ACME
|
||||
// problem documents. It can only be present in subproblems.
|
||||
// Subproblems need not all have the same type, and they do not need to
|
||||
// match the top level type.
|
||||
Identifier *Identifier `json:"identifier,omitempty"`
|
||||
}
|
||||
|
||||
// AddSubproblems adds the Subproblems to Error. It
|
||||
// returns the Error, allowing for fluent addition.
|
||||
func (e *Error) AddSubproblems(subproblems ...Subproblem) *Error {
|
||||
e.Subproblems = append(e.Subproblems, subproblems...)
|
||||
return e
|
||||
}
|
||||
|
||||
// NewError creates a new Error type.
|
||||
func NewError(pt ProblemType, msg string, args ...interface{}) *Error {
|
||||
return newError(pt, errors.Errorf(msg, args...))
|
||||
}
|
||||
|
||||
// NewSubproblem creates a new Subproblem. The msg and args
|
||||
// are used to create a new error, which is set as the Detail, allowing
|
||||
// for more detailed error messages to be returned to the ACME client.
|
||||
func NewSubproblem(pt ProblemType, msg string, args ...interface{}) Subproblem {
|
||||
e := newError(pt, fmt.Errorf(msg, args...))
|
||||
s := Subproblem{
|
||||
Type: e.Type,
|
||||
Detail: e.Err.Error(),
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// NewSubproblemWithIdentifier creates a new Subproblem with a specific ACME
|
||||
// Identifier. It calls NewSubproblem and sets the Identifier.
|
||||
func NewSubproblemWithIdentifier(pt ProblemType, identifier Identifier, msg string, args ...interface{}) Subproblem {
|
||||
s := NewSubproblem(pt, msg, args...)
|
||||
s.Identifier = &identifier
|
||||
return s
|
||||
}
|
||||
|
||||
func newError(pt ProblemType, err error) *Error {
|
||||
meta, ok := errorMap[pt]
|
||||
if !ok {
|
||||
meta = errorServerInternalMetadata
|
||||
return &Error{
|
||||
Type: meta.typ,
|
||||
Detail: meta.details,
|
||||
Status: meta.status,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return &Error{
|
||||
Type: meta.typ,
|
||||
Detail: meta.details,
|
||||
Status: meta.status,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorISE creates a new ErrorServerInternalType Error.
|
||||
func NewErrorISE(msg string, args ...interface{}) *Error {
|
||||
return NewError(ErrorServerInternalType, msg, args...)
|
||||
}
|
||||
|
||||
// WrapError attempts to wrap the internal error.
|
||||
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
||||
var e *Error
|
||||
switch {
|
||||
case err == nil:
|
||||
return nil
|
||||
case errors.As(err, &e):
|
||||
if e.Err == nil {
|
||||
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
||||
} else {
|
||||
e.Err = errors.Wrapf(e.Err, msg, args...)
|
||||
}
|
||||
return e
|
||||
default:
|
||||
return newError(typ, errors.Wrapf(err, msg, args...))
|
||||
}
|
||||
}
|
||||
|
||||
// WrapErrorISE shortcut to wrap an internal server error type.
|
||||
func WrapErrorISE(err error, msg string, args ...interface{}) *Error {
|
||||
return WrapError(ErrorServerInternalType, err, msg, args...)
|
||||
}
|
||||
|
||||
// StatusCode returns the status code and implements the StatusCoder interface.
|
||||
func (e *Error) StatusCode() int {
|
||||
return e.Status
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *Error) Error() string {
|
||||
if e.Err == nil {
|
||||
return e.Detail
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// Cause returns the internal error and implements the Causer interface.
|
||||
func (e *Error) Cause() error {
|
||||
if e.Err == nil {
|
||||
return errors.New(e.Detail)
|
||||
}
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// ToLog implements the EnableLogger interface.
|
||||
func (e *Error) ToLog() (interface{}, error) {
|
||||
b, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return nil, WrapErrorISE(err, "error marshaling acme.Error for logging")
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// Render implements render.RenderableError for Error.
|
||||
func (e *Error) Render(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/problem+json")
|
||||
render.JSONStatus(w, e, e.StatusCode())
|
||||
}
|
263
acme/linker.go
263
acme/linker.go
|
@ -1,263 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
// LinkType captures the link type.
|
||||
type LinkType int
|
||||
|
||||
const (
|
||||
// NewNonceLinkType new-nonce
|
||||
NewNonceLinkType LinkType = iota
|
||||
// NewAccountLinkType new-account
|
||||
NewAccountLinkType
|
||||
// AccountLinkType account
|
||||
AccountLinkType
|
||||
// OrderLinkType order
|
||||
OrderLinkType
|
||||
// NewOrderLinkType new-order
|
||||
NewOrderLinkType
|
||||
// OrdersByAccountLinkType list of orders owned by account
|
||||
OrdersByAccountLinkType
|
||||
// FinalizeLinkType finalize order
|
||||
FinalizeLinkType
|
||||
// NewAuthzLinkType authz
|
||||
NewAuthzLinkType
|
||||
// AuthzLinkType new-authz
|
||||
AuthzLinkType
|
||||
// ChallengeLinkType challenge
|
||||
ChallengeLinkType
|
||||
// CertificateLinkType certificate
|
||||
CertificateLinkType
|
||||
// DirectoryLinkType directory
|
||||
DirectoryLinkType
|
||||
// RevokeCertLinkType revoke certificate
|
||||
RevokeCertLinkType
|
||||
// KeyChangeLinkType key rollover
|
||||
KeyChangeLinkType
|
||||
)
|
||||
|
||||
func (l LinkType) String() string {
|
||||
switch l {
|
||||
case NewNonceLinkType:
|
||||
return "new-nonce"
|
||||
case NewAccountLinkType:
|
||||
return "new-account"
|
||||
case AccountLinkType:
|
||||
return "account"
|
||||
case NewOrderLinkType:
|
||||
return "new-order"
|
||||
case OrderLinkType:
|
||||
return "order"
|
||||
case NewAuthzLinkType:
|
||||
return "new-authz"
|
||||
case AuthzLinkType:
|
||||
return "authz"
|
||||
case ChallengeLinkType:
|
||||
return "challenge"
|
||||
case CertificateLinkType:
|
||||
return "certificate"
|
||||
case DirectoryLinkType:
|
||||
return "directory"
|
||||
case RevokeCertLinkType:
|
||||
return "revoke-cert"
|
||||
case KeyChangeLinkType:
|
||||
return "key-change"
|
||||
default:
|
||||
return fmt.Sprintf("unexpected LinkType '%d'", int(l))
|
||||
}
|
||||
}
|
||||
|
||||
func GetUnescapedPathSuffix(typ LinkType, provisionerName string, inputs ...string) string {
|
||||
switch typ {
|
||||
case NewNonceLinkType, NewAccountLinkType, NewOrderLinkType, NewAuthzLinkType, DirectoryLinkType, KeyChangeLinkType, RevokeCertLinkType:
|
||||
return fmt.Sprintf("/%s/%s", provisionerName, typ)
|
||||
case AccountLinkType, OrderLinkType, AuthzLinkType, CertificateLinkType:
|
||||
return fmt.Sprintf("/%s/%s/%s", provisionerName, typ, inputs[0])
|
||||
case ChallengeLinkType:
|
||||
return fmt.Sprintf("/%s/%s/%s/%s", provisionerName, typ, inputs[0], inputs[1])
|
||||
case OrdersByAccountLinkType:
|
||||
return fmt.Sprintf("/%s/%s/%s/orders", provisionerName, AccountLinkType, inputs[0])
|
||||
case FinalizeLinkType:
|
||||
return fmt.Sprintf("/%s/%s/%s/finalize", provisionerName, OrderLinkType, inputs[0])
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// NewLinker returns a new Directory type.
|
||||
func NewLinker(dns, prefix string) Linker {
|
||||
_, _, err := net.SplitHostPort(dns)
|
||||
if err != nil && strings.Contains(err.Error(), "too many colons in address") {
|
||||
// this is most probably an IPv6 without brackets, e.g. ::1, 2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
||||
// in case a port was appended to this wrong format, we try to extract the port, then check if it's
|
||||
// still a valid IPv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334:8443 (8443 is the port). If none of
|
||||
// these cases, then the input dns is not changed.
|
||||
lastIndex := strings.LastIndex(dns, ":")
|
||||
hostPart, portPart := dns[:lastIndex], dns[lastIndex+1:]
|
||||
if ip := net.ParseIP(hostPart); ip != nil {
|
||||
dns = "[" + hostPart + "]:" + portPart
|
||||
} else if ip := net.ParseIP(dns); ip != nil {
|
||||
dns = "[" + dns + "]"
|
||||
}
|
||||
}
|
||||
return &linker{prefix: prefix, dns: dns}
|
||||
}
|
||||
|
||||
// Linker interface for generating links for ACME resources.
|
||||
type Linker interface {
|
||||
GetLink(ctx context.Context, typ LinkType, inputs ...string) string
|
||||
Middleware(http.Handler) http.Handler
|
||||
LinkOrder(ctx context.Context, o *Order)
|
||||
LinkAccount(ctx context.Context, o *Account)
|
||||
LinkChallenge(ctx context.Context, o *Challenge, azID string)
|
||||
LinkAuthorization(ctx context.Context, o *Authorization)
|
||||
LinkOrdersByAccountID(ctx context.Context, orders []string)
|
||||
}
|
||||
|
||||
type linkerKey struct{}
|
||||
|
||||
// NewLinkerContext adds the given linker to the context.
|
||||
func NewLinkerContext(ctx context.Context, v Linker) context.Context {
|
||||
return context.WithValue(ctx, linkerKey{}, v)
|
||||
}
|
||||
|
||||
// LinkerFromContext returns the current linker from the given context.
|
||||
func LinkerFromContext(ctx context.Context) (v Linker, ok bool) {
|
||||
v, ok = ctx.Value(linkerKey{}).(Linker)
|
||||
return
|
||||
}
|
||||
|
||||
// MustLinkerFromContext returns the current linker from the given context. It
|
||||
// will panic if it's not in the context.
|
||||
func MustLinkerFromContext(ctx context.Context) Linker {
|
||||
if v, ok := LinkerFromContext(ctx); !ok {
|
||||
panic("acme linker is not the context")
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
type baseURLKey struct{}
|
||||
|
||||
func newBaseURLContext(ctx context.Context, r *http.Request) context.Context {
|
||||
var u *url.URL
|
||||
if r.Host != "" {
|
||||
u = &url.URL{Scheme: "https", Host: r.Host}
|
||||
}
|
||||
return context.WithValue(ctx, baseURLKey{}, u)
|
||||
}
|
||||
|
||||
func baseURLFromContext(ctx context.Context) *url.URL {
|
||||
if u, ok := ctx.Value(baseURLKey{}).(*url.URL); ok {
|
||||
return u
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// linker generates ACME links.
|
||||
type linker struct {
|
||||
prefix string
|
||||
dns string
|
||||
}
|
||||
|
||||
// Middleware gets the provisioner and current url from the request and sets
|
||||
// them in the context so we can use the linker to create ACME links.
|
||||
func (l *linker) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Add base url to the context.
|
||||
ctx := newBaseURLContext(r.Context(), r)
|
||||
|
||||
// Add provisioner to the context.
|
||||
nameEscaped := chi.URLParam(r, "provisionerID")
|
||||
name, err := url.PathUnescape(nameEscaped)
|
||||
if err != nil {
|
||||
render.Error(w, WrapErrorISE(err, "error url unescaping provisioner name '%s'", nameEscaped))
|
||||
return
|
||||
}
|
||||
|
||||
p, err := authority.MustFromContext(ctx).LoadProvisionerByName(name)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
acmeProv, ok := p.(*provisioner.ACME)
|
||||
if !ok {
|
||||
render.Error(w, NewError(ErrorAccountDoesNotExistType, "provisioner must be of type ACME"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx = NewProvisionerContext(ctx, Provisioner(acmeProv))
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// GetLink is a helper for GetLinkExplicit.
|
||||
func (l *linker) GetLink(ctx context.Context, typ LinkType, inputs ...string) string {
|
||||
var name string
|
||||
if p, ok := ProvisionerFromContext(ctx); ok {
|
||||
name = p.GetName()
|
||||
}
|
||||
|
||||
var u url.URL
|
||||
if baseURL := baseURLFromContext(ctx); baseURL != nil {
|
||||
u = *baseURL
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "https"
|
||||
}
|
||||
if u.Host == "" {
|
||||
u.Host = l.dns
|
||||
}
|
||||
|
||||
u.Path = l.prefix + GetUnescapedPathSuffix(typ, name, inputs...)
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// LinkOrder sets the ACME links required by an ACME order.
|
||||
func (l *linker) LinkOrder(ctx context.Context, o *Order) {
|
||||
o.AuthorizationURLs = make([]string, len(o.AuthorizationIDs))
|
||||
for i, azID := range o.AuthorizationIDs {
|
||||
o.AuthorizationURLs[i] = l.GetLink(ctx, AuthzLinkType, azID)
|
||||
}
|
||||
o.FinalizeURL = l.GetLink(ctx, FinalizeLinkType, o.ID)
|
||||
if o.CertificateID != "" {
|
||||
o.CertificateURL = l.GetLink(ctx, CertificateLinkType, o.CertificateID)
|
||||
}
|
||||
}
|
||||
|
||||
// LinkAccount sets the ACME links required by an ACME account.
|
||||
func (l *linker) LinkAccount(ctx context.Context, acc *Account) {
|
||||
acc.OrdersURL = l.GetLink(ctx, OrdersByAccountLinkType, acc.ID)
|
||||
}
|
||||
|
||||
// LinkChallenge sets the ACME links required by an ACME challenge.
|
||||
func (l *linker) LinkChallenge(ctx context.Context, ch *Challenge, azID string) {
|
||||
ch.URL = l.GetLink(ctx, ChallengeLinkType, azID, ch.ID)
|
||||
}
|
||||
|
||||
// LinkAuthorization sets the ACME links required by an ACME authorization.
|
||||
func (l *linker) LinkAuthorization(ctx context.Context, az *Authorization) {
|
||||
for _, ch := range az.Challenges {
|
||||
l.LinkChallenge(ctx, ch, az.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// LinkOrdersByAccountID converts each order ID to an ACME link.
|
||||
func (l *linker) LinkOrdersByAccountID(ctx context.Context, orders []string) {
|
||||
for i, id := range orders {
|
||||
orders[i] = l.GetLink(ctx, OrderLinkType, id)
|
||||
}
|
||||
}
|
|
@ -1,380 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
func mockProvisioner(t *testing.T) Provisioner {
|
||||
t.Helper()
|
||||
var defaultDisableRenewal = false
|
||||
|
||||
// Initialize provisioners
|
||||
p := &provisioner.ACME{
|
||||
Type: "ACME",
|
||||
Name: "test@acme-<test>provisioner.com",
|
||||
}
|
||||
if err := p.Init(provisioner.Config{Claims: provisioner.Claims{
|
||||
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute},
|
||||
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||
DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||
DisableRenewal: &defaultDisableRenewal,
|
||||
}}); err != nil {
|
||||
fmt.Printf("%v", err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func TestGetUnescapedPathSuffix(t *testing.T) {
|
||||
getPath := GetUnescapedPathSuffix
|
||||
|
||||
assert.Equals(t, getPath(NewNonceLinkType, "{provisionerID}"), "/{provisionerID}/new-nonce")
|
||||
assert.Equals(t, getPath(DirectoryLinkType, "{provisionerID}"), "/{provisionerID}/directory")
|
||||
assert.Equals(t, getPath(NewAccountLinkType, "{provisionerID}"), "/{provisionerID}/new-account")
|
||||
assert.Equals(t, getPath(AccountLinkType, "{provisionerID}", "{accID}"), "/{provisionerID}/account/{accID}")
|
||||
assert.Equals(t, getPath(KeyChangeLinkType, "{provisionerID}"), "/{provisionerID}/key-change")
|
||||
assert.Equals(t, getPath(NewOrderLinkType, "{provisionerID}"), "/{provisionerID}/new-order")
|
||||
assert.Equals(t, getPath(OrderLinkType, "{provisionerID}", "{ordID}"), "/{provisionerID}/order/{ordID}")
|
||||
assert.Equals(t, getPath(OrdersByAccountLinkType, "{provisionerID}", "{accID}"), "/{provisionerID}/account/{accID}/orders")
|
||||
assert.Equals(t, getPath(FinalizeLinkType, "{provisionerID}", "{ordID}"), "/{provisionerID}/order/{ordID}/finalize")
|
||||
assert.Equals(t, getPath(AuthzLinkType, "{provisionerID}", "{authzID}"), "/{provisionerID}/authz/{authzID}")
|
||||
assert.Equals(t, getPath(ChallengeLinkType, "{provisionerID}", "{authzID}", "{chID}"), "/{provisionerID}/challenge/{authzID}/{chID}")
|
||||
assert.Equals(t, getPath(CertificateLinkType, "{provisionerID}", "{certID}"), "/{provisionerID}/certificate/{certID}")
|
||||
}
|
||||
|
||||
func TestLinker_DNS(t *testing.T) {
|
||||
prov := mockProvisioner(t)
|
||||
escProvName := url.PathEscape(prov.GetName())
|
||||
ctx := NewProvisionerContext(context.Background(), prov)
|
||||
type test struct {
|
||||
name string
|
||||
dns string
|
||||
prefix string
|
||||
expectedDirectoryLink string
|
||||
}
|
||||
tests := []test{
|
||||
{
|
||||
name: "domain",
|
||||
dns: "ca.smallstep.com",
|
||||
prefix: "acme",
|
||||
expectedDirectoryLink: fmt.Sprintf("https://ca.smallstep.com/acme/%s/directory", escProvName),
|
||||
},
|
||||
{
|
||||
name: "domain-port",
|
||||
dns: "ca.smallstep.com:8443",
|
||||
prefix: "acme",
|
||||
expectedDirectoryLink: fmt.Sprintf("https://ca.smallstep.com:8443/acme/%s/directory", escProvName),
|
||||
},
|
||||
{
|
||||
name: "ipv4",
|
||||
dns: "127.0.0.1",
|
||||
prefix: "acme",
|
||||
expectedDirectoryLink: fmt.Sprintf("https://127.0.0.1/acme/%s/directory", escProvName),
|
||||
},
|
||||
{
|
||||
name: "ipv4-port",
|
||||
dns: "127.0.0.1:8443",
|
||||
prefix: "acme",
|
||||
expectedDirectoryLink: fmt.Sprintf("https://127.0.0.1:8443/acme/%s/directory", escProvName),
|
||||
},
|
||||
{
|
||||
name: "ipv6",
|
||||
dns: "[::1]",
|
||||
prefix: "acme",
|
||||
expectedDirectoryLink: fmt.Sprintf("https://[::1]/acme/%s/directory", escProvName),
|
||||
},
|
||||
{
|
||||
name: "ipv6-port",
|
||||
dns: "[::1]:8443",
|
||||
prefix: "acme",
|
||||
expectedDirectoryLink: fmt.Sprintf("https://[::1]:8443/acme/%s/directory", escProvName),
|
||||
},
|
||||
{
|
||||
name: "ipv6-no-brackets",
|
||||
dns: "::1",
|
||||
prefix: "acme",
|
||||
expectedDirectoryLink: fmt.Sprintf("https://[::1]/acme/%s/directory", escProvName),
|
||||
},
|
||||
{
|
||||
name: "ipv6-port-no-brackets",
|
||||
dns: "::1:8443",
|
||||
prefix: "acme",
|
||||
expectedDirectoryLink: fmt.Sprintf("https://[::1]:8443/acme/%s/directory", escProvName),
|
||||
},
|
||||
{
|
||||
name: "ipv6-long-no-brackets",
|
||||
dns: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
|
||||
prefix: "acme",
|
||||
expectedDirectoryLink: fmt.Sprintf("https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]/acme/%s/directory", escProvName),
|
||||
},
|
||||
{
|
||||
name: "ipv6-long-port-no-brackets",
|
||||
dns: "2001:0db8:85a3:0000:0000:8a2e:0370:7334:8443",
|
||||
prefix: "acme",
|
||||
expectedDirectoryLink: fmt.Sprintf("https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8443/acme/%s/directory", escProvName),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
linker := NewLinker(tt.dns, tt.prefix)
|
||||
assert.Equals(t, tt.expectedDirectoryLink, linker.GetLink(ctx, DirectoryLinkType))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinker_GetLink(t *testing.T) {
|
||||
dns := "ca.smallstep.com"
|
||||
prefix := "acme"
|
||||
linker := NewLinker(dns, prefix)
|
||||
id := "1234"
|
||||
|
||||
prov := mockProvisioner(t)
|
||||
escProvName := url.PathEscape(prov.GetName())
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
ctx := NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, baseURLKey{}, baseURL)
|
||||
|
||||
// No provisioner and no BaseURL from request
|
||||
assert.Equals(t, linker.GetLink(context.Background(), NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", "https://ca.smallstep.com", ""))
|
||||
// Provisioner: yes, BaseURL: no
|
||||
assert.Equals(t, linker.GetLink(context.WithValue(context.Background(), provisionerKey{}, prov), NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", "https://ca.smallstep.com", escProvName))
|
||||
|
||||
// Provisioner: no, BaseURL: yes
|
||||
assert.Equals(t, linker.GetLink(context.WithValue(context.Background(), baseURLKey{}, baseURL), NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", "https://test.ca.smallstep.com", ""))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", baseURL, escProvName))
|
||||
assert.Equals(t, linker.GetLink(ctx, NewNonceLinkType), fmt.Sprintf("%s/acme/%s/new-nonce", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, NewAccountLinkType), fmt.Sprintf("%s/acme/%s/new-account", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, AccountLinkType, id), fmt.Sprintf("%s/acme/%s/account/1234", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, NewOrderLinkType), fmt.Sprintf("%s/acme/%s/new-order", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, OrderLinkType, id), fmt.Sprintf("%s/acme/%s/order/1234", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, OrdersByAccountLinkType, id), fmt.Sprintf("%s/acme/%s/account/1234/orders", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, FinalizeLinkType, id), fmt.Sprintf("%s/acme/%s/order/1234/finalize", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, NewAuthzLinkType), fmt.Sprintf("%s/acme/%s/new-authz", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, AuthzLinkType, id), fmt.Sprintf("%s/acme/%s/authz/1234", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, DirectoryLinkType), fmt.Sprintf("%s/acme/%s/directory", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, RevokeCertLinkType, id), fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, KeyChangeLinkType), fmt.Sprintf("%s/acme/%s/key-change", baseURL, escProvName))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, ChallengeLinkType, id, id), fmt.Sprintf("%s/acme/%s/challenge/%s/%s", baseURL, escProvName, id, id))
|
||||
|
||||
assert.Equals(t, linker.GetLink(ctx, CertificateLinkType, id), fmt.Sprintf("%s/acme/%s/certificate/1234", baseURL, escProvName))
|
||||
}
|
||||
|
||||
func TestLinker_LinkOrder(t *testing.T) {
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
prov := mockProvisioner(t)
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
ctx := NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, baseURLKey{}, baseURL)
|
||||
|
||||
oid := "orderID"
|
||||
certID := "certID"
|
||||
linkerPrefix := "acme"
|
||||
l := NewLinker("dns", linkerPrefix)
|
||||
type test struct {
|
||||
o *Order
|
||||
validate func(o *Order)
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"no-authz-and-no-cert": {
|
||||
o: &Order{
|
||||
ID: oid,
|
||||
},
|
||||
validate: func(o *Order) {
|
||||
assert.Equals(t, o.FinalizeURL, fmt.Sprintf("%s/%s/%s/order/%s/finalize", baseURL, linkerPrefix, provName, oid))
|
||||
assert.Equals(t, o.AuthorizationURLs, []string{})
|
||||
assert.Equals(t, o.CertificateURL, "")
|
||||
},
|
||||
},
|
||||
"one-authz-and-cert": {
|
||||
o: &Order{
|
||||
ID: oid,
|
||||
CertificateID: certID,
|
||||
AuthorizationIDs: []string{"foo"},
|
||||
},
|
||||
validate: func(o *Order) {
|
||||
assert.Equals(t, o.FinalizeURL, fmt.Sprintf("%s/%s/%s/order/%s/finalize", baseURL, linkerPrefix, provName, oid))
|
||||
assert.Equals(t, o.AuthorizationURLs, []string{
|
||||
fmt.Sprintf("%s/%s/%s/authz/%s", baseURL, linkerPrefix, provName, "foo"),
|
||||
})
|
||||
assert.Equals(t, o.CertificateURL, fmt.Sprintf("%s/%s/%s/certificate/%s", baseURL, linkerPrefix, provName, certID))
|
||||
},
|
||||
},
|
||||
"many-authz": {
|
||||
o: &Order{
|
||||
ID: oid,
|
||||
CertificateID: certID,
|
||||
AuthorizationIDs: []string{"foo", "bar", "zap"},
|
||||
},
|
||||
validate: func(o *Order) {
|
||||
assert.Equals(t, o.FinalizeURL, fmt.Sprintf("%s/%s/%s/order/%s/finalize", baseURL, linkerPrefix, provName, oid))
|
||||
assert.Equals(t, o.AuthorizationURLs, []string{
|
||||
fmt.Sprintf("%s/%s/%s/authz/%s", baseURL, linkerPrefix, provName, "foo"),
|
||||
fmt.Sprintf("%s/%s/%s/authz/%s", baseURL, linkerPrefix, provName, "bar"),
|
||||
fmt.Sprintf("%s/%s/%s/authz/%s", baseURL, linkerPrefix, provName, "zap"),
|
||||
})
|
||||
assert.Equals(t, o.CertificateURL, fmt.Sprintf("%s/%s/%s/certificate/%s", baseURL, linkerPrefix, provName, certID))
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
l.LinkOrder(ctx, tc.o)
|
||||
tc.validate(tc.o)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinker_LinkAccount(t *testing.T) {
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
prov := mockProvisioner(t)
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
ctx := NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, baseURLKey{}, baseURL)
|
||||
|
||||
accID := "accountID"
|
||||
linkerPrefix := "acme"
|
||||
l := NewLinker("dns", linkerPrefix)
|
||||
type test struct {
|
||||
a *Account
|
||||
validate func(o *Account)
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"ok": {
|
||||
a: &Account{
|
||||
ID: accID,
|
||||
},
|
||||
validate: func(a *Account) {
|
||||
assert.Equals(t, a.OrdersURL, fmt.Sprintf("%s/%s/%s/account/%s/orders", baseURL, linkerPrefix, provName, accID))
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
l.LinkAccount(ctx, tc.a)
|
||||
tc.validate(tc.a)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinker_LinkChallenge(t *testing.T) {
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
prov := mockProvisioner(t)
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
ctx := NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, baseURLKey{}, baseURL)
|
||||
|
||||
chID := "chID"
|
||||
azID := "azID"
|
||||
linkerPrefix := "acme"
|
||||
l := NewLinker("dns", linkerPrefix)
|
||||
type test struct {
|
||||
ch *Challenge
|
||||
validate func(o *Challenge)
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"ok": {
|
||||
ch: &Challenge{
|
||||
ID: chID,
|
||||
},
|
||||
validate: func(ch *Challenge) {
|
||||
assert.Equals(t, ch.URL, fmt.Sprintf("%s/%s/%s/challenge/%s/%s", baseURL, linkerPrefix, provName, azID, ch.ID))
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
l.LinkChallenge(ctx, tc.ch, azID)
|
||||
tc.validate(tc.ch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinker_LinkAuthorization(t *testing.T) {
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
prov := mockProvisioner(t)
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
ctx := NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, baseURLKey{}, baseURL)
|
||||
|
||||
chID0 := "chID-0"
|
||||
chID1 := "chID-1"
|
||||
chID2 := "chID-2"
|
||||
azID := "azID"
|
||||
linkerPrefix := "acme"
|
||||
l := NewLinker("dns", linkerPrefix)
|
||||
type test struct {
|
||||
az *Authorization
|
||||
validate func(o *Authorization)
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"ok": {
|
||||
az: &Authorization{
|
||||
ID: azID,
|
||||
Challenges: []*Challenge{
|
||||
{ID: chID0},
|
||||
{ID: chID1},
|
||||
{ID: chID2},
|
||||
},
|
||||
},
|
||||
validate: func(az *Authorization) {
|
||||
assert.Equals(t, az.Challenges[0].URL, fmt.Sprintf("%s/%s/%s/challenge/%s/%s", baseURL, linkerPrefix, provName, az.ID, chID0))
|
||||
assert.Equals(t, az.Challenges[1].URL, fmt.Sprintf("%s/%s/%s/challenge/%s/%s", baseURL, linkerPrefix, provName, az.ID, chID1))
|
||||
assert.Equals(t, az.Challenges[2].URL, fmt.Sprintf("%s/%s/%s/challenge/%s/%s", baseURL, linkerPrefix, provName, az.ID, chID2))
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
l.LinkAuthorization(ctx, tc.az)
|
||||
tc.validate(tc.az)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinker_LinkOrdersByAccountID(t *testing.T) {
|
||||
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
|
||||
prov := mockProvisioner(t)
|
||||
provName := url.PathEscape(prov.GetName())
|
||||
ctx := NewProvisionerContext(context.Background(), prov)
|
||||
ctx = context.WithValue(ctx, baseURLKey{}, baseURL)
|
||||
|
||||
linkerPrefix := "acme"
|
||||
l := NewLinker("dns", linkerPrefix)
|
||||
type test struct {
|
||||
oids []string
|
||||
}
|
||||
var tests = map[string]test{
|
||||
"ok": {
|
||||
oids: []string{"foo", "bar", "baz"},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
l.LinkOrdersByAccountID(ctx, tc.oids)
|
||||
assert.Equals(t, tc.oids, []string{
|
||||
fmt.Sprintf("%s/%s/%s/order/%s", baseURL, linkerPrefix, provName, "foo"),
|
||||
fmt.Sprintf("%s/%s/%s/order/%s", baseURL, linkerPrefix, provName, "bar"),
|
||||
fmt.Sprintf("%s/%s/%s/order/%s", baseURL, linkerPrefix, provName, "baz"),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package acme
|
||||
|
||||
// Nonce represents an ACME nonce type.
|
||||
type Nonce string
|
||||
|
||||
// String implements the ToString interface.
|
||||
func (n Nonce) String() string {
|
||||
return string(n)
|
||||
}
|
460
acme/order.go
460
acme/order.go
|
@ -1,460 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
type IdentifierType string
|
||||
|
||||
const (
|
||||
// IP is the ACME ip identifier type
|
||||
IP IdentifierType = "ip"
|
||||
// DNS is the ACME dns identifier type
|
||||
DNS IdentifierType = "dns"
|
||||
// PermanentIdentifier is the ACME permanent-identifier identifier type
|
||||
// defined in https://datatracker.ietf.org/doc/html/draft-bweeks-acme-device-attest-00
|
||||
PermanentIdentifier IdentifierType = "permanent-identifier"
|
||||
)
|
||||
|
||||
// Identifier encodes the type that an order pertains to.
|
||||
type Identifier struct {
|
||||
Type IdentifierType `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Order contains order metadata for the ACME protocol order type.
|
||||
type Order struct {
|
||||
ID string `json:"id"`
|
||||
AccountID string `json:"-"`
|
||||
ProvisionerID string `json:"-"`
|
||||
Status Status `json:"status"`
|
||||
ExpiresAt time.Time `json:"expires"`
|
||||
Identifiers []Identifier `json:"identifiers"`
|
||||
NotBefore time.Time `json:"notBefore"`
|
||||
NotAfter time.Time `json:"notAfter"`
|
||||
Error *Error `json:"error,omitempty"`
|
||||
AuthorizationIDs []string `json:"-"`
|
||||
AuthorizationURLs []string `json:"authorizations"`
|
||||
FinalizeURL string `json:"finalize"`
|
||||
CertificateID string `json:"-"`
|
||||
CertificateURL string `json:"certificate,omitempty"`
|
||||
}
|
||||
|
||||
// ToLog enables response logging.
|
||||
func (o *Order) ToLog() (interface{}, error) {
|
||||
b, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return nil, WrapErrorISE(err, "error marshaling order for logging")
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// UpdateStatus updates the ACME Order Status if necessary.
|
||||
// Changes to the order are saved using the database interface.
|
||||
func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
|
||||
now := clock.Now()
|
||||
|
||||
switch o.Status {
|
||||
case StatusInvalid:
|
||||
return nil
|
||||
case StatusValid:
|
||||
return nil
|
||||
case StatusReady:
|
||||
// Check expiry
|
||||
if now.After(o.ExpiresAt) {
|
||||
o.Status = StatusInvalid
|
||||
o.Error = NewError(ErrorMalformedType, "order has expired")
|
||||
break
|
||||
}
|
||||
return nil
|
||||
case StatusPending:
|
||||
// Check expiry
|
||||
if now.After(o.ExpiresAt) {
|
||||
o.Status = StatusInvalid
|
||||
o.Error = NewError(ErrorMalformedType, "order has expired")
|
||||
break
|
||||
}
|
||||
|
||||
var count = map[Status]int{
|
||||
StatusValid: 0,
|
||||
StatusInvalid: 0,
|
||||
StatusPending: 0,
|
||||
}
|
||||
for _, azID := range o.AuthorizationIDs {
|
||||
az, err := db.GetAuthorization(ctx, azID)
|
||||
if err != nil {
|
||||
return WrapErrorISE(err, "error getting authorization ID %s", azID)
|
||||
}
|
||||
if err = az.UpdateStatus(ctx, db); err != nil {
|
||||
return WrapErrorISE(err, "error updating authorization ID %s", azID)
|
||||
}
|
||||
st := az.Status
|
||||
count[st]++
|
||||
}
|
||||
switch {
|
||||
case count[StatusInvalid] > 0:
|
||||
o.Status = StatusInvalid
|
||||
|
||||
// No change in the order status, so just return the order as is -
|
||||
// without writing any changes.
|
||||
case count[StatusPending] > 0:
|
||||
return nil
|
||||
|
||||
case count[StatusValid] == len(o.AuthorizationIDs):
|
||||
o.Status = StatusReady
|
||||
|
||||
default:
|
||||
return NewErrorISE("unexpected authz status")
|
||||
}
|
||||
default:
|
||||
return NewErrorISE("unrecognized order status: %s", o.Status)
|
||||
}
|
||||
if err := db.UpdateOrder(ctx, o); err != nil {
|
||||
return WrapErrorISE(err, "error updating order")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getKeyFingerprint returns a fingerprint from the list of authorizations. This
|
||||
// fingerprint is used on the device-attest-01 flow to verify the attestation
|
||||
// certificate public key with the CSR public key.
|
||||
//
|
||||
// There's no point on reading all the authorizations as there will be only one
|
||||
// for a permanent identifier.
|
||||
func (o *Order) getAuthorizationFingerprint(ctx context.Context, db DB) (string, error) {
|
||||
for _, azID := range o.AuthorizationIDs {
|
||||
az, err := db.GetAuthorization(ctx, azID)
|
||||
if err != nil {
|
||||
return "", WrapErrorISE(err, "error getting authorization %q", azID)
|
||||
}
|
||||
// There's no point on reading all the authorizations as there will
|
||||
// be only one for a permanent identifier.
|
||||
if az.Fingerprint != "" {
|
||||
return az.Fingerprint, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Finalize signs a certificate if the necessary conditions for Order completion
|
||||
// have been met.
|
||||
//
|
||||
// TODO(mariano): Here or in the challenge validation we should perform some
|
||||
// external validation using the identifier value and the attestation data. From
|
||||
// a validation service we can get the list of SANs to set in the final
|
||||
// certificate.
|
||||
func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateRequest, auth CertificateAuthority, p Provisioner) error {
|
||||
if err := o.UpdateStatus(ctx, db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch o.Status {
|
||||
case StatusInvalid:
|
||||
return NewError(ErrorOrderNotReadyType, "order %s has been abandoned", o.ID)
|
||||
case StatusValid:
|
||||
return nil
|
||||
case StatusPending:
|
||||
return NewError(ErrorOrderNotReadyType, "order %s is not ready", o.ID)
|
||||
case StatusReady:
|
||||
break
|
||||
default:
|
||||
return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID)
|
||||
}
|
||||
|
||||
// Get key fingerprint if any. And then compare it with the CSR fingerprint.
|
||||
//
|
||||
// In device-attest-01 challenges we should check that the keys in the CSR
|
||||
// and the attestation certificate are the same.
|
||||
fingerprint, err := o.getAuthorizationFingerprint(ctx, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fingerprint != "" {
|
||||
fp, err := keyutil.Fingerprint(csr.PublicKey)
|
||||
if err != nil {
|
||||
return WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(fingerprint), []byte(fp)) == 0 {
|
||||
return NewError(ErrorUnauthorizedType, "order %s csr does not match the attested key", o.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// canonicalize the CSR to allow for comparison
|
||||
csr = canonicalize(csr)
|
||||
|
||||
// Template data
|
||||
data := x509util.NewTemplateData()
|
||||
data.SetCommonName(csr.Subject.CommonName)
|
||||
|
||||
// Custom sign options passed to authority.Sign
|
||||
var extraOptions []provisioner.SignOption
|
||||
|
||||
// TODO: support for multiple identifiers?
|
||||
var permanentIdentifier string
|
||||
for i := range o.Identifiers {
|
||||
if o.Identifiers[i].Type == PermanentIdentifier {
|
||||
permanentIdentifier = o.Identifiers[i].Value
|
||||
// the first (and only) Permanent Identifier that gets added to the certificate
|
||||
// should be equal to the Subject Common Name if it's set. If not equal, the CSR
|
||||
// is rejected, because the Common Name hasn't been challenged in that case. This
|
||||
// could result in unauthorized access if a relying system relies on the Common
|
||||
// Name in its authorization logic.
|
||||
if csr.Subject.CommonName != "" && csr.Subject.CommonName != permanentIdentifier {
|
||||
return NewError(ErrorBadCSRType, "CSR Subject Common Name does not match identifiers exactly: "+
|
||||
"CSR Subject Common Name = %s, Order Permanent Identifier = %s", csr.Subject.CommonName, permanentIdentifier)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var defaultTemplate string
|
||||
if permanentIdentifier != "" {
|
||||
defaultTemplate = x509util.DefaultAttestedLeafTemplate
|
||||
data.SetSubjectAlternativeNames(x509util.SubjectAlternativeName{
|
||||
Type: x509util.PermanentIdentifierType,
|
||||
Value: permanentIdentifier,
|
||||
})
|
||||
extraOptions = append(extraOptions, provisioner.AttestationData{
|
||||
PermanentIdentifier: permanentIdentifier,
|
||||
})
|
||||
} else {
|
||||
defaultTemplate = x509util.DefaultLeafTemplate
|
||||
sans, err := o.sans(csr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data.SetSubjectAlternativeNames(sans...)
|
||||
}
|
||||
|
||||
// Get authorizations from the ACME provisioner.
|
||||
ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)
|
||||
signOps, err := p.AuthorizeSign(ctx, "")
|
||||
if err != nil {
|
||||
return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner")
|
||||
}
|
||||
// Unlike most of the provisioners, ACME's AuthorizeSign method doesn't
|
||||
// define the templates, and the template data used in WebHooks is not
|
||||
// available.
|
||||
for _, signOp := range signOps {
|
||||
if wc, ok := signOp.(*provisioner.WebhookController); ok {
|
||||
wc.TemplateData = data
|
||||
}
|
||||
}
|
||||
|
||||
templateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate)
|
||||
if err != nil {
|
||||
return WrapErrorISE(err, "error creating template options from ACME provisioner")
|
||||
}
|
||||
|
||||
// Build extra signing options.
|
||||
signOps = append(signOps, templateOptions)
|
||||
signOps = append(signOps, extraOptions...)
|
||||
|
||||
// Sign a new certificate.
|
||||
certChain, err := auth.Sign(csr, provisioner.SignOptions{
|
||||
NotBefore: provisioner.NewTimeDuration(o.NotBefore),
|
||||
NotAfter: provisioner.NewTimeDuration(o.NotAfter),
|
||||
}, signOps...)
|
||||
if err != nil {
|
||||
return WrapErrorISE(err, "error signing certificate for order %s", o.ID)
|
||||
}
|
||||
|
||||
cert := &Certificate{
|
||||
AccountID: o.AccountID,
|
||||
OrderID: o.ID,
|
||||
Leaf: certChain[0],
|
||||
Intermediates: certChain[1:],
|
||||
}
|
||||
if err := db.CreateCertificate(ctx, cert); err != nil {
|
||||
return WrapErrorISE(err, "error creating certificate for order %s", o.ID)
|
||||
}
|
||||
|
||||
o.CertificateID = cert.ID
|
||||
o.Status = StatusValid
|
||||
if err = db.UpdateOrder(ctx, o); err != nil {
|
||||
return WrapErrorISE(err, "error updating order %s", o.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativeName, error) {
|
||||
var sans []x509util.SubjectAlternativeName
|
||||
if len(csr.EmailAddresses) > 0 || len(csr.URIs) > 0 {
|
||||
return sans, NewError(ErrorBadCSRType, "Only DNS names and IP addresses are allowed")
|
||||
}
|
||||
|
||||
// order the DNS names and IP addresses, so that they can be compared against the canonicalized CSR
|
||||
orderNames := make([]string, numberOfIdentifierType(DNS, o.Identifiers))
|
||||
orderIPs := make([]net.IP, numberOfIdentifierType(IP, o.Identifiers))
|
||||
orderPIDs := make([]string, numberOfIdentifierType(PermanentIdentifier, o.Identifiers))
|
||||
indexDNS, indexIP, indexPID := 0, 0, 0
|
||||
for _, n := range o.Identifiers {
|
||||
switch n.Type {
|
||||
case DNS:
|
||||
orderNames[indexDNS] = n.Value
|
||||
indexDNS++
|
||||
case IP:
|
||||
orderIPs[indexIP] = net.ParseIP(n.Value) // NOTE: this assumes are all valid IPs at this time; or will result in nil entries
|
||||
indexIP++
|
||||
case PermanentIdentifier:
|
||||
orderPIDs[indexPID] = n.Value
|
||||
indexPID++
|
||||
default:
|
||||
return sans, NewErrorISE("unsupported identifier type in order: %s", n.Type)
|
||||
}
|
||||
}
|
||||
orderNames = uniqueSortedLowerNames(orderNames)
|
||||
orderIPs = uniqueSortedIPs(orderIPs)
|
||||
|
||||
totalNumberOfSANs := len(csr.DNSNames) + len(csr.IPAddresses)
|
||||
sans = make([]x509util.SubjectAlternativeName, totalNumberOfSANs)
|
||||
index := 0
|
||||
|
||||
// Validate identifier names against CSR alternative names.
|
||||
//
|
||||
// Note that with certificate templates we are not going to check for the
|
||||
// absence of other SANs as they will only be set if the template allows
|
||||
// them.
|
||||
if len(csr.DNSNames) != len(orderNames) {
|
||||
return sans, NewError(ErrorBadCSRType, "CSR names do not match identifiers exactly: "+
|
||||
"CSR names = %v, Order names = %v", csr.DNSNames, orderNames)
|
||||
}
|
||||
|
||||
for i := range csr.DNSNames {
|
||||
if csr.DNSNames[i] != orderNames[i] {
|
||||
return sans, NewError(ErrorBadCSRType, "CSR names do not match identifiers exactly: "+
|
||||
"CSR names = %v, Order names = %v", csr.DNSNames, orderNames)
|
||||
}
|
||||
sans[index] = x509util.SubjectAlternativeName{
|
||||
Type: x509util.DNSType,
|
||||
Value: csr.DNSNames[i],
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
if len(csr.IPAddresses) != len(orderIPs) {
|
||||
return sans, NewError(ErrorBadCSRType, "CSR IPs do not match identifiers exactly: "+
|
||||
"CSR IPs = %v, Order IPs = %v", csr.IPAddresses, orderIPs)
|
||||
}
|
||||
|
||||
for i := range csr.IPAddresses {
|
||||
if !ipsAreEqual(csr.IPAddresses[i], orderIPs[i]) {
|
||||
return sans, NewError(ErrorBadCSRType, "CSR IPs do not match identifiers exactly: "+
|
||||
"CSR IPs = %v, Order IPs = %v", csr.IPAddresses, orderIPs)
|
||||
}
|
||||
sans[index] = x509util.SubjectAlternativeName{
|
||||
Type: x509util.IPType,
|
||||
Value: csr.IPAddresses[i].String(),
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
return sans, nil
|
||||
}
|
||||
|
||||
// numberOfIdentifierType returns the number of Identifiers that
|
||||
// are of type typ.
|
||||
func numberOfIdentifierType(typ IdentifierType, ids []Identifier) int {
|
||||
c := 0
|
||||
for _, id := range ids {
|
||||
if id.Type == typ {
|
||||
c++
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// canonicalize canonicalizes a CSR so that it can be compared against an Order
|
||||
// NOTE: this effectively changes the order of SANs in the CSR, which may be OK,
|
||||
// but may not be expected. It also adds a Subject Common Name to either the 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.
|
||||
func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) {
|
||||
// for clarity only; we're operating on the same object by pointer
|
||||
canonicalized = csr
|
||||
|
||||
// RFC8555: The CSR MUST indicate the exact same set of requested
|
||||
// identifiers as the initial newOrder request. Identifiers of type "dns"
|
||||
// MUST appear either in the commonName portion of the requested subject
|
||||
// name or in an extensionRequest attribute [RFC2985] requesting a
|
||||
// subjectAltName extension, or both. Subject Common Names that can be
|
||||
// parsed as an IP are included as an IP address for the equality check.
|
||||
// If these were excluded, a certificate could contain an IP as the
|
||||
// common name without having been challenged.
|
||||
if csr.Subject.CommonName != "" {
|
||||
if ip := net.ParseIP(csr.Subject.CommonName); ip != nil {
|
||||
canonicalized.IPAddresses = append(canonicalized.IPAddresses, ip)
|
||||
} else {
|
||||
canonicalized.DNSNames = append(canonicalized.DNSNames, csr.Subject.CommonName)
|
||||
}
|
||||
}
|
||||
|
||||
canonicalized.DNSNames = uniqueSortedLowerNames(canonicalized.DNSNames)
|
||||
canonicalized.IPAddresses = uniqueSortedIPs(canonicalized.IPAddresses)
|
||||
|
||||
return canonicalized
|
||||
}
|
||||
|
||||
// ipsAreEqual compares IPs to be equal. Nil values (i.e. invalid IPs) are
|
||||
// not considered equal. IPv6 representations of IPv4 addresses are
|
||||
// considered equal to the IPv4 address in this implementation, which is
|
||||
// standard Go behavior. An example is "::ffff:192.168.42.42", which
|
||||
// is equal to "192.168.42.42". This is considered a known issue within
|
||||
// step and is tracked here too: https://github.com/golang/go/issues/37921.
|
||||
func ipsAreEqual(x, y net.IP) bool {
|
||||
if x == nil || y == nil {
|
||||
return false
|
||||
}
|
||||
return x.Equal(y)
|
||||
}
|
||||
|
||||
// uniqueSortedLowerNames returns the set of all unique names in the input after all
|
||||
// of them are lowercased. The returned names will be in their lowercased form
|
||||
// and sorted alphabetically.
|
||||
func uniqueSortedLowerNames(names []string) (unique []string) {
|
||||
nameMap := make(map[string]int, len(names))
|
||||
for _, name := range names {
|
||||
nameMap[strings.ToLower(name)] = 1
|
||||
}
|
||||
unique = make([]string, 0, len(nameMap))
|
||||
for name := range nameMap {
|
||||
unique = append(unique, name)
|
||||
}
|
||||
sort.Strings(unique)
|
||||
return
|
||||
}
|
||||
|
||||
// uniqueSortedIPs returns the set of all unique net.IPs in the input. They
|
||||
// are sorted by their bytes (octet) representation.
|
||||
func uniqueSortedIPs(ips []net.IP) (unique []net.IP) {
|
||||
type entry struct {
|
||||
ip net.IP
|
||||
}
|
||||
ipEntryMap := make(map[string]entry, len(ips))
|
||||
for _, ip := range ips {
|
||||
// reparsing the IP results in the IP being represented using 16 bytes
|
||||
// for both IPv4 as well as IPv6, even when the ips slice contains IPs that
|
||||
// are represented by 4 bytes. This ensures a fair comparison and thus ordering.
|
||||
ipEntryMap[ip.String()] = entry{ip: net.ParseIP(ip.String())}
|
||||
}
|
||||
unique = make([]net.IP, 0, len(ipEntryMap))
|
||||
for _, entry := range ipEntryMap {
|
||||
unique = append(unique, entry.ip)
|
||||
}
|
||||
sort.Slice(unique, func(i, j int) bool {
|
||||
return bytes.Compare(unique[i], unique[j]) < 0
|
||||
})
|
||||
return
|
||||
}
|
1945
acme/order_test.go
1945
acme/order_test.go
File diff suppressed because it is too large
Load diff
|
@ -1,20 +0,0 @@
|
|||
package acme
|
||||
|
||||
// Status represents an ACME status.
|
||||
type Status string
|
||||
|
||||
var (
|
||||
// StatusValid -- valid
|
||||
StatusValid = Status("valid")
|
||||
// StatusInvalid -- invalid
|
||||
StatusInvalid = Status("invalid")
|
||||
// StatusPending -- pending; e.g. an Order that is not ready to be finalized.
|
||||
StatusPending = Status("pending")
|
||||
// StatusDeactivated -- deactivated; e.g. for an Account that is not longer valid.
|
||||
StatusDeactivated = Status("deactivated")
|
||||
// StatusReady -- ready; e.g. for an Order that is ready to be finalized.
|
||||
StatusReady = Status("ready")
|
||||
//statusExpired = "expired"
|
||||
//statusActive = "active"
|
||||
//statusProcessing = "processing"
|
||||
)
|
471
api/api.go
471
api/api.go
|
@ -1,19 +1,10 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/dsa" //nolint:staticcheck // support legacy algorithms
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -21,58 +12,21 @@ import (
|
|||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/smallstep/certificates/api/log"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
"github.com/smallstep/cli/crypto/tlsutil"
|
||||
)
|
||||
|
||||
// Authority is the interface implemented by a CA authority.
|
||||
type Authority interface {
|
||||
SSHAuthority
|
||||
// context specifies the Authorize[Sign|Revoke|etc.] method.
|
||||
Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error)
|
||||
AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error)
|
||||
GetTLSOptions() *config.TLSOptions
|
||||
Authorize(ott string) ([]interface{}, error)
|
||||
GetTLSOptions() *tlsutil.TLSOptions
|
||||
Root(shasum string) (*x509.Certificate, error)
|
||||
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
Renew(peer *x509.Certificate) ([]*x509.Certificate, error)
|
||||
RenewContext(ctx context.Context, peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||
Rekey(peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)
|
||||
LoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)
|
||||
LoadProvisionerByName(string) (provisioner.Interface, error)
|
||||
GetProvisioners(cursor string, limit int) (provisioner.List, string, error)
|
||||
Revoke(context.Context, *authority.RevokeOptions) error
|
||||
Sign(cr *x509.CertificateRequest, signOpts authority.SignOptions, extraOpts ...interface{}) (*x509.Certificate, *x509.Certificate, error)
|
||||
Renew(peer *x509.Certificate) (*x509.Certificate, *x509.Certificate, error)
|
||||
GetProvisioners(cursor string, limit int) ([]*authority.Provisioner, string, error)
|
||||
GetEncryptedKey(kid string) (string, error)
|
||||
GetRoots() ([]*x509.Certificate, error)
|
||||
GetRoots() (federation []*x509.Certificate, err error)
|
||||
GetFederation() ([]*x509.Certificate, error)
|
||||
Version() authority.Version
|
||||
GetCertificateRevocationList() ([]byte, error)
|
||||
}
|
||||
|
||||
// mustAuthority will be replaced on unit tests.
|
||||
var mustAuthority = func(ctx context.Context) Authority {
|
||||
return authority.MustFromContext(ctx)
|
||||
}
|
||||
|
||||
// TimeDuration is an alias of provisioner.TimeDuration
|
||||
type TimeDuration = provisioner.TimeDuration
|
||||
|
||||
// NewTimeDuration returns a TimeDuration with the defined time.
|
||||
func NewTimeDuration(t time.Time) TimeDuration {
|
||||
return provisioner.NewTimeDuration(t)
|
||||
}
|
||||
|
||||
// ParseTimeDuration returns a new TimeDuration parsing the RFC 3339 time or
|
||||
// time.Duration string.
|
||||
func ParseTimeDuration(s string) (TimeDuration, error) {
|
||||
return provisioner.ParseTimeDuration(s)
|
||||
}
|
||||
|
||||
// Certificate wraps a *x509.Certificate and adds the json.Marshaler interface.
|
||||
|
@ -88,13 +42,6 @@ func NewCertificate(cr *x509.Certificate) Certificate {
|
|||
}
|
||||
}
|
||||
|
||||
// reset sets the inner x509.CertificateRequest to nil
|
||||
func (c *Certificate) reset() {
|
||||
if c != nil {
|
||||
c.Certificate = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface. The certificate is
|
||||
// quoted string using the PEM encoding.
|
||||
func (c Certificate) MarshalJSON() ([]byte, error) {
|
||||
|
@ -115,13 +62,6 @@ func (c *Certificate) UnmarshalJSON(data []byte) error {
|
|||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return errors.Wrap(err, "error decoding certificate")
|
||||
}
|
||||
|
||||
// Make sure the inner x509.Certificate is nil
|
||||
if s == "null" || s == "" {
|
||||
c.reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(s))
|
||||
if block == nil {
|
||||
return errors.New("error decoding certificate")
|
||||
|
@ -148,13 +88,6 @@ func NewCertificateRequest(cr *x509.CertificateRequest) CertificateRequest {
|
|||
}
|
||||
}
|
||||
|
||||
// reset sets the inner x509.CertificateRequest to nil
|
||||
func (c *CertificateRequest) reset() {
|
||||
if c != nil {
|
||||
c.CertificateRequest = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface. The certificate request
|
||||
// is a quoted string using the PEM encoding.
|
||||
func (c CertificateRequest) MarshalJSON() ([]byte, error) {
|
||||
|
@ -175,13 +108,6 @@ func (c *CertificateRequest) UnmarshalJSON(data []byte) error {
|
|||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return errors.Wrap(err, "error decoding csr")
|
||||
}
|
||||
|
||||
// Make sure the inner x509.CertificateRequest is nil
|
||||
if s == "null" || s == "" {
|
||||
c.reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(s))
|
||||
if block == nil {
|
||||
return errors.New("error decoding csr")
|
||||
|
@ -207,13 +133,6 @@ type RouterHandler interface {
|
|||
Route(r Router)
|
||||
}
|
||||
|
||||
// VersionResponse is the response object that returns the version of the
|
||||
// server.
|
||||
type VersionResponse struct {
|
||||
Version string `json:"version"`
|
||||
RequireClientAuthentication bool `json:"requireClientAuthentication,omitempty"`
|
||||
}
|
||||
|
||||
// HealthResponse is the response object that returns the health of the server.
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
|
@ -224,50 +143,51 @@ type RootResponse struct {
|
|||
RootPEM Certificate `json:"ca"`
|
||||
}
|
||||
|
||||
// SignRequest is the request body for a certificate signature request.
|
||||
type SignRequest struct {
|
||||
CsrPEM CertificateRequest `json:"csr"`
|
||||
OTT string `json:"ott"`
|
||||
NotAfter time.Time `json:"notAfter"`
|
||||
NotBefore time.Time `json:"notBefore"`
|
||||
}
|
||||
|
||||
// ProvisionersResponse is the response object that returns the list of
|
||||
// provisioners.
|
||||
type ProvisionersResponse struct {
|
||||
Provisioners provisioner.List
|
||||
NextCursor string
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler. It marshals the ProvisionersResponse
|
||||
// into a byte slice.
|
||||
//
|
||||
// Special treatment is given to the SCEP provisioner, as it contains a
|
||||
// challenge secret that MUST NOT be leaked in (public) HTTP responses. The
|
||||
// challenge value is thus redacted in HTTP responses.
|
||||
func (p ProvisionersResponse) MarshalJSON() ([]byte, error) {
|
||||
for _, item := range p.Provisioners {
|
||||
scepProv, ok := item.(*provisioner.SCEP)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
old := scepProv.ChallengePassword
|
||||
scepProv.ChallengePassword = "*** REDACTED ***"
|
||||
defer func(p string) { //nolint:gocritic // defer in loop required to restore initial state of provisioners
|
||||
scepProv.ChallengePassword = p
|
||||
}(old)
|
||||
}
|
||||
|
||||
var list = struct {
|
||||
Provisioners []provisioner.Interface `json:"provisioners"`
|
||||
Provisioners []*authority.Provisioner `json:"provisioners"`
|
||||
NextCursor string `json:"nextCursor"`
|
||||
}{
|
||||
Provisioners: []provisioner.Interface(p.Provisioners),
|
||||
NextCursor: p.NextCursor,
|
||||
}
|
||||
|
||||
return json.Marshal(list)
|
||||
}
|
||||
|
||||
// ProvisionerKeyResponse is the response object that returns the encrypted key
|
||||
// ProvisionerKeyResponse is the response object that returns the encryptoed key
|
||||
// of a provisioner.
|
||||
type ProvisionerKeyResponse struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// Validate checks the fields of the SignRequest and returns nil if they are ok
|
||||
// or an error if something is wrong.
|
||||
func (s *SignRequest) Validate() error {
|
||||
if s.CsrPEM.CertificateRequest == nil {
|
||||
return BadRequest(errors.New("missing csr"))
|
||||
}
|
||||
if err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil {
|
||||
return BadRequest(errors.Wrap(err, "invalid csr"))
|
||||
}
|
||||
if s.OTT == "" {
|
||||
return BadRequest(errors.New("missing ott"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignResponse is the response object of the certificate signature request.
|
||||
type SignResponse struct {
|
||||
ServerPEM Certificate `json:"crt"`
|
||||
CaPEM Certificate `json:"ca"`
|
||||
TLSOptions *tlsutil.TLSOptions `json:"tlsOptions,omitempty"`
|
||||
TLS *tls.ConnectionState `json:"-"`
|
||||
}
|
||||
|
||||
// RootsResponse is the response object of the roots request.
|
||||
type RootsResponse struct {
|
||||
Certificates []Certificate `json:"crts"`
|
||||
|
@ -283,125 +203,142 @@ type caHandler struct {
|
|||
Authority Authority
|
||||
}
|
||||
|
||||
// Route configures the http request router.
|
||||
func (h *caHandler) Route(r Router) {
|
||||
Route(r)
|
||||
}
|
||||
|
||||
// New creates a new RouterHandler with the CA endpoints.
|
||||
//
|
||||
// Deprecated: Use api.Route(r Router)
|
||||
func New(Authority) RouterHandler {
|
||||
return &caHandler{}
|
||||
func New(authority Authority) RouterHandler {
|
||||
return &caHandler{
|
||||
Authority: authority,
|
||||
}
|
||||
}
|
||||
|
||||
func Route(r Router) {
|
||||
r.MethodFunc("GET", "/version", Version)
|
||||
r.MethodFunc("GET", "/health", Health)
|
||||
r.MethodFunc("GET", "/root/{sha}", Root)
|
||||
r.MethodFunc("POST", "/sign", Sign)
|
||||
r.MethodFunc("POST", "/renew", Renew)
|
||||
r.MethodFunc("POST", "/rekey", Rekey)
|
||||
r.MethodFunc("POST", "/revoke", Revoke)
|
||||
r.MethodFunc("GET", "/crl", CRL)
|
||||
r.MethodFunc("GET", "/provisioners", Provisioners)
|
||||
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
|
||||
r.MethodFunc("GET", "/roots", Roots)
|
||||
r.MethodFunc("GET", "/roots.pem", RootsPEM)
|
||||
r.MethodFunc("GET", "/federation", Federation)
|
||||
// SSH CA
|
||||
r.MethodFunc("POST", "/ssh/sign", SSHSign)
|
||||
r.MethodFunc("POST", "/ssh/renew", SSHRenew)
|
||||
r.MethodFunc("POST", "/ssh/revoke", SSHRevoke)
|
||||
r.MethodFunc("POST", "/ssh/rekey", SSHRekey)
|
||||
r.MethodFunc("GET", "/ssh/roots", SSHRoots)
|
||||
r.MethodFunc("GET", "/ssh/federation", SSHFederation)
|
||||
r.MethodFunc("POST", "/ssh/config", SSHConfig)
|
||||
r.MethodFunc("POST", "/ssh/config/{type}", SSHConfig)
|
||||
r.MethodFunc("POST", "/ssh/check-host", SSHCheckHost)
|
||||
r.MethodFunc("GET", "/ssh/hosts", SSHGetHosts)
|
||||
r.MethodFunc("POST", "/ssh/bastion", SSHBastion)
|
||||
|
||||
func (h *caHandler) Route(r Router) {
|
||||
r.MethodFunc("GET", "/health", h.Health)
|
||||
r.MethodFunc("GET", "/root/{sha}", h.Root)
|
||||
r.MethodFunc("POST", "/sign", h.Sign)
|
||||
r.MethodFunc("POST", "/renew", h.Renew)
|
||||
r.MethodFunc("GET", "/provisioners", h.Provisioners)
|
||||
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey)
|
||||
r.MethodFunc("GET", "/roots", h.Roots)
|
||||
r.MethodFunc("GET", "/federation", h.Federation)
|
||||
// For compatibility with old code:
|
||||
r.MethodFunc("POST", "/re-sign", Renew)
|
||||
r.MethodFunc("POST", "/sign-ssh", SSHSign)
|
||||
r.MethodFunc("GET", "/ssh/get-hosts", SSHGetHosts)
|
||||
}
|
||||
|
||||
// Version is an HTTP handler that returns the version of the server.
|
||||
func Version(w http.ResponseWriter, r *http.Request) {
|
||||
v := mustAuthority(r.Context()).Version()
|
||||
render.JSON(w, VersionResponse{
|
||||
Version: v.Version,
|
||||
RequireClientAuthentication: v.RequireClientAuthentication,
|
||||
})
|
||||
r.MethodFunc("POST", "/re-sign", h.Renew)
|
||||
}
|
||||
|
||||
// Health is an HTTP handler that returns the status of the server.
|
||||
func Health(w http.ResponseWriter, _ *http.Request) {
|
||||
render.JSON(w, HealthResponse{Status: "ok"})
|
||||
func (h *caHandler) Health(w http.ResponseWriter, r *http.Request) {
|
||||
JSON(w, HealthResponse{Status: "ok"})
|
||||
}
|
||||
|
||||
// Root is an HTTP handler that using the SHA256 from the URL, returns the root
|
||||
// certificate for the given SHA256.
|
||||
func Root(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *caHandler) Root(w http.ResponseWriter, r *http.Request) {
|
||||
sha := chi.URLParam(r, "sha")
|
||||
sum := strings.ToLower(strings.ReplaceAll(sha, "-", ""))
|
||||
sum := strings.ToLower(strings.Replace(sha, "-", "", -1))
|
||||
// Load root certificate with the
|
||||
cert, err := mustAuthority(r.Context()).Root(sum)
|
||||
cert, err := h.Authority.Root(sum)
|
||||
if err != nil {
|
||||
render.Error(w, errs.Wrapf(http.StatusNotFound, err, "%s was not found", r.RequestURI))
|
||||
WriteError(w, NotFound(errors.Wrapf(err, "%s was not found", r.RequestURI)))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, &RootResponse{RootPEM: Certificate{cert}})
|
||||
JSON(w, &RootResponse{RootPEM: Certificate{cert}})
|
||||
}
|
||||
|
||||
func certChainToPEM(certChain []*x509.Certificate) []Certificate {
|
||||
certChainPEM := make([]Certificate, 0, len(certChain))
|
||||
for _, c := range certChain {
|
||||
certChainPEM = append(certChainPEM, Certificate{c})
|
||||
// Sign is an HTTP handler that reads a certificate request and an
|
||||
// one-time-token (ott) from the body and creates a new certificate with the
|
||||
// information in the certificate request.
|
||||
func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) {
|
||||
var body SignRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
|
||||
return
|
||||
}
|
||||
return certChainPEM
|
||||
if err := body.Validate(); err != nil {
|
||||
WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
signOpts := authority.SignOptions{
|
||||
NotBefore: body.NotBefore,
|
||||
NotAfter: body.NotAfter,
|
||||
}
|
||||
|
||||
extraOpts, err := h.Authority.Authorize(body.OTT)
|
||||
if err != nil {
|
||||
WriteError(w, Unauthorized(err))
|
||||
return
|
||||
}
|
||||
|
||||
cert, root, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, signOpts, extraOpts...)
|
||||
if err != nil {
|
||||
WriteError(w, Forbidden(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
JSON(w, &SignResponse{
|
||||
ServerPEM: Certificate{cert},
|
||||
CaPEM: Certificate{root},
|
||||
TLSOptions: h.Authority.GetTLSOptions(),
|
||||
})
|
||||
}
|
||||
|
||||
// Renew uses the information of certificate in the TLS connection to create a
|
||||
// new one.
|
||||
func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
|
||||
WriteError(w, BadRequest(errors.New("missing peer certificate")))
|
||||
return
|
||||
}
|
||||
|
||||
cert, root, err := h.Authority.Renew(r.TLS.PeerCertificates[0])
|
||||
if err != nil {
|
||||
WriteError(w, Forbidden(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
JSON(w, &SignResponse{
|
||||
ServerPEM: Certificate{cert},
|
||||
CaPEM: Certificate{root},
|
||||
TLSOptions: h.Authority.GetTLSOptions(),
|
||||
})
|
||||
}
|
||||
|
||||
// Provisioners returns the list of provisioners configured in the authority.
|
||||
func Provisioners(w http.ResponseWriter, r *http.Request) {
|
||||
cursor, limit, err := ParseCursor(r)
|
||||
func (h *caHandler) Provisioners(w http.ResponseWriter, r *http.Request) {
|
||||
cursor, limit, err := parseCursor(r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
WriteError(w, BadRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
p, next, err := mustAuthority(r.Context()).GetProvisioners(cursor, limit)
|
||||
p, next, err := h.Authority.GetProvisioners(cursor, limit)
|
||||
if err != nil {
|
||||
render.Error(w, errs.InternalServerErr(err))
|
||||
WriteError(w, InternalServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, &ProvisionersResponse{
|
||||
JSON(w, &ProvisionersResponse{
|
||||
Provisioners: p,
|
||||
NextCursor: next,
|
||||
})
|
||||
}
|
||||
|
||||
// ProvisionerKey returns the encrypted key of a provisioner by it's key id.
|
||||
func ProvisionerKey(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *caHandler) ProvisionerKey(w http.ResponseWriter, r *http.Request) {
|
||||
kid := chi.URLParam(r, "kid")
|
||||
key, err := mustAuthority(r.Context()).GetEncryptedKey(kid)
|
||||
key, err := h.Authority.GetEncryptedKey(kid)
|
||||
if err != nil {
|
||||
render.Error(w, errs.NotFoundErr(err))
|
||||
WriteError(w, NotFound(err))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, &ProvisionerKeyResponse{key})
|
||||
JSON(w, &ProvisionerKeyResponse{key})
|
||||
}
|
||||
|
||||
// Roots returns all the root certificates for the CA.
|
||||
func Roots(w http.ResponseWriter, r *http.Request) {
|
||||
roots, err := mustAuthority(r.Context()).GetRoots()
|
||||
func (h *caHandler) Roots(w http.ResponseWriter, r *http.Request) {
|
||||
roots, err := h.Authority.GetRoots()
|
||||
if err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error getting roots"))
|
||||
WriteError(w, Forbidden(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -410,39 +347,17 @@ func Roots(w http.ResponseWriter, r *http.Request) {
|
|||
certs[i] = Certificate{roots[i]}
|
||||
}
|
||||
|
||||
render.JSONStatus(w, &RootsResponse{
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
JSON(w, &RootsResponse{
|
||||
Certificates: certs,
|
||||
}, http.StatusCreated)
|
||||
}
|
||||
|
||||
// RootsPEM returns all the root certificates for the CA in PEM format.
|
||||
func RootsPEM(w http.ResponseWriter, r *http.Request) {
|
||||
roots, err := mustAuthority(r.Context()).GetRoots()
|
||||
if err != nil {
|
||||
render.Error(w, errs.InternalServerErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/x-pem-file")
|
||||
|
||||
for _, root := range roots {
|
||||
block := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: root.Raw,
|
||||
})
|
||||
|
||||
if _, err := w.Write(block); err != nil {
|
||||
log.Error(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Federation returns all the public certificates in the federation.
|
||||
func Federation(w http.ResponseWriter, r *http.Request) {
|
||||
federated, err := mustAuthority(r.Context()).GetFederation()
|
||||
func (h *caHandler) Federation(w http.ResponseWriter, r *http.Request) {
|
||||
federated, err := h.Authority.GetFederation()
|
||||
if err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error getting federated roots"))
|
||||
WriteError(w, Forbidden(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -451,120 +366,20 @@ func Federation(w http.ResponseWriter, r *http.Request) {
|
|||
certs[i] = Certificate{federated[i]}
|
||||
}
|
||||
|
||||
render.JSONStatus(w, &FederationResponse{
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
JSON(w, &FederationResponse{
|
||||
Certificates: certs,
|
||||
}, http.StatusCreated)
|
||||
}
|
||||
|
||||
var oidStepProvisioner = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}
|
||||
|
||||
type stepProvisioner struct {
|
||||
Type int
|
||||
Name []byte
|
||||
CredentialID []byte
|
||||
}
|
||||
|
||||
func logOtt(w http.ResponseWriter, token string) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"ott": token,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// LogCertificate adds certificate fields to the log message.
|
||||
func LogCertificate(w http.ResponseWriter, cert *x509.Certificate) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
m := map[string]interface{}{
|
||||
"serial": cert.SerialNumber.String(),
|
||||
"subject": cert.Subject.CommonName,
|
||||
"issuer": cert.Issuer.CommonName,
|
||||
"valid-from": cert.NotBefore.Format(time.RFC3339),
|
||||
"valid-to": cert.NotAfter.Format(time.RFC3339),
|
||||
"public-key": fmtPublicKey(cert),
|
||||
"certificate": base64.StdEncoding.EncodeToString(cert.Raw),
|
||||
}
|
||||
for _, ext := range cert.Extensions {
|
||||
if !ext.Id.Equal(oidStepProvisioner) {
|
||||
continue
|
||||
}
|
||||
val := &stepProvisioner{}
|
||||
rest, err := asn1.Unmarshal(ext.Value, val)
|
||||
if err != nil || len(rest) > 0 {
|
||||
break
|
||||
}
|
||||
if len(val.CredentialID) > 0 {
|
||||
m["provisioner"] = fmt.Sprintf("%s (%s)", val.Name, val.CredentialID)
|
||||
} else {
|
||||
m["provisioner"] = string(val.Name)
|
||||
}
|
||||
break
|
||||
}
|
||||
rl.WithFields(m)
|
||||
}
|
||||
}
|
||||
|
||||
// LogSSHCertificate adds SSH certificate fields to the log message.
|
||||
func LogSSHCertificate(w http.ResponseWriter, cert *ssh.Certificate) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
mak := bytes.TrimSpace(ssh.MarshalAuthorizedKey(cert))
|
||||
var certificate string
|
||||
parts := strings.Split(string(mak), " ")
|
||||
if len(parts) > 1 {
|
||||
certificate = parts[1]
|
||||
}
|
||||
var userOrHost string
|
||||
if cert.CertType == ssh.HostCert {
|
||||
userOrHost = "host"
|
||||
} else {
|
||||
userOrHost = "user"
|
||||
}
|
||||
certificateType := fmt.Sprintf("%s %s certificate", parts[0], userOrHost) // e.g. ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate
|
||||
m := map[string]interface{}{
|
||||
"serial": cert.Serial,
|
||||
"principals": cert.ValidPrincipals,
|
||||
"valid-from": time.Unix(int64(cert.ValidAfter), 0).Format(time.RFC3339),
|
||||
"valid-to": time.Unix(int64(cert.ValidBefore), 0).Format(time.RFC3339),
|
||||
"certificate": certificate,
|
||||
"certificate-type": certificateType,
|
||||
}
|
||||
fingerprint, err := sshutil.FormatFingerprint(mak, sshutil.DefaultFingerprint)
|
||||
if err == nil {
|
||||
fpParts := strings.Split(fingerprint, " ")
|
||||
if len(fpParts) > 3 {
|
||||
m["public-key"] = fmt.Sprintf("%s %s", fpParts[1], fpParts[len(fpParts)-1])
|
||||
}
|
||||
}
|
||||
rl.WithFields(m)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseCursor parses the cursor and limit from the request query params.
|
||||
func ParseCursor(r *http.Request) (cursor string, limit int, err error) {
|
||||
func parseCursor(r *http.Request) (cursor string, limit int, err error) {
|
||||
q := r.URL.Query()
|
||||
cursor = q.Get("cursor")
|
||||
if v := q.Get("limit"); len(v) > 0 {
|
||||
limit, err = strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return "", 0, errs.BadRequestErr(err, "limit '%s' is not an integer", v)
|
||||
return "", 0, errors.Wrapf(err, "error converting %s to integer", v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fmtPublicKey(cert *x509.Certificate) string {
|
||||
var params string
|
||||
switch pk := cert.PublicKey.(type) {
|
||||
case *ecdsa.PublicKey:
|
||||
params = pk.Curve.Params().Name
|
||||
case *rsa.PublicKey:
|
||||
params = strconv.Itoa(pk.Size() * 8)
|
||||
case ed25519.PublicKey:
|
||||
return cert.PublicKeyAlgorithm.String()
|
||||
case *dsa.PublicKey:
|
||||
params = strconv.Itoa(pk.Q.BitLen() * 8)
|
||||
default:
|
||||
params = "unknown"
|
||||
}
|
||||
return fmt.Sprintf("%s %s", cert.PublicKeyAlgorithm, params)
|
||||
}
|
||||
|
|
1138
api/api_test.go
1138
api/api_test.go
File diff suppressed because it is too large
Load diff
32
api/crl.go
32
api/crl.go
|
@ -1,32 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
)
|
||||
|
||||
// CRL is an HTTP handler that returns the current CRL in DER or PEM format
|
||||
func CRL(w http.ResponseWriter, r *http.Request) {
|
||||
crlBytes, err := mustAuthority(r.Context()).GetCertificateRevocationList()
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, formatAsPEM := r.URL.Query()["pem"]
|
||||
if formatAsPEM {
|
||||
w.Header().Add("Content-Type", "application/x-pem-file")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.pem\"")
|
||||
|
||||
_ = pem.Encode(w, &pem.Block{
|
||||
Type: "X509 CRL",
|
||||
Bytes: crlBytes,
|
||||
})
|
||||
} else {
|
||||
w.Header().Add("Content-Type", "application/pkix-crl")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"")
|
||||
w.Write(crlBytes)
|
||||
}
|
||||
}
|
142
api/errors.go
Normal file
142
api/errors.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
// StatusCoder interface is used by errors that returns the HTTP response code.
|
||||
type StatusCoder interface {
|
||||
StatusCode() int
|
||||
}
|
||||
|
||||
// StackTracer must be by those errors that return an stack trace.
|
||||
type StackTracer interface {
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
// Error represents the CA API errors.
|
||||
type Error struct {
|
||||
Status int
|
||||
Err error
|
||||
}
|
||||
|
||||
// ErrorResponse represents an error in JSON format.
|
||||
type ErrorResponse struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Cause implements the errors.Causer interface and returns the original error.
|
||||
func (e *Error) Cause() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Error implements the error interface and returns the error string.
|
||||
func (e *Error) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// StatusCode implements the StatusCoder interface and returns the HTTP response
|
||||
// code.
|
||||
func (e *Error) StatusCode() int {
|
||||
return e.Status
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaller interface for the Error struct.
|
||||
func (e *Error) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&ErrorResponse{Status: e.Status, Message: http.StatusText(e.Status)})
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler interface for the Error struct.
|
||||
func (e *Error) UnmarshalJSON(data []byte) error {
|
||||
var er ErrorResponse
|
||||
if err := json.Unmarshal(data, &er); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Status = er.Status
|
||||
e.Err = fmt.Errorf(er.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewError returns a new Error. If the given error implements the StatusCoder
|
||||
// interface we will ignore the given status.
|
||||
func NewError(status int, err error) error {
|
||||
if sc, ok := err.(StatusCoder); ok {
|
||||
return &Error{Status: sc.StatusCode(), Err: err}
|
||||
}
|
||||
cause := errors.Cause(err)
|
||||
if sc, ok := cause.(StatusCoder); ok {
|
||||
return &Error{Status: sc.StatusCode(), Err: err}
|
||||
}
|
||||
return &Error{Status: status, Err: err}
|
||||
}
|
||||
|
||||
// InternalServerError returns a 500 error with the given error.
|
||||
func InternalServerError(err error) error {
|
||||
return NewError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
// BadRequest returns an 400 error with the given error.
|
||||
func BadRequest(err error) error {
|
||||
return NewError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
// Unauthorized returns an 401 error with the given error.
|
||||
func Unauthorized(err error) error {
|
||||
return NewError(http.StatusUnauthorized, err)
|
||||
}
|
||||
|
||||
// Forbidden returns an 403 error with the given error.
|
||||
func Forbidden(err error) error {
|
||||
return NewError(http.StatusForbidden, err)
|
||||
}
|
||||
|
||||
// NotFound returns an 404 error with the given error.
|
||||
func NotFound(err error) error {
|
||||
return NewError(http.StatusNotFound, err)
|
||||
}
|
||||
|
||||
// WriteError writes to w a JSON representation of the given error.
|
||||
func WriteError(w http.ResponseWriter, err error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
cause := errors.Cause(err)
|
||||
if sc, ok := err.(StatusCoder); ok {
|
||||
w.WriteHeader(sc.StatusCode())
|
||||
} else {
|
||||
if sc, ok := cause.(StatusCoder); ok {
|
||||
w.WriteHeader(sc.StatusCode())
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// Write errors in the response writer
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"error": err,
|
||||
})
|
||||
if os.Getenv("STEPDEBUG") == "1" {
|
||||
if e, ok := err.(StackTracer); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"stack-trace": fmt.Sprintf("%+v", e),
|
||||
})
|
||||
} else {
|
||||
if e, ok := cause.(StackTracer); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"stack-trace": fmt.Sprintf("%+v", e),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(err); err != nil {
|
||||
LogError(w, err)
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
// Package log implements API-related logging helpers.
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// StackTracedError is the set of errors implementing the StackTrace function.
|
||||
//
|
||||
// Errors implementing this interface have their stack traces logged when passed
|
||||
// to the Error function of this package.
|
||||
type StackTracedError interface {
|
||||
error
|
||||
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
type fieldCarrier interface {
|
||||
WithFields(map[string]any)
|
||||
Fields() map[string]any
|
||||
}
|
||||
|
||||
// Error adds to the response writer the given error if it implements
|
||||
// logging.ResponseLogger. If it does not implement it, then writes the error
|
||||
// using the log package.
|
||||
func Error(rw http.ResponseWriter, err error) {
|
||||
fc, ok := rw.(fieldCarrier)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
fc.WithFields(map[string]any{
|
||||
"error": err,
|
||||
})
|
||||
|
||||
if os.Getenv("STEPDEBUG") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
var st StackTracedError
|
||||
if errors.As(err, &st) {
|
||||
fc.WithFields(map[string]any{
|
||||
"stack-trace": fmt.Sprintf("%+v", st.StackTrace()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// EnabledResponse log the response object if it implements the EnableLogger
|
||||
// interface.
|
||||
func EnabledResponse(rw http.ResponseWriter, v any) {
|
||||
type enableLogger interface {
|
||||
ToLog() (any, error)
|
||||
}
|
||||
|
||||
if el, ok := v.(enableLogger); ok {
|
||||
out, err := el.ToLog()
|
||||
if err != nil {
|
||||
Error(rw, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if rl, ok := rw.(fieldCarrier); ok {
|
||||
rl.WithFields(map[string]any{
|
||||
"response": out,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
type stackTracedError struct{}
|
||||
|
||||
func (stackTracedError) Error() string {
|
||||
return "a stacktraced error"
|
||||
}
|
||||
|
||||
func (stackTracedError) StackTrace() pkgerrors.StackTrace {
|
||||
f := struct{}{}
|
||||
return pkgerrors.StackTrace{ // fake stacktrace
|
||||
pkgerrors.Frame(unsafe.Pointer(&f)),
|
||||
pkgerrors.Frame(unsafe.Pointer(&f)),
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
error
|
||||
rw http.ResponseWriter
|
||||
isFieldCarrier bool
|
||||
stepDebug bool
|
||||
expectStackTrace bool
|
||||
}{
|
||||
{"noLogger", nil, nil, false, false, false},
|
||||
{"noError", nil, logging.NewResponseLogger(httptest.NewRecorder()), true, false, false},
|
||||
{"noErrorDebug", nil, logging.NewResponseLogger(httptest.NewRecorder()), true, true, false},
|
||||
{"anError", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), true, false, false},
|
||||
{"anErrorDebug", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), true, true, false},
|
||||
{"stackTracedError", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), true, true, true},
|
||||
{"stackTracedErrorDebug", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), true, true, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.stepDebug {
|
||||
t.Setenv("STEPDEBUG", "1")
|
||||
} else {
|
||||
t.Setenv("STEPDEBUG", "0")
|
||||
}
|
||||
|
||||
Error(tt.rw, tt.error)
|
||||
|
||||
// return early if test case doesn't use logger
|
||||
if !tt.isFieldCarrier {
|
||||
return
|
||||
}
|
||||
|
||||
fields := tt.rw.(logging.ResponseLogger).Fields()
|
||||
|
||||
// expect the error field to be (not) set and to be the same error that was fed to Error
|
||||
if tt.error == nil {
|
||||
assert.Nil(t, fields["error"])
|
||||
} else {
|
||||
assert.Same(t, tt.error, fields["error"])
|
||||
}
|
||||
|
||||
// check if stack-trace is set when expected
|
||||
if _, hasStackTrace := fields["stack-trace"]; tt.expectStackTrace && !hasStackTrace {
|
||||
t.Error(`ResponseLogger["stack-trace"] not set`)
|
||||
} else if !tt.expectStackTrace && hasStackTrace {
|
||||
t.Error(`ResponseLogger["stack-trace"] was set`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
// Package read implements request object readers.
|
||||
package read
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
// JSON reads JSON from the request body and stores it in the value
|
||||
// pointed to by v.
|
||||
func JSON(r io.Reader, v interface{}) error {
|
||||
if err := json.NewDecoder(r).Decode(v); err != nil {
|
||||
return errs.BadRequestErr(err, "error decoding json")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProtoJSON reads JSON from the request body and stores it in the value
|
||||
// pointed to by m.
|
||||
func ProtoJSON(r io.Reader, m proto.Message) error {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return errs.BadRequestErr(err, "error reading request body")
|
||||
}
|
||||
|
||||
switch err := protojson.Unmarshal(data, m); {
|
||||
case errors.Is(err, proto.Error):
|
||||
return badProtoJSONError(err.Error())
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// badProtoJSONError is an error type that is returned by ProtoJSON
|
||||
// when a proto message cannot be unmarshaled. Usually this is caused
|
||||
// by an error in the request body.
|
||||
type badProtoJSONError string
|
||||
|
||||
// Error implements error for badProtoJSONError
|
||||
func (e badProtoJSONError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// Render implements render.RenderableError for badProtoJSONError
|
||||
func (e badProtoJSONError) Render(w http.ResponseWriter) {
|
||||
v := struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Type: "badRequest",
|
||||
Detail: "bad request",
|
||||
// trim the proto prefix for the message
|
||||
Message: strings.TrimSpace(strings.TrimPrefix(e.Error(), "proto:")),
|
||||
}
|
||||
render.JSONStatus(w, v, http.StatusBadRequest)
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
package read
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
type args struct {
|
||||
r io.Reader
|
||||
v interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{strings.NewReader(`{"foo":"bar"}`), make(map[string]interface{})}, false},
|
||||
{"fail", args{strings.NewReader(`{"foo"}`), make(map[string]interface{})}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := JSON(tt.args.r, &tt.args.v)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("JSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
var e *errs.Error
|
||||
if errors.As(err, &e) {
|
||||
if code := e.StatusCode(); code != 400 {
|
||||
t.Errorf("error.StatusCode() = %v, wants 400", code)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("error type = %T, wants *Error", err)
|
||||
}
|
||||
} else if !reflect.DeepEqual(tt.args.v, map[string]interface{}{"foo": "bar"}) {
|
||||
t.Errorf("JSON value = %v, wants %v", tt.args.v, map[string]interface{}{"foo": "bar"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtoJSON(t *testing.T) {
|
||||
|
||||
p := new(linkedca.Policy) // TODO(hs): can we use something different, so we don't need the import?
|
||||
|
||||
type args struct {
|
||||
r io.Reader
|
||||
m proto.Message
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "fail/io.ReadAll",
|
||||
args: args{
|
||||
r: iotest.ErrReader(errors.New("read error")),
|
||||
m: p,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "fail/proto",
|
||||
args: args{
|
||||
r: strings.NewReader(`{?}`),
|
||||
m: p,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
r: strings.NewReader(`{"x509":{}}`),
|
||||
m: p,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ProtoJSON(tt.args.r, tt.args.m)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ProtoJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
var (
|
||||
ee *errs.Error
|
||||
bpe badProtoJSONError
|
||||
)
|
||||
switch {
|
||||
case errors.As(err, &bpe):
|
||||
assert.Contains(t, err.Error(), "syntax error")
|
||||
case errors.As(err, &ee):
|
||||
assert.Equal(t, http.StatusBadRequest, ee.Status)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, protoreflect.FullName("linkedca.Policy"), proto.MessageName(tt.args.m))
|
||||
assert.True(t, proto.Equal(&linkedca.Policy{X509: &linkedca.X509Policy{}}, tt.args.m))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_badProtoJSONError_Render(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
e badProtoJSONError
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "bad proto normal space",
|
||||
e: badProtoJSONError("proto: syntax error (line 1:2): invalid value ?"),
|
||||
expected: "syntax error (line 1:2): invalid value ?",
|
||||
},
|
||||
{
|
||||
name: "bad proto non breaking space",
|
||||
e: badProtoJSONError("proto: syntax error (line 1:2): invalid value ?"),
|
||||
expected: "syntax error (line 1:2): invalid value ?",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
tt.e.Render(w)
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
v := struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
Message string `json:"message"`
|
||||
}{}
|
||||
|
||||
assert.NoError(t, json.Unmarshal(data, &v))
|
||||
assert.Equal(t, "badRequest", v.Type)
|
||||
assert.Equal(t, "bad request", v.Detail)
|
||||
assert.Equal(t, "syntax error (line 1:2): invalid value ?", v.Message)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
66
api/rekey.go
66
api/rekey.go
|
@ -1,66 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
// RekeyRequest is the request body for a certificate rekey request.
|
||||
type RekeyRequest struct {
|
||||
CsrPEM CertificateRequest `json:"csr"`
|
||||
}
|
||||
|
||||
// Validate checks the fields of the RekeyRequest and returns nil if they are ok
|
||||
// or an error if something is wrong.
|
||||
func (s *RekeyRequest) Validate() error {
|
||||
if s.CsrPEM.CertificateRequest == nil {
|
||||
return errs.BadRequest("missing csr")
|
||||
}
|
||||
if err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil {
|
||||
return errs.BadRequestErr(err, "invalid csr")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rekey is similar to renew except that the certificate will be renewed with new key from csr.
|
||||
func Rekey(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
|
||||
render.Error(w, errs.BadRequest("missing client certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
var body RekeyRequest
|
||||
if err := read.JSON(r.Body, &body); err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := body.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
a := mustAuthority(r.Context())
|
||||
certChain, err := a.Rekey(r.TLS.PeerCertificates[0], body.CsrPEM.CertificateRequest.PublicKey)
|
||||
if err != nil {
|
||||
render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Rekey"))
|
||||
return
|
||||
}
|
||||
certChainPEM := certChainToPEM(certChain)
|
||||
var caPEM Certificate
|
||||
if len(certChainPEM) > 1 {
|
||||
caPEM = certChainPEM[1]
|
||||
}
|
||||
|
||||
LogCertificate(w, certChain[0])
|
||||
render.JSONStatus(w, &SignResponse{
|
||||
ServerPEM: certChainPEM[0],
|
||||
CaPEM: caPEM,
|
||||
CertChainPEM: certChainPEM,
|
||||
TLSOptions: a.GetTLSOptions(),
|
||||
}, http.StatusCreated)
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
// Package render implements functionality related to response rendering.
|
||||
package render
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/smallstep/certificates/api/log"
|
||||
)
|
||||
|
||||
// JSON is shorthand for JSONStatus(w, v, http.StatusOK).
|
||||
func JSON(w http.ResponseWriter, v interface{}) {
|
||||
JSONStatus(w, v, http.StatusOK)
|
||||
}
|
||||
|
||||
// JSONStatus marshals v into w. It additionally sets the status code of
|
||||
// w to the given one.
|
||||
//
|
||||
// JSONStatus sets the Content-Type of w to application/json unless one is
|
||||
// specified.
|
||||
func JSONStatus(w http.ResponseWriter, v interface{}, status int) {
|
||||
setContentTypeUnlessPresent(w, "application/json")
|
||||
w.WriteHeader(status)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
var errUnsupportedType *json.UnsupportedTypeError
|
||||
if errors.As(err, &errUnsupportedType) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var errUnsupportedValue *json.UnsupportedValueError
|
||||
if errors.As(err, &errUnsupportedValue) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var errMarshalError *json.MarshalerError
|
||||
if errors.As(err, &errMarshalError) {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.EnabledResponse(w, v)
|
||||
}
|
||||
|
||||
// ProtoJSON is shorthand for ProtoJSONStatus(w, m, http.StatusOK).
|
||||
func ProtoJSON(w http.ResponseWriter, m proto.Message) {
|
||||
ProtoJSONStatus(w, m, http.StatusOK)
|
||||
}
|
||||
|
||||
// ProtoJSONStatus writes the given value into the http.ResponseWriter and the
|
||||
// given status is written as the status code of the response.
|
||||
func ProtoJSONStatus(w http.ResponseWriter, m proto.Message, status int) {
|
||||
b, err := protojson.Marshal(m)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
setContentTypeUnlessPresent(w, "application/json")
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
func setContentTypeUnlessPresent(w http.ResponseWriter, contentType string) {
|
||||
const header = "Content-Type"
|
||||
|
||||
h := w.Header()
|
||||
if _, ok := h[header]; !ok {
|
||||
h.Set(header, contentType)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderableError is the set of errors that implement the basic Render method.
|
||||
//
|
||||
// Errors that implement this interface will use their own Render method when
|
||||
// being rendered into responses.
|
||||
type RenderableError interface {
|
||||
error
|
||||
|
||||
Render(http.ResponseWriter)
|
||||
}
|
||||
|
||||
// Error marshals the JSON representation of err to w. In case err implements
|
||||
// RenderableError its own Render method will be called instead.
|
||||
func Error(w http.ResponseWriter, err error) {
|
||||
log.Error(w, err)
|
||||
|
||||
var r RenderableError
|
||||
if errors.As(err, &r) {
|
||||
r.Render(w)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
JSONStatus(w, err, statusCodeFromError(err))
|
||||
}
|
||||
|
||||
// StatusCodedError is the set of errors that implement the basic StatusCode
|
||||
// function.
|
||||
//
|
||||
// Errors that implement this interface will use the code reported by StatusCode
|
||||
// as the HTTP response code when being rendered by this package.
|
||||
type StatusCodedError interface {
|
||||
error
|
||||
|
||||
StatusCode() int
|
||||
}
|
||||
|
||||
func statusCodeFromError(err error) (code int) {
|
||||
code = http.StatusInternalServerError
|
||||
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
for err != nil {
|
||||
var sc StatusCodedError
|
||||
if errors.As(err, &sc) {
|
||||
code = sc.StatusCode()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
var c causer
|
||||
if !errors.As(err, &c) {
|
||||
break
|
||||
}
|
||||
err = c.Cause()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
rw := logging.NewResponseLogger(rec)
|
||||
|
||||
JSON(rw, map[string]interface{}{"foo": "bar"})
|
||||
|
||||
assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
|
||||
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}\n", rec.Body.String())
|
||||
|
||||
assert.Empty(t, rw.Fields())
|
||||
}
|
||||
|
||||
func TestJSONPanicsOnUnsupportedType(t *testing.T) {
|
||||
jsonPanicTest[json.UnsupportedTypeError](t, make(chan struct{}))
|
||||
}
|
||||
|
||||
func TestJSONPanicsOnUnsupportedValue(t *testing.T) {
|
||||
jsonPanicTest[json.UnsupportedValueError](t, math.NaN())
|
||||
}
|
||||
|
||||
func TestJSONPanicsOnMarshalerError(t *testing.T) {
|
||||
var v erroneousJSONMarshaler
|
||||
jsonPanicTest[json.MarshalerError](t, v)
|
||||
}
|
||||
|
||||
type erroneousJSONMarshaler struct{}
|
||||
|
||||
func (erroneousJSONMarshaler) MarshalJSON() ([]byte, error) {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
|
||||
func jsonPanicTest[T json.UnsupportedTypeError | json.UnsupportedValueError | json.MarshalerError](t *testing.T, v any) {
|
||||
t.Helper()
|
||||
|
||||
defer func() {
|
||||
var err error
|
||||
if r := recover(); r == nil {
|
||||
t.Fatal("expected panic")
|
||||
} else if e, ok := r.(error); !ok {
|
||||
t.Fatalf("did not panic with an error (%T)", r)
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
var e *T
|
||||
assert.ErrorAs(t, err, &e)
|
||||
}()
|
||||
|
||||
JSON(httptest.NewRecorder(), v)
|
||||
}
|
||||
|
||||
type renderableError struct {
|
||||
Code int `json:"-"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (err renderableError) Error() string {
|
||||
return err.Message
|
||||
}
|
||||
|
||||
func (err renderableError) Render(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "something/custom")
|
||||
|
||||
JSONStatus(w, err, err.Code)
|
||||
}
|
||||
|
||||
type statusedError struct {
|
||||
Contents string
|
||||
}
|
||||
|
||||
func (err statusedError) Error() string { return err.Contents }
|
||||
|
||||
func (statusedError) StatusCode() int { return 432 }
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
cases := []struct {
|
||||
err error
|
||||
code int
|
||||
body string
|
||||
header string
|
||||
}{
|
||||
0: {
|
||||
err: renderableError{532, "some string"},
|
||||
code: 532,
|
||||
body: "{\"message\":\"some string\"}\n",
|
||||
header: "something/custom",
|
||||
},
|
||||
1: {
|
||||
err: statusedError{"123"},
|
||||
code: 432,
|
||||
body: "{\"Contents\":\"123\"}\n",
|
||||
header: "application/json",
|
||||
},
|
||||
}
|
||||
|
||||
for caseIndex := range cases {
|
||||
kase := cases[caseIndex]
|
||||
|
||||
t.Run(strconv.Itoa(caseIndex), func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
Error(rec, kase.err)
|
||||
|
||||
assert.Equal(t, kase.code, rec.Result().StatusCode)
|
||||
assert.Equal(t, kase.body, rec.Body.String())
|
||||
assert.Equal(t, kase.header, rec.Header().Get("Content-Type"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type causedError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (err causedError) Error() string { return fmt.Sprintf("cause: %s", err.cause) }
|
||||
func (err causedError) Cause() error { return err.cause }
|
||||
|
||||
func TestStatusCodeFromError(t *testing.T) {
|
||||
cases := []struct {
|
||||
err error
|
||||
exp int
|
||||
}{
|
||||
0: {nil, http.StatusInternalServerError},
|
||||
1: {io.EOF, http.StatusInternalServerError},
|
||||
2: {statusedError{"123"}, 432},
|
||||
3: {causedError{statusedError{"432"}}, 432},
|
||||
}
|
||||
|
||||
for caseIndex, kase := range cases {
|
||||
assert.Equal(t, kase.exp, statusCodeFromError(kase.err), "case: %d", caseIndex)
|
||||
}
|
||||
}
|
68
api/renew.go
68
api/renew.go
|
@ -1,68 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
const (
|
||||
authorizationHeader = "Authorization"
|
||||
bearerScheme = "Bearer"
|
||||
)
|
||||
|
||||
// Renew uses the information of certificate in the TLS connection to create a
|
||||
// new one.
|
||||
func Renew(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get the leaf certificate from the peer or the token.
|
||||
cert, token, err := getPeerCertificate(r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// The token can be used by RAs to renew a certificate.
|
||||
if token != "" {
|
||||
ctx = authority.NewTokenContext(ctx, token)
|
||||
}
|
||||
|
||||
a := mustAuthority(ctx)
|
||||
certChain, err := a.RenewContext(ctx, cert, nil)
|
||||
if err != nil {
|
||||
render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
|
||||
return
|
||||
}
|
||||
certChainPEM := certChainToPEM(certChain)
|
||||
var caPEM Certificate
|
||||
if len(certChainPEM) > 1 {
|
||||
caPEM = certChainPEM[1]
|
||||
}
|
||||
|
||||
LogCertificate(w, certChain[0])
|
||||
render.JSONStatus(w, &SignResponse{
|
||||
ServerPEM: certChainPEM[0],
|
||||
CaPEM: caPEM,
|
||||
CertChainPEM: certChainPEM,
|
||||
TLSOptions: a.GetTLSOptions(),
|
||||
}, http.StatusCreated)
|
||||
}
|
||||
|
||||
func getPeerCertificate(r *http.Request) (*x509.Certificate, string, error) {
|
||||
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
|
||||
return r.TLS.PeerCertificates[0], "", nil
|
||||
}
|
||||
if s := r.Header.Get(authorizationHeader); s != "" {
|
||||
if parts := strings.SplitN(s, bearerScheme+" ", 2); len(parts) == 2 {
|
||||
ctx := r.Context()
|
||||
peer, err := mustAuthority(ctx).AuthorizeRenewToken(ctx, parts[1])
|
||||
return peer, parts[1], err
|
||||
}
|
||||
}
|
||||
return nil, "", errs.BadRequest("missing client certificate")
|
||||
}
|
127
api/revoke.go
127
api/revoke.go
|
@ -1,127 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
// RevokeResponse is the response object that returns the health of the server.
|
||||
type RevokeResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// RevokeRequest is the request body for a revocation request.
|
||||
type RevokeRequest struct {
|
||||
Serial string `json:"serial"`
|
||||
OTT string `json:"ott"`
|
||||
ReasonCode int `json:"reasonCode"`
|
||||
Reason string `json:"reason"`
|
||||
Passive bool `json:"passive"`
|
||||
}
|
||||
|
||||
// Validate checks the fields of the RevokeRequest and returns nil if they are ok
|
||||
// or an error if something is wrong.
|
||||
func (r *RevokeRequest) Validate() (err error) {
|
||||
if r.Serial == "" {
|
||||
return errs.BadRequest("missing serial")
|
||||
}
|
||||
sn, ok := new(big.Int).SetString(r.Serial, 0)
|
||||
if !ok {
|
||||
return errs.BadRequest("'%s' is not a valid serial number - use a base 10 representation or a base 16 representation with '0x' prefix", r.Serial)
|
||||
}
|
||||
r.Serial = sn.String()
|
||||
if r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise {
|
||||
return errs.BadRequest("reasonCode out of bounds")
|
||||
}
|
||||
if !r.Passive {
|
||||
return errs.NotImplemented("non-passive revocation not implemented")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Revoke supports handful of different methods that revoke a Certificate.
|
||||
//
|
||||
// NOTE: currently only Passive revocation is supported.
|
||||
//
|
||||
// TODO: Add CRL and OCSP support.
|
||||
func Revoke(w http.ResponseWriter, r *http.Request) {
|
||||
var body RevokeRequest
|
||||
if err := read.JSON(r.Body, &body); err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := body.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
opts := &authority.RevokeOptions{
|
||||
Serial: body.Serial,
|
||||
Reason: body.Reason,
|
||||
ReasonCode: body.ReasonCode,
|
||||
PassiveOnly: body.Passive,
|
||||
}
|
||||
|
||||
ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.RevokeMethod)
|
||||
a := mustAuthority(ctx)
|
||||
|
||||
// A token indicates that we are using the api via a provisioner token,
|
||||
// otherwise it is assumed that the certificate is revoking itself over mTLS.
|
||||
if len(body.OTT) > 0 {
|
||||
logOtt(w, body.OTT)
|
||||
if _, err := a.Authorize(ctx, body.OTT); err != nil {
|
||||
render.Error(w, errs.UnauthorizedErr(err))
|
||||
return
|
||||
}
|
||||
opts.OTT = body.OTT
|
||||
} else {
|
||||
// If no token is present, then the request must be made over mTLS and
|
||||
// the client certificate Serial Number must match the serial number
|
||||
// being revoked.
|
||||
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
|
||||
render.Error(w, errs.BadRequest("missing ott or client certificate"))
|
||||
return
|
||||
}
|
||||
opts.Crt = r.TLS.PeerCertificates[0]
|
||||
if opts.Crt.SerialNumber.String() != opts.Serial {
|
||||
render.Error(w, errs.BadRequest("serial number in client certificate different than body"))
|
||||
return
|
||||
}
|
||||
// TODO: should probably be checking if the certificate was revoked here.
|
||||
// Will need to thread that request down to the authority, so will need
|
||||
// to add API for that.
|
||||
LogCertificate(w, opts.Crt)
|
||||
opts.MTLS = true
|
||||
}
|
||||
|
||||
if err := a.Revoke(ctx, opts); err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error revoking certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
logRevoke(w, opts)
|
||||
render.JSON(w, &RevokeResponse{Status: "ok"})
|
||||
}
|
||||
|
||||
func logRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"serial": ri.Serial,
|
||||
"reasonCode": ri.ReasonCode,
|
||||
"reason": ri.Reason,
|
||||
"passiveOnly": ri.PassiveOnly,
|
||||
"mTLS": ri.MTLS,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,252 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
func TestRevokeRequestValidate(t *testing.T) {
|
||||
type test struct {
|
||||
rr *RevokeRequest
|
||||
err *errs.Error
|
||||
}
|
||||
tests := map[string]test{
|
||||
"error/missing serial": {
|
||||
rr: &RevokeRequest{},
|
||||
err: &errs.Error{Err: errors.New("missing serial"), Status: http.StatusBadRequest},
|
||||
},
|
||||
"error/bad sn": {
|
||||
rr: &RevokeRequest{Serial: "sn"},
|
||||
err: &errs.Error{Err: errors.New("'sn' is not a valid serial number - use a base 10 representation or a base 16 representation with '0x' prefix"), Status: http.StatusBadRequest},
|
||||
},
|
||||
"error/bad reasonCode": {
|
||||
rr: &RevokeRequest{
|
||||
Serial: "10",
|
||||
ReasonCode: 15,
|
||||
Passive: true,
|
||||
},
|
||||
err: &errs.Error{Err: errors.New("reasonCode out of bounds"), Status: http.StatusBadRequest},
|
||||
},
|
||||
"error/non-passive not implemented": {
|
||||
rr: &RevokeRequest{
|
||||
Serial: "10",
|
||||
ReasonCode: 8,
|
||||
Passive: false,
|
||||
},
|
||||
err: &errs.Error{Err: errors.New("non-passive revocation not implemented"), Status: http.StatusNotImplemented},
|
||||
},
|
||||
"ok": {
|
||||
rr: &RevokeRequest{
|
||||
Serial: "10",
|
||||
ReasonCode: 9,
|
||||
Passive: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.rr.Validate(); err != nil {
|
||||
var ee *errs.Error
|
||||
if errors.As(err, &ee) {
|
||||
assert.HasPrefix(t, ee.Error(), tc.err.Error())
|
||||
assert.Equals(t, ee.StatusCode(), tc.err.Status)
|
||||
} else {
|
||||
t.Errorf("unexpected error type: %T", err)
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_caHandler_Revoke(t *testing.T) {
|
||||
type test struct {
|
||||
input string
|
||||
auth Authority
|
||||
tls *tls.ConnectionState
|
||||
statusCode int
|
||||
expected []byte
|
||||
}
|
||||
tests := map[string]func(*testing.T) test{
|
||||
"400/json read error": func(t *testing.T) test {
|
||||
return test{
|
||||
input: "{",
|
||||
statusCode: http.StatusBadRequest,
|
||||
}
|
||||
},
|
||||
"400/invalid request body": func(t *testing.T) test {
|
||||
input, err := json.Marshal(RevokeRequest{})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusBadRequest,
|
||||
}
|
||||
},
|
||||
"200/ott": func(t *testing.T) test {
|
||||
input, err := json.Marshal(RevokeRequest{
|
||||
Serial: "10",
|
||||
ReasonCode: 4,
|
||||
Reason: "foo",
|
||||
OTT: "valid",
|
||||
Passive: true,
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusOK,
|
||||
auth: &mockAuthority{
|
||||
authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||
return nil, nil
|
||||
},
|
||||
revoke: func(ctx context.Context, opts *authority.RevokeOptions) error {
|
||||
assert.True(t, opts.PassiveOnly)
|
||||
assert.False(t, opts.MTLS)
|
||||
assert.Equals(t, opts.Serial, "10")
|
||||
assert.Equals(t, opts.ReasonCode, 4)
|
||||
assert.Equals(t, opts.Reason, "foo")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expected: []byte(`{"status":"ok"}`),
|
||||
}
|
||||
},
|
||||
"400/no OTT and no peer certificate": func(t *testing.T) test {
|
||||
input, err := json.Marshal(RevokeRequest{
|
||||
Serial: "10",
|
||||
ReasonCode: 4,
|
||||
Passive: true,
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusBadRequest,
|
||||
}
|
||||
},
|
||||
"200/no ott": func(t *testing.T) test {
|
||||
cs := &tls.ConnectionState{
|
||||
PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},
|
||||
}
|
||||
input, err := json.Marshal(RevokeRequest{
|
||||
Serial: "1404354960355712309",
|
||||
ReasonCode: 4,
|
||||
Reason: "foo",
|
||||
Passive: true,
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusOK,
|
||||
tls: cs,
|
||||
auth: &mockAuthority{
|
||||
authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||
return nil, nil
|
||||
},
|
||||
revoke: func(ctx context.Context, ri *authority.RevokeOptions) error {
|
||||
assert.True(t, ri.PassiveOnly)
|
||||
assert.True(t, ri.MTLS)
|
||||
assert.Equals(t, ri.Serial, "1404354960355712309")
|
||||
assert.Equals(t, ri.ReasonCode, 4)
|
||||
assert.Equals(t, ri.Reason, "foo")
|
||||
return nil
|
||||
},
|
||||
loadProvisionerByCertificate: func(crt *x509.Certificate) (provisioner.Interface, error) {
|
||||
return &mockProvisioner{
|
||||
getID: func() string {
|
||||
return "mock-provisioner-id"
|
||||
},
|
||||
}, err
|
||||
},
|
||||
},
|
||||
expected: []byte(`{"status":"ok"}`),
|
||||
}
|
||||
},
|
||||
"500/ott authority.Revoke": func(t *testing.T) test {
|
||||
input, err := json.Marshal(RevokeRequest{
|
||||
Serial: "10",
|
||||
ReasonCode: 4,
|
||||
Reason: "foo",
|
||||
OTT: "valid",
|
||||
Passive: true,
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusInternalServerError,
|
||||
auth: &mockAuthority{
|
||||
authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||
return nil, nil
|
||||
},
|
||||
revoke: func(ctx context.Context, opts *authority.RevokeOptions) error {
|
||||
return errs.InternalServer("force")
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"403/ott authority.Revoke": func(t *testing.T) test {
|
||||
input, err := json.Marshal(RevokeRequest{
|
||||
Serial: "10",
|
||||
ReasonCode: 4,
|
||||
Reason: "foo",
|
||||
OTT: "valid",
|
||||
Passive: true,
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
return test{
|
||||
input: string(input),
|
||||
statusCode: http.StatusForbidden,
|
||||
auth: &mockAuthority{
|
||||
authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||
return nil, nil
|
||||
},
|
||||
revoke: func(ctx context.Context, opts *authority.RevokeOptions) error {
|
||||
return errors.New("force")
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for name, _tc := range tests {
|
||||
tc := _tc(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mockMustAuthority(t, tc.auth)
|
||||
req := httptest.NewRequest("POST", "http://example.com/revoke", strings.NewReader(tc.input))
|
||||
if tc.tls != nil {
|
||||
req.TLS = tc.tls
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
Revoke(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if tc.statusCode < http.StatusBadRequest {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), tc.expected) {
|
||||
t.Errorf("caHandler.Root Body = %s, wants %s", body, tc.expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
99
api/sign.go
99
api/sign.go
|
@ -1,99 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
// SignRequest is the request body for a certificate signature request.
|
||||
type SignRequest struct {
|
||||
CsrPEM CertificateRequest `json:"csr"`
|
||||
OTT string `json:"ott"`
|
||||
NotAfter TimeDuration `json:"notAfter,omitempty"`
|
||||
NotBefore TimeDuration `json:"notBefore,omitempty"`
|
||||
TemplateData json.RawMessage `json:"templateData,omitempty"`
|
||||
}
|
||||
|
||||
// Validate checks the fields of the SignRequest and returns nil if they are ok
|
||||
// or an error if something is wrong.
|
||||
func (s *SignRequest) Validate() error {
|
||||
if s.CsrPEM.CertificateRequest == nil {
|
||||
return errs.BadRequest("missing csr")
|
||||
}
|
||||
if err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil {
|
||||
return errs.BadRequestErr(err, "invalid csr")
|
||||
}
|
||||
if s.OTT == "" {
|
||||
return errs.BadRequest("missing ott")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignResponse is the response object of the certificate signature request.
|
||||
type SignResponse struct {
|
||||
ServerPEM Certificate `json:"crt"`
|
||||
CaPEM Certificate `json:"ca"`
|
||||
CertChainPEM []Certificate `json:"certChain"`
|
||||
TLSOptions *config.TLSOptions `json:"tlsOptions,omitempty"`
|
||||
TLS *tls.ConnectionState `json:"-"`
|
||||
}
|
||||
|
||||
// Sign is an HTTP handler that reads a certificate request and an
|
||||
// one-time-token (ott) from the body and creates a new certificate with the
|
||||
// information in the certificate request.
|
||||
func Sign(w http.ResponseWriter, r *http.Request) {
|
||||
var body SignRequest
|
||||
if err := read.JSON(r.Body, &body); err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
|
||||
logOtt(w, body.OTT)
|
||||
if err := body.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
opts := provisioner.SignOptions{
|
||||
NotBefore: body.NotBefore,
|
||||
NotAfter: body.NotAfter,
|
||||
TemplateData: body.TemplateData,
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
a := mustAuthority(ctx)
|
||||
|
||||
ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)
|
||||
signOpts, err := a.Authorize(ctx, body.OTT)
|
||||
if err != nil {
|
||||
render.Error(w, errs.UnauthorizedErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
certChain, err := a.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...)
|
||||
if err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error signing certificate"))
|
||||
return
|
||||
}
|
||||
certChainPEM := certChainToPEM(certChain)
|
||||
var caPEM Certificate
|
||||
if len(certChainPEM) > 1 {
|
||||
caPEM = certChainPEM[1]
|
||||
}
|
||||
|
||||
LogCertificate(w, certChain[0])
|
||||
render.JSONStatus(w, &SignResponse{
|
||||
ServerPEM: certChainPEM[0],
|
||||
CaPEM: caPEM,
|
||||
CertChainPEM: certChainPEM,
|
||||
TLSOptions: a.GetTLSOptions(),
|
||||
}, http.StatusCreated)
|
||||
}
|
511
api/ssh.go
511
api/ssh.go
|
@ -1,511 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/templates"
|
||||
)
|
||||
|
||||
// SSHAuthority is the interface implemented by a SSH CA authority.
|
||||
type SSHAuthority interface {
|
||||
SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
||||
RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||
RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
||||
SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||
GetSSHRoots(ctx context.Context) (*config.SSHKeys, error)
|
||||
GetSSHFederation(ctx context.Context) (*config.SSHKeys, error)
|
||||
GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error)
|
||||
CheckSSHHost(ctx context.Context, principal string, token string) (bool, error)
|
||||
GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]config.Host, error)
|
||||
GetSSHBastion(ctx context.Context, user string, hostname string) (*config.Bastion, error)
|
||||
}
|
||||
|
||||
// SSHSignRequest is the request body of an SSH certificate request.
|
||||
type SSHSignRequest struct {
|
||||
PublicKey []byte `json:"publicKey"` // base64 encoded
|
||||
OTT string `json:"ott"`
|
||||
CertType string `json:"certType,omitempty"`
|
||||
KeyID string `json:"keyID,omitempty"`
|
||||
Principals []string `json:"principals,omitempty"`
|
||||
ValidAfter TimeDuration `json:"validAfter,omitempty"`
|
||||
ValidBefore TimeDuration `json:"validBefore,omitempty"`
|
||||
AddUserPublicKey []byte `json:"addUserPublicKey,omitempty"`
|
||||
IdentityCSR CertificateRequest `json:"identityCSR,omitempty"`
|
||||
TemplateData json.RawMessage `json:"templateData,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates the SSHSignRequest.
|
||||
func (s *SSHSignRequest) Validate() error {
|
||||
switch {
|
||||
case s.CertType != "" && s.CertType != provisioner.SSHUserCert && s.CertType != provisioner.SSHHostCert:
|
||||
return errs.BadRequest("invalid certType '%s'", s.CertType)
|
||||
case len(s.PublicKey) == 0:
|
||||
return errs.BadRequest("missing or empty publicKey")
|
||||
case s.OTT == "":
|
||||
return errs.BadRequest("missing or empty ott")
|
||||
default:
|
||||
// Validate identity signature if provided
|
||||
if s.IdentityCSR.CertificateRequest != nil {
|
||||
if err := s.IdentityCSR.CertificateRequest.CheckSignature(); err != nil {
|
||||
return errs.BadRequestErr(err, "invalid identityCSR")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SSHSignResponse is the response object that returns the SSH certificate.
|
||||
type SSHSignResponse struct {
|
||||
Certificate SSHCertificate `json:"crt"`
|
||||
AddUserCertificate *SSHCertificate `json:"addUserCrt,omitempty"`
|
||||
IdentityCertificate []Certificate `json:"identityCrt,omitempty"`
|
||||
}
|
||||
|
||||
// SSHRootsResponse represents the response object that returns the SSH user and
|
||||
// host keys.
|
||||
type SSHRootsResponse struct {
|
||||
UserKeys []SSHPublicKey `json:"userKey,omitempty"`
|
||||
HostKeys []SSHPublicKey `json:"hostKey,omitempty"`
|
||||
}
|
||||
|
||||
// SSHCertificate represents the response SSH certificate.
|
||||
type SSHCertificate struct {
|
||||
*ssh.Certificate `json:"omitempty"`
|
||||
}
|
||||
|
||||
// SSHGetHostsResponse is the response object that returns the list of valid
|
||||
// hosts for SSH.
|
||||
type SSHGetHostsResponse struct {
|
||||
Hosts []config.Host `json:"hosts"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface. Returns a quoted,
|
||||
// base64 encoded, openssh wire format version of the certificate.
|
||||
func (c SSHCertificate) MarshalJSON() ([]byte, error) {
|
||||
if c.Certificate == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
s := base64.StdEncoding.EncodeToString(c.Certificate.Marshal())
|
||||
return []byte(`"` + s + `"`), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface. The certificate is
|
||||
// expected to be a quoted, base64 encoded, openssh wire formatted block of bytes.
|
||||
func (c *SSHCertificate) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return errors.Wrap(err, "error decoding certificate")
|
||||
}
|
||||
if s == "" {
|
||||
c.Certificate = nil
|
||||
return nil
|
||||
}
|
||||
certData, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error decoding ssh certificate")
|
||||
}
|
||||
pub, err := ssh.ParsePublicKey(certData)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing ssh certificate")
|
||||
}
|
||||
cert, ok := pub.(*ssh.Certificate)
|
||||
if !ok {
|
||||
return errors.Errorf("error decoding ssh certificate: %T is not an *ssh.Certificate", pub)
|
||||
}
|
||||
c.Certificate = cert
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSHPublicKey represents a public key in a response object.
|
||||
type SSHPublicKey struct {
|
||||
ssh.PublicKey
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface. Returns a quoted,
|
||||
// base64 encoded, openssh wire format version of the public key.
|
||||
func (p *SSHPublicKey) MarshalJSON() ([]byte, error) {
|
||||
if p == nil || p.PublicKey == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
s := base64.StdEncoding.EncodeToString(p.PublicKey.Marshal())
|
||||
return []byte(`"` + s + `"`), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface. The public key is
|
||||
// expected to be a quoted, base64 encoded, openssh wire formatted block of
|
||||
// bytes.
|
||||
func (p *SSHPublicKey) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return errors.Wrap(err, "error decoding ssh public key")
|
||||
}
|
||||
if s == "" {
|
||||
p.PublicKey = nil
|
||||
return nil
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error decoding ssh public key")
|
||||
}
|
||||
pub, err := ssh.ParsePublicKey(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing ssh public key")
|
||||
}
|
||||
p.PublicKey = pub
|
||||
return nil
|
||||
}
|
||||
|
||||
// Template represents the output of a template.
|
||||
type Template = templates.Output
|
||||
|
||||
// SSHConfigRequest is the request body used to get the SSH configuration
|
||||
// templates.
|
||||
type SSHConfigRequest struct {
|
||||
Type string `json:"type"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
// Validate checks the values of the SSHConfigurationRequest.
|
||||
func (r *SSHConfigRequest) Validate() error {
|
||||
switch r.Type {
|
||||
case "":
|
||||
r.Type = provisioner.SSHUserCert
|
||||
return nil
|
||||
case provisioner.SSHUserCert, provisioner.SSHHostCert:
|
||||
return nil
|
||||
default:
|
||||
return errs.BadRequest("invalid type '%s'", r.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// SSHConfigResponse is the response that returns the rendered templates.
|
||||
type SSHConfigResponse struct {
|
||||
UserTemplates []Template `json:"userTemplates,omitempty"`
|
||||
HostTemplates []Template `json:"hostTemplates,omitempty"`
|
||||
}
|
||||
|
||||
// SSHCheckPrincipalRequest is the request body used to check if a principal
|
||||
// certificate has been created. Right now it only supported for hosts
|
||||
// certificates.
|
||||
type SSHCheckPrincipalRequest struct {
|
||||
Type string `json:"type"`
|
||||
Principal string `json:"principal"`
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// Validate checks the check principal request.
|
||||
func (r *SSHCheckPrincipalRequest) Validate() error {
|
||||
switch {
|
||||
case r.Type != provisioner.SSHHostCert:
|
||||
return errs.BadRequest("unsupported type '%s'", r.Type)
|
||||
case r.Principal == "":
|
||||
return errs.BadRequest("missing or empty principal")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SSHCheckPrincipalResponse is the response body used to check if a principal
|
||||
// exists.
|
||||
type SSHCheckPrincipalResponse struct {
|
||||
Exists bool `json:"exists"`
|
||||
}
|
||||
|
||||
// SSHBastionRequest is the request body used to get the bastion for a given
|
||||
// host.
|
||||
type SSHBastionRequest struct {
|
||||
User string `json:"user"`
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
// Validate checks the values of the SSHBastionRequest.
|
||||
func (r *SSHBastionRequest) Validate() error {
|
||||
if r.Hostname == "" {
|
||||
return errs.BadRequest("missing or empty hostname")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSHBastionResponse is the response body used to return the bastion for a
|
||||
// given host.
|
||||
type SSHBastionResponse struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Bastion *config.Bastion `json:"bastion,omitempty"`
|
||||
}
|
||||
|
||||
// SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token
|
||||
// (ott) from the body and creates a new SSH certificate with the information in
|
||||
// the request.
|
||||
func SSHSign(w http.ResponseWriter, r *http.Request) {
|
||||
var body SSHSignRequest
|
||||
if err := read.JSON(r.Body, &body); err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
|
||||
logOtt(w, body.OTT)
|
||||
if err := body.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
publicKey, err := ssh.ParsePublicKey(body.PublicKey)
|
||||
if err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error parsing publicKey"))
|
||||
return
|
||||
}
|
||||
|
||||
var addUserPublicKey ssh.PublicKey
|
||||
if body.AddUserPublicKey != nil {
|
||||
addUserPublicKey, err = ssh.ParsePublicKey(body.AddUserPublicKey)
|
||||
if err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error parsing addUserPublicKey"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
opts := provisioner.SignSSHOptions{
|
||||
CertType: body.CertType,
|
||||
KeyID: body.KeyID,
|
||||
Principals: body.Principals,
|
||||
ValidBefore: body.ValidBefore,
|
||||
ValidAfter: body.ValidAfter,
|
||||
TemplateData: body.TemplateData,
|
||||
}
|
||||
|
||||
ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHSignMethod)
|
||||
ctx = provisioner.NewContextWithToken(ctx, body.OTT)
|
||||
|
||||
a := mustAuthority(ctx)
|
||||
signOpts, err := a.Authorize(ctx, body.OTT)
|
||||
if err != nil {
|
||||
render.Error(w, errs.UnauthorizedErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
cert, err := a.SignSSH(ctx, publicKey, opts, signOpts...)
|
||||
if err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error signing ssh certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
var addUserCertificate *SSHCertificate
|
||||
if addUserPublicKey != nil && authority.IsValidForAddUser(cert) == nil {
|
||||
addUserCert, err := a.SignSSHAddUser(ctx, addUserPublicKey, cert)
|
||||
if err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error signing ssh certificate"))
|
||||
return
|
||||
}
|
||||
addUserCertificate = &SSHCertificate{addUserCert}
|
||||
}
|
||||
|
||||
// Sign identity certificate if available.
|
||||
var identityCertificate []Certificate
|
||||
if cr := body.IdentityCSR.CertificateRequest; cr != nil {
|
||||
ctx := authority.NewContextWithSkipTokenReuse(r.Context())
|
||||
ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)
|
||||
signOpts, err := a.Authorize(ctx, body.OTT)
|
||||
if err != nil {
|
||||
render.Error(w, errs.UnauthorizedErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
// Enforce the same duration as ssh certificate.
|
||||
signOpts = append(signOpts, &identityModifier{
|
||||
NotBefore: time.Unix(int64(cert.ValidAfter), 0),
|
||||
NotAfter: time.Unix(int64(cert.ValidBefore), 0),
|
||||
})
|
||||
|
||||
certChain, err := a.Sign(cr, provisioner.SignOptions{}, signOpts...)
|
||||
if err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error signing identity certificate"))
|
||||
return
|
||||
}
|
||||
identityCertificate = certChainToPEM(certChain)
|
||||
}
|
||||
|
||||
LogSSHCertificate(w, cert)
|
||||
render.JSONStatus(w, &SSHSignResponse{
|
||||
Certificate: SSHCertificate{cert},
|
||||
AddUserCertificate: addUserCertificate,
|
||||
IdentityCertificate: identityCertificate,
|
||||
}, http.StatusCreated)
|
||||
}
|
||||
|
||||
// SSHRoots is an HTTP handler that returns the SSH public keys for user and host
|
||||
// certificates.
|
||||
func SSHRoots(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
keys, err := mustAuthority(ctx).GetSSHRoots(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, errs.InternalServerErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 {
|
||||
render.Error(w, errs.NotFound("no keys found"))
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(SSHRootsResponse)
|
||||
for _, k := range keys.HostKeys {
|
||||
resp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k})
|
||||
}
|
||||
for _, k := range keys.UserKeys {
|
||||
resp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k})
|
||||
}
|
||||
|
||||
render.JSON(w, resp)
|
||||
}
|
||||
|
||||
// SSHFederation is an HTTP handler that returns the federated SSH public keys
|
||||
// for user and host certificates.
|
||||
func SSHFederation(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
keys, err := mustAuthority(ctx).GetSSHFederation(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, errs.InternalServerErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 {
|
||||
render.Error(w, errs.NotFound("no keys found"))
|
||||
return
|
||||
}
|
||||
|
||||
resp := new(SSHRootsResponse)
|
||||
for _, k := range keys.HostKeys {
|
||||
resp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k})
|
||||
}
|
||||
for _, k := range keys.UserKeys {
|
||||
resp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k})
|
||||
}
|
||||
|
||||
render.JSON(w, resp)
|
||||
}
|
||||
|
||||
// SSHConfig is an HTTP handler that returns rendered templates for ssh clients
|
||||
// and servers.
|
||||
func SSHConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var body SSHConfigRequest
|
||||
if err := read.JSON(r.Body, &body); err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ts, err := mustAuthority(ctx).GetSSHConfig(ctx, body.Type, body.Data)
|
||||
if err != nil {
|
||||
render.Error(w, errs.InternalServerErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
var cfg SSHConfigResponse
|
||||
switch body.Type {
|
||||
case provisioner.SSHUserCert:
|
||||
cfg.UserTemplates = ts
|
||||
case provisioner.SSHHostCert:
|
||||
cfg.HostTemplates = ts
|
||||
default:
|
||||
render.Error(w, errs.InternalServer("it should hot get here"))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, cfg)
|
||||
}
|
||||
|
||||
// SSHCheckHost is the HTTP handler that returns if a hosts certificate exists or not.
|
||||
func SSHCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||
var body SSHCheckPrincipalRequest
|
||||
if err := read.JSON(r.Body, &body); err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
exists, err := mustAuthority(ctx).CheckSSHHost(ctx, body.Principal, body.Token)
|
||||
if err != nil {
|
||||
render.Error(w, errs.InternalServerErr(err))
|
||||
return
|
||||
}
|
||||
render.JSON(w, &SSHCheckPrincipalResponse{
|
||||
Exists: exists,
|
||||
})
|
||||
}
|
||||
|
||||
// SSHGetHosts is the HTTP handler that returns a list of valid ssh hosts.
|
||||
func SSHGetHosts(w http.ResponseWriter, r *http.Request) {
|
||||
var cert *x509.Certificate
|
||||
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
|
||||
cert = r.TLS.PeerCertificates[0]
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
hosts, err := mustAuthority(ctx).GetSSHHosts(ctx, cert)
|
||||
if err != nil {
|
||||
render.Error(w, errs.InternalServerErr(err))
|
||||
return
|
||||
}
|
||||
render.JSON(w, &SSHGetHostsResponse{
|
||||
Hosts: hosts,
|
||||
})
|
||||
}
|
||||
|
||||
// SSHBastion provides returns the bastion configured if any.
|
||||
func SSHBastion(w http.ResponseWriter, r *http.Request) {
|
||||
var body SSHBastionRequest
|
||||
if err := read.JSON(r.Body, &body); err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
bastion, err := mustAuthority(ctx).GetSSHBastion(ctx, body.User, body.Hostname)
|
||||
if err != nil {
|
||||
render.Error(w, errs.InternalServerErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, &SSHBastionResponse{
|
||||
Hostname: body.Hostname,
|
||||
Bastion: bastion,
|
||||
})
|
||||
}
|
||||
|
||||
// identityModifier is a custom modifier used to force a fixed duration.
|
||||
type identityModifier struct {
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
}
|
||||
|
||||
func (m *identityModifier) Enforce(cert *x509.Certificate) error {
|
||||
cert.NotBefore = m.NotBefore
|
||||
cert.NotAfter = m.NotAfter
|
||||
return nil
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
// SSHRekeyRequest is the request body of an SSH certificate request.
|
||||
type SSHRekeyRequest struct {
|
||||
OTT string `json:"ott"`
|
||||
PublicKey []byte `json:"publicKey"` //base64 encoded
|
||||
}
|
||||
|
||||
// Validate validates the SSHSignRekey.
|
||||
func (s *SSHRekeyRequest) Validate() error {
|
||||
switch {
|
||||
case s.OTT == "":
|
||||
return errs.BadRequest("missing or empty ott")
|
||||
case len(s.PublicKey) == 0:
|
||||
return errs.BadRequest("missing or empty public key")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SSHRekeyResponse is the response object that returns the SSH certificate.
|
||||
type SSHRekeyResponse struct {
|
||||
Certificate SSHCertificate `json:"crt"`
|
||||
IdentityCertificate []Certificate `json:"identityCrt,omitempty"`
|
||||
}
|
||||
|
||||
// SSHRekey is an HTTP handler that reads an RekeySSHRequest with a one-time-token
|
||||
// (ott) from the body and creates a new SSH certificate with the information in
|
||||
// the request.
|
||||
func SSHRekey(w http.ResponseWriter, r *http.Request) {
|
||||
var body SSHRekeyRequest
|
||||
if err := read.JSON(r.Body, &body); err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
|
||||
logOtt(w, body.OTT)
|
||||
if err := body.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
publicKey, err := ssh.ParsePublicKey(body.PublicKey)
|
||||
if err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error parsing publicKey"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRekeyMethod)
|
||||
ctx = provisioner.NewContextWithToken(ctx, body.OTT)
|
||||
|
||||
a := mustAuthority(ctx)
|
||||
signOpts, err := a.Authorize(ctx, body.OTT)
|
||||
if err != nil {
|
||||
render.Error(w, errs.UnauthorizedErr(err))
|
||||
return
|
||||
}
|
||||
oldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT)
|
||||
if err != nil {
|
||||
render.Error(w, errs.InternalServerErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
newCert, err := a.RekeySSH(ctx, oldCert, publicKey, signOpts...)
|
||||
if err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error rekeying ssh certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
// Match identity cert with the SSH cert
|
||||
notBefore := time.Unix(int64(oldCert.ValidAfter), 0)
|
||||
notAfter := time.Unix(int64(oldCert.ValidBefore), 0)
|
||||
|
||||
identity, err := renewIdentityCertificate(r, notBefore, notAfter)
|
||||
if err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error renewing identity certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
LogSSHCertificate(w, newCert)
|
||||
render.JSONStatus(w, &SSHRekeyResponse{
|
||||
Certificate: SSHCertificate{newCert},
|
||||
IdentityCertificate: identity,
|
||||
}, http.StatusCreated)
|
||||
}
|
118
api/sshRenew.go
118
api/sshRenew.go
|
@ -1,118 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
// SSHRenewRequest is the request body of an SSH certificate request.
|
||||
type SSHRenewRequest struct {
|
||||
OTT string `json:"ott"`
|
||||
}
|
||||
|
||||
// Validate validates the SSHSignRequest.
|
||||
func (s *SSHRenewRequest) Validate() error {
|
||||
switch {
|
||||
case s.OTT == "":
|
||||
return errs.BadRequest("missing or empty ott")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SSHRenewResponse is the response object that returns the SSH certificate.
|
||||
type SSHRenewResponse struct {
|
||||
Certificate SSHCertificate `json:"crt"`
|
||||
IdentityCertificate []Certificate `json:"identityCrt,omitempty"`
|
||||
}
|
||||
|
||||
// SSHRenew is an HTTP handler that reads an RenewSSHRequest with a one-time-token
|
||||
// (ott) from the body and creates a new SSH certificate with the information in
|
||||
// the request.
|
||||
func SSHRenew(w http.ResponseWriter, r *http.Request) {
|
||||
var body SSHRenewRequest
|
||||
if err := read.JSON(r.Body, &body); err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
|
||||
logOtt(w, body.OTT)
|
||||
if err := body.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRenewMethod)
|
||||
ctx = provisioner.NewContextWithToken(ctx, body.OTT)
|
||||
|
||||
a := mustAuthority(ctx)
|
||||
_, err := a.Authorize(ctx, body.OTT)
|
||||
if err != nil {
|
||||
render.Error(w, errs.UnauthorizedErr(err))
|
||||
return
|
||||
}
|
||||
oldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT)
|
||||
if err != nil {
|
||||
render.Error(w, errs.InternalServerErr(err))
|
||||
return
|
||||
}
|
||||
|
||||
newCert, err := a.RenewSSH(ctx, oldCert)
|
||||
if err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error renewing ssh certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
// Match identity cert with the SSH cert
|
||||
notBefore := time.Unix(int64(oldCert.ValidAfter), 0)
|
||||
notAfter := time.Unix(int64(oldCert.ValidBefore), 0)
|
||||
|
||||
identity, err := renewIdentityCertificate(r, notBefore, notAfter)
|
||||
if err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error renewing identity certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
LogSSHCertificate(w, newCert)
|
||||
render.JSONStatus(w, &SSHSignResponse{
|
||||
Certificate: SSHCertificate{newCert},
|
||||
IdentityCertificate: identity,
|
||||
}, http.StatusCreated)
|
||||
}
|
||||
|
||||
// renewIdentityCertificate request the client TLS certificate if present. If notBefore and notAfter are passed the
|
||||
func renewIdentityCertificate(r *http.Request, notBefore, notAfter time.Time) ([]Certificate, error) {
|
||||
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Clone the certificate as we can modify it.
|
||||
cert, err := x509.ParseCertificate(r.TLS.PeerCertificates[0].Raw)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing client certificate")
|
||||
}
|
||||
|
||||
// Enforce the cert to match another certificate, for example an ssh
|
||||
// certificate.
|
||||
if !notBefore.IsZero() {
|
||||
cert.NotBefore = notBefore
|
||||
}
|
||||
if !notAfter.IsZero() {
|
||||
cert.NotAfter = notAfter
|
||||
}
|
||||
|
||||
certChain, err := mustAuthority(r.Context()).Renew(cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certChainToPEM(certChain), nil
|
||||
}
|
103
api/sshRevoke.go
103
api/sshRevoke.go
|
@ -1,103 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
// SSHRevokeResponse is the response object that returns the health of the server.
|
||||
type SSHRevokeResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// SSHRevokeRequest is the request body for a revocation request.
|
||||
type SSHRevokeRequest struct {
|
||||
Serial string `json:"serial"`
|
||||
OTT string `json:"ott"`
|
||||
ReasonCode int `json:"reasonCode"`
|
||||
Reason string `json:"reason"`
|
||||
Passive bool `json:"passive"`
|
||||
}
|
||||
|
||||
// Validate checks the fields of the RevokeRequest and returns nil if they are ok
|
||||
// or an error if something is wrong.
|
||||
func (r *SSHRevokeRequest) Validate() (err error) {
|
||||
if r.Serial == "" {
|
||||
return errs.BadRequest("missing serial")
|
||||
}
|
||||
if r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise {
|
||||
return errs.BadRequest("reasonCode out of bounds")
|
||||
}
|
||||
if !r.Passive {
|
||||
return errs.NotImplemented("non-passive revocation not implemented")
|
||||
}
|
||||
if r.OTT == "" {
|
||||
return errs.BadRequest("missing ott")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Revoke supports handful of different methods that revoke a Certificate.
|
||||
//
|
||||
// NOTE: currently only Passive revocation is supported.
|
||||
func SSHRevoke(w http.ResponseWriter, r *http.Request) {
|
||||
var body SSHRevokeRequest
|
||||
if err := read.JSON(r.Body, &body); err != nil {
|
||||
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := body.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
opts := &authority.RevokeOptions{
|
||||
Serial: body.Serial,
|
||||
Reason: body.Reason,
|
||||
ReasonCode: body.ReasonCode,
|
||||
PassiveOnly: body.Passive,
|
||||
}
|
||||
|
||||
ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRevokeMethod)
|
||||
a := mustAuthority(ctx)
|
||||
|
||||
// A token indicates that we are using the api via a provisioner token,
|
||||
// otherwise it is assumed that the certificate is revoking itself over mTLS.
|
||||
logOtt(w, body.OTT)
|
||||
|
||||
if _, err := a.Authorize(ctx, body.OTT); err != nil {
|
||||
render.Error(w, errs.UnauthorizedErr(err))
|
||||
return
|
||||
}
|
||||
opts.OTT = body.OTT
|
||||
|
||||
if err := a.Revoke(ctx, opts); err != nil {
|
||||
render.Error(w, errs.ForbiddenErr(err, "error revoking ssh certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
logSSHRevoke(w, opts)
|
||||
render.JSON(w, &SSHRevokeResponse{Status: "ok"})
|
||||
}
|
||||
|
||||
func logSSHRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"serial": ri.Serial,
|
||||
"reasonCode": ri.ReasonCode,
|
||||
"reason": ri.Reason,
|
||||
"passiveOnly": ri.PassiveOnly,
|
||||
"mTLS": ri.MTLS,
|
||||
"ssh": true,
|
||||
})
|
||||
}
|
||||
}
|
738
api/ssh_test.go
738
api/ssh_test.go
|
@ -1,738 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
"github.com/smallstep/certificates/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
sshSignerKey = mustKey()
|
||||
sshUserKey = mustKey()
|
||||
sshHostKey = mustKey()
|
||||
)
|
||||
|
||||
func mustKey() *ecdsa.PrivateKey {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return priv
|
||||
}
|
||||
|
||||
func signSSHCertificate(cert *ssh.Certificate) error {
|
||||
signerKey, err := ssh.NewPublicKey(sshSignerKey.Public())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signer, err := ssh.NewSignerFromSigner(sshSignerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.SignatureKey = signerKey
|
||||
data := cert.Marshal()
|
||||
data = data[:len(data)-4]
|
||||
sig, err := signer.Sign(rand.Reader, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.Signature = sig
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSignedUserCertificate() (*ssh.Certificate, error) {
|
||||
key, err := ssh.NewPublicKey(sshUserKey.Public())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := time.Now()
|
||||
cert := &ssh.Certificate{
|
||||
Nonce: []byte("1234567890"),
|
||||
Key: key,
|
||||
Serial: 1234567890,
|
||||
CertType: ssh.UserCert,
|
||||
KeyId: "user@localhost",
|
||||
ValidPrincipals: []string{"user"},
|
||||
ValidAfter: uint64(t.Unix()),
|
||||
ValidBefore: uint64(t.Add(time.Hour).Unix()),
|
||||
Permissions: ssh.Permissions{
|
||||
CriticalOptions: map[string]string{},
|
||||
Extensions: map[string]string{
|
||||
"permit-X11-forwarding": "",
|
||||
"permit-agent-forwarding": "",
|
||||
"permit-port-forwarding": "",
|
||||
"permit-pty": "",
|
||||
"permit-user-rc": "",
|
||||
},
|
||||
},
|
||||
Reserved: []byte{},
|
||||
}
|
||||
if err := signSSHCertificate(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func getSignedHostCertificate() (*ssh.Certificate, error) {
|
||||
key, err := ssh.NewPublicKey(sshHostKey.Public())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := time.Now()
|
||||
cert := &ssh.Certificate{
|
||||
Nonce: []byte("1234567890"),
|
||||
Key: key,
|
||||
Serial: 1234567890,
|
||||
CertType: ssh.UserCert,
|
||||
KeyId: "internal.smallstep.com",
|
||||
ValidPrincipals: []string{"internal.smallstep.com"},
|
||||
ValidAfter: uint64(t.Unix()),
|
||||
ValidBefore: uint64(t.Add(time.Hour).Unix()),
|
||||
Permissions: ssh.Permissions{
|
||||
CriticalOptions: map[string]string{},
|
||||
Extensions: map[string]string{},
|
||||
},
|
||||
Reserved: []byte{},
|
||||
}
|
||||
if err := signSSHCertificate(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func TestSSHCertificate_MarshalJSON(t *testing.T) {
|
||||
user, err := getSignedUserCertificate()
|
||||
assert.FatalError(t, err)
|
||||
host, err := getSignedHostCertificate()
|
||||
assert.FatalError(t, err)
|
||||
userB64 := base64.StdEncoding.EncodeToString(user.Marshal())
|
||||
hostB64 := base64.StdEncoding.EncodeToString(host.Marshal())
|
||||
|
||||
type fields struct {
|
||||
Certificate *ssh.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{"nil", fields{Certificate: nil}, []byte("null"), false},
|
||||
{"user", fields{Certificate: user}, []byte(`"` + userB64 + `"`), false},
|
||||
{"user", fields{Certificate: host}, []byte(`"` + hostB64 + `"`), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := SSHCertificate{
|
||||
Certificate: tt.fields.Certificate,
|
||||
}
|
||||
got, err := c.MarshalJSON()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SSHCertificate.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SSHCertificate.MarshalJSON() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHCertificate_UnmarshalJSON(t *testing.T) {
|
||||
user, err := getSignedUserCertificate()
|
||||
assert.FatalError(t, err)
|
||||
host, err := getSignedHostCertificate()
|
||||
assert.FatalError(t, err)
|
||||
userB64 := base64.StdEncoding.EncodeToString(user.Marshal())
|
||||
hostB64 := base64.StdEncoding.EncodeToString(host.Marshal())
|
||||
keyB64 := base64.StdEncoding.EncodeToString(user.Key.Marshal())
|
||||
|
||||
type args struct {
|
||||
data []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *ssh.Certificate
|
||||
wantErr bool
|
||||
}{
|
||||
{"null", args{[]byte(`null`)}, nil, false},
|
||||
{"empty", args{[]byte(`""`)}, nil, false},
|
||||
{"user", args{[]byte(`"` + userB64 + `"`)}, user, false},
|
||||
{"host", args{[]byte(`"` + hostB64 + `"`)}, host, false},
|
||||
{"bad-string", args{[]byte(userB64)}, nil, true},
|
||||
{"bad-base64", args{[]byte(`"this-is-not-base64"`)}, nil, true},
|
||||
{"bad-key", args{[]byte(`"bm90LWEta2V5"`)}, nil, true},
|
||||
{"bat-cert", args{[]byte(`"` + keyB64 + `"`)}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &SSHCertificate{}
|
||||
if err := c.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
|
||||
t.Errorf("SSHCertificate.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.want, c.Certificate) {
|
||||
t.Errorf("SSHCertificate.UnmarshalJSON() got = %v, want %v\n", c.Certificate, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignSSHRequest_Validate(t *testing.T) {
|
||||
csr := parseCertificateRequest(csrPEM)
|
||||
badCSR := parseCertificateRequest(csrPEM)
|
||||
badCSR.SignatureAlgorithm = x509.SHA1WithRSA
|
||||
|
||||
type fields struct {
|
||||
PublicKey []byte
|
||||
OTT string
|
||||
CertType string
|
||||
Principals []string
|
||||
ValidAfter TimeDuration
|
||||
ValidBefore TimeDuration
|
||||
AddUserPublicKey []byte
|
||||
KeyID string
|
||||
IdentityCSR CertificateRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok-empty", fields{[]byte("Zm9v"), "ott", "", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, false},
|
||||
{"ok-user", fields{[]byte("Zm9v"), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, false},
|
||||
{"ok-host", fields{[]byte("Zm9v"), "ott", "host", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, false},
|
||||
{"ok-keyID", fields{[]byte("Zm9v"), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "key-id", CertificateRequest{}}, false},
|
||||
{"ok-identityCSR", fields{[]byte("Zm9v"), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "key-id", CertificateRequest{CertificateRequest: csr}}, false},
|
||||
{"key", fields{nil, "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, true},
|
||||
{"key", fields{[]byte(""), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, true},
|
||||
{"type", fields{[]byte("Zm9v"), "ott", "foo", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, true},
|
||||
{"ott", fields{[]byte("Zm9v"), "", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "", CertificateRequest{}}, true},
|
||||
{"identityCSR", fields{[]byte("Zm9v"), "ott", "user", []string{"user"}, TimeDuration{}, TimeDuration{}, nil, "key-id", CertificateRequest{CertificateRequest: badCSR}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SSHSignRequest{
|
||||
PublicKey: tt.fields.PublicKey,
|
||||
OTT: tt.fields.OTT,
|
||||
CertType: tt.fields.CertType,
|
||||
Principals: tt.fields.Principals,
|
||||
ValidAfter: tt.fields.ValidAfter,
|
||||
ValidBefore: tt.fields.ValidBefore,
|
||||
AddUserPublicKey: tt.fields.AddUserPublicKey,
|
||||
KeyID: tt.fields.KeyID,
|
||||
IdentityCSR: tt.fields.IdentityCSR,
|
||||
}
|
||||
if err := s.Validate(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("SignSSHRequest.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SSHSign(t *testing.T) {
|
||||
user, err := getSignedUserCertificate()
|
||||
assert.FatalError(t, err)
|
||||
host, err := getSignedHostCertificate()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
userB64 := base64.StdEncoding.EncodeToString(user.Marshal())
|
||||
hostB64 := base64.StdEncoding.EncodeToString(host.Marshal())
|
||||
|
||||
userReq, err := json.Marshal(SSHSignRequest{
|
||||
PublicKey: user.Key.Marshal(),
|
||||
OTT: "ott",
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
hostReq, err := json.Marshal(SSHSignRequest{
|
||||
PublicKey: host.Key.Marshal(),
|
||||
OTT: "ott",
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
userAddReq, err := json.Marshal(SSHSignRequest{
|
||||
PublicKey: user.Key.Marshal(),
|
||||
OTT: "ott",
|
||||
AddUserPublicKey: user.Key.Marshal(),
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
userIdentityReq, err := json.Marshal(SSHSignRequest{
|
||||
PublicKey: user.Key.Marshal(),
|
||||
OTT: "ott",
|
||||
IdentityCSR: CertificateRequest{parseCertificateRequest(csrPEM)},
|
||||
})
|
||||
assert.FatalError(t, err)
|
||||
identityCerts := []*x509.Certificate{
|
||||
parseCertificate(certPEM),
|
||||
}
|
||||
identityCertsPEM := []byte(`"` + strings.ReplaceAll(certPEM, "\n", `\n`) + `\n"`)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req []byte
|
||||
authErr error
|
||||
signCert *ssh.Certificate
|
||||
signErr error
|
||||
addUserCert *ssh.Certificate
|
||||
addUserErr error
|
||||
tlsSignCerts []*x509.Certificate
|
||||
tlsSignErr error
|
||||
body []byte
|
||||
statusCode int
|
||||
}{
|
||||
{"ok-user", userReq, nil, user, nil, nil, nil, nil, nil, []byte(fmt.Sprintf(`{"crt":%q}`, userB64)), http.StatusCreated},
|
||||
{"ok-host", hostReq, nil, host, nil, nil, nil, nil, nil, []byte(fmt.Sprintf(`{"crt":%q}`, hostB64)), http.StatusCreated},
|
||||
{"ok-user-add", userAddReq, nil, user, nil, user, nil, nil, nil, []byte(fmt.Sprintf(`{"crt":%q,"addUserCrt":%q}`, userB64, userB64)), http.StatusCreated},
|
||||
{"ok-user-identity", userIdentityReq, nil, user, nil, user, nil, identityCerts, nil, []byte(fmt.Sprintf(`{"crt":%q,"identityCrt":[%s]}`, userB64, identityCertsPEM)), http.StatusCreated},
|
||||
{"fail-body", []byte("bad-json"), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest},
|
||||
{"fail-validate", []byte("{}"), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest},
|
||||
{"fail-publicKey", []byte(`{"publicKey":"Zm9v","ott":"ott"}`), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest},
|
||||
{"fail-publicKey", []byte(fmt.Sprintf(`{"publicKey":%q,"ott":"ott","addUserPublicKey":"Zm9v"}`, base64.StdEncoding.EncodeToString(user.Key.Marshal()))), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest},
|
||||
{"fail-authorize", userReq, fmt.Errorf("an-error"), nil, nil, nil, nil, nil, nil, nil, http.StatusUnauthorized},
|
||||
{"fail-signSSH", userReq, nil, nil, fmt.Errorf("an-error"), nil, nil, nil, nil, nil, http.StatusForbidden},
|
||||
{"fail-SignSSHAddUser", userAddReq, nil, user, nil, nil, fmt.Errorf("an-error"), nil, nil, nil, http.StatusForbidden},
|
||||
{"fail-user-identity", userIdentityReq, nil, user, nil, user, nil, nil, fmt.Errorf("an-error"), nil, http.StatusForbidden},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockMustAuthority(t, &mockAuthority{
|
||||
authorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||
return []provisioner.SignOption{}, tt.authErr
|
||||
},
|
||||
signSSH: func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
|
||||
return tt.signCert, tt.signErr
|
||||
},
|
||||
signSSHAddUser: func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) {
|
||||
return tt.addUserCert, tt.addUserErr
|
||||
},
|
||||
sign: func(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
|
||||
return tt.tlsSignCerts, tt.tlsSignErr
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("POST", "http://example.com/ssh/sign", bytes.NewReader(tt.req))
|
||||
w := httptest.NewRecorder()
|
||||
SSHSign(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.SignSSH StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("caHandler.SignSSH unexpected error = %v", err)
|
||||
}
|
||||
if tt.statusCode < http.StatusBadRequest {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), tt.body) {
|
||||
t.Errorf("caHandler.SignSSH Body = %s, wants %s", body, tt.body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SSHRoots(t *testing.T) {
|
||||
user, err := ssh.NewPublicKey(sshUserKey.Public())
|
||||
assert.FatalError(t, err)
|
||||
userB64 := base64.StdEncoding.EncodeToString(user.Marshal())
|
||||
|
||||
host, err := ssh.NewPublicKey(sshHostKey.Public())
|
||||
assert.FatalError(t, err)
|
||||
hostB64 := base64.StdEncoding.EncodeToString(host.Marshal())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
keys *authority.SSHKeys
|
||||
keysErr error
|
||||
body []byte
|
||||
statusCode int
|
||||
}{
|
||||
{"ok", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}, UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{"userKey":[%q],"hostKey":[%q]}`, userB64, hostB64)), http.StatusOK},
|
||||
{"many", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host, host}, UserKeys: []ssh.PublicKey{user, user}}, nil, []byte(fmt.Sprintf(`{"userKey":[%q,%q],"hostKey":[%q,%q]}`, userB64, userB64, hostB64, hostB64)), http.StatusOK},
|
||||
{"user", &authority.SSHKeys{UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{"userKey":[%q]}`, userB64)), http.StatusOK},
|
||||
{"host", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}}, nil, []byte(fmt.Sprintf(`{"hostKey":[%q]}`, hostB64)), http.StatusOK},
|
||||
{"empty", &authority.SSHKeys{}, nil, nil, http.StatusNotFound},
|
||||
{"error", nil, fmt.Errorf("an error"), nil, http.StatusInternalServerError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockMustAuthority(t, &mockAuthority{
|
||||
getSSHRoots: func(ctx context.Context) (*authority.SSHKeys, error) {
|
||||
return tt.keys, tt.keysErr
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/ssh/roots", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
SSHRoots(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.SSHRoots StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("caHandler.SSHRoots unexpected error = %v", err)
|
||||
}
|
||||
if tt.statusCode < http.StatusBadRequest {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), tt.body) {
|
||||
t.Errorf("caHandler.SSHRoots Body = %s, wants %s", body, tt.body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SSHFederation(t *testing.T) {
|
||||
user, err := ssh.NewPublicKey(sshUserKey.Public())
|
||||
assert.FatalError(t, err)
|
||||
userB64 := base64.StdEncoding.EncodeToString(user.Marshal())
|
||||
|
||||
host, err := ssh.NewPublicKey(sshHostKey.Public())
|
||||
assert.FatalError(t, err)
|
||||
hostB64 := base64.StdEncoding.EncodeToString(host.Marshal())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
keys *authority.SSHKeys
|
||||
keysErr error
|
||||
body []byte
|
||||
statusCode int
|
||||
}{
|
||||
{"ok", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}, UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{"userKey":[%q],"hostKey":[%q]}`, userB64, hostB64)), http.StatusOK},
|
||||
{"many", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host, host}, UserKeys: []ssh.PublicKey{user, user}}, nil, []byte(fmt.Sprintf(`{"userKey":[%q,%q],"hostKey":[%q,%q]}`, userB64, userB64, hostB64, hostB64)), http.StatusOK},
|
||||
{"user", &authority.SSHKeys{UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{"userKey":[%q]}`, userB64)), http.StatusOK},
|
||||
{"host", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}}, nil, []byte(fmt.Sprintf(`{"hostKey":[%q]}`, hostB64)), http.StatusOK},
|
||||
{"empty", &authority.SSHKeys{}, nil, nil, http.StatusNotFound},
|
||||
{"error", nil, fmt.Errorf("an error"), nil, http.StatusInternalServerError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockMustAuthority(t, &mockAuthority{
|
||||
getSSHFederation: func(ctx context.Context) (*authority.SSHKeys, error) {
|
||||
return tt.keys, tt.keysErr
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/ssh/federation", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
SSHFederation(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.SSHFederation StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("caHandler.SSHFederation unexpected error = %v", err)
|
||||
}
|
||||
if tt.statusCode < http.StatusBadRequest {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), tt.body) {
|
||||
t.Errorf("caHandler.SSHFederation Body = %s, wants %s", body, tt.body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SSHConfig(t *testing.T) {
|
||||
userOutput := []templates.Output{
|
||||
{Name: "config.tpl", Type: templates.File, Comment: "#", Path: "ssh/config", Content: []byte("UserKnownHostsFile /home/user/.step/ssh/known_hosts")},
|
||||
{Name: "known_host.tpl", Type: templates.File, Comment: "#", Path: "ssh/known_host", Content: []byte("@cert-authority * ecdsa-sha2-nistp256 AAAA...=")},
|
||||
}
|
||||
hostOutput := []templates.Output{
|
||||
{Name: "sshd_config.tpl", Type: templates.Snippet, Comment: "#", Path: "/etc/ssh/sshd_config", Content: []byte("TrustedUserCAKeys /etc/ssh/ca.pub")},
|
||||
{Name: "ca.tpl", Type: templates.File, Comment: "#", Path: "/etc/ssh/ca.pub", Content: []byte("ecdsa-sha2-nistp256 AAAA...=")},
|
||||
}
|
||||
userJSON, err := json.Marshal(userOutput)
|
||||
assert.FatalError(t, err)
|
||||
hostJSON, err := json.Marshal(hostOutput)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req string
|
||||
output []templates.Output
|
||||
err error
|
||||
body []byte
|
||||
statusCode int
|
||||
}{
|
||||
{"user", `{"type":"user"}`, userOutput, nil, []byte(fmt.Sprintf(`{"userTemplates":%s}`, userJSON)), http.StatusOK},
|
||||
{"host", `{"type":"host"}`, hostOutput, nil, []byte(fmt.Sprintf(`{"hostTemplates":%s}`, hostJSON)), http.StatusOK},
|
||||
{"noType", `{}`, userOutput, nil, []byte(fmt.Sprintf(`{"userTemplates":%s}`, userJSON)), http.StatusOK},
|
||||
{"badType", `{"type":"bad"}`, userOutput, nil, nil, http.StatusBadRequest},
|
||||
{"badData", `{"type":"user","data":{"bad"}}`, userOutput, nil, nil, http.StatusBadRequest},
|
||||
{"error", `{"type": "user"}`, nil, fmt.Errorf("an error"), nil, http.StatusInternalServerError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockMustAuthority(t, &mockAuthority{
|
||||
getSSHConfig: func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) {
|
||||
return tt.output, tt.err
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/ssh/config", strings.NewReader(tt.req))
|
||||
w := httptest.NewRecorder()
|
||||
SSHConfig(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.SSHConfig StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("caHandler.SSHConfig unexpected error = %v", err)
|
||||
}
|
||||
if tt.statusCode < http.StatusBadRequest {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), tt.body) {
|
||||
t.Errorf("caHandler.SSHConfig Body = %s, wants %s", body, tt.body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SSHCheckHost(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req string
|
||||
exists bool
|
||||
err error
|
||||
body []byte
|
||||
statusCode int
|
||||
}{
|
||||
{"true", `{"type":"host","principal":"foo.example.com"}`, true, nil, []byte(`{"exists":true}`), http.StatusOK},
|
||||
{"false", `{"type":"host","principal":"bar.example.com"}`, false, nil, []byte(`{"exists":false}`), http.StatusOK},
|
||||
{"badType", `{"type":"user","principal":"bar.example.com"}`, false, nil, nil, http.StatusBadRequest},
|
||||
{"badPrincipal", `{"type":"host","principal":""}`, false, nil, nil, http.StatusBadRequest},
|
||||
{"badRequest", `{"foo"}`, false, nil, nil, http.StatusBadRequest},
|
||||
{"error", `{"type":"host","principal":"foo.example.com"}`, false, fmt.Errorf("an error"), nil, http.StatusInternalServerError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockMustAuthority(t, &mockAuthority{
|
||||
checkSSHHost: func(ctx context.Context, principal, token string) (bool, error) {
|
||||
return tt.exists, tt.err
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/ssh/check-host", strings.NewReader(tt.req))
|
||||
w := httptest.NewRecorder()
|
||||
SSHCheckHost(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.SSHCheckHost StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("caHandler.SSHCheckHost unexpected error = %v", err)
|
||||
}
|
||||
if tt.statusCode < http.StatusBadRequest {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), tt.body) {
|
||||
t.Errorf("caHandler.SSHCheckHost Body = %s, wants %s", body, tt.body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SSHGetHosts(t *testing.T) {
|
||||
hosts := []authority.Host{
|
||||
{HostID: "1", HostTags: []authority.HostTag{{ID: "1", Name: "group", Value: "1"}}, Hostname: "host1"},
|
||||
{HostID: "2", HostTags: []authority.HostTag{{ID: "1", Name: "group", Value: "1"}, {ID: "2", Name: "group", Value: "2"}}, Hostname: "host2"},
|
||||
}
|
||||
hostsJSON, err := json.Marshal(hosts)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hosts []authority.Host
|
||||
err error
|
||||
body []byte
|
||||
statusCode int
|
||||
}{
|
||||
{"ok", hosts, nil, []byte(fmt.Sprintf(`{"hosts":%s}`, hostsJSON)), http.StatusOK},
|
||||
{"empty (array)", []authority.Host{}, nil, []byte(`{"hosts":[]}`), http.StatusOK},
|
||||
{"empty (nil)", nil, nil, []byte(`{"hosts":null}`), http.StatusOK},
|
||||
{"error", nil, fmt.Errorf("an error"), nil, http.StatusInternalServerError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockMustAuthority(t, &mockAuthority{
|
||||
getSSHHosts: func(context.Context, *x509.Certificate) ([]authority.Host, error) {
|
||||
return tt.hosts, tt.err
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/ssh/host", http.NoBody)
|
||||
w := httptest.NewRecorder()
|
||||
SSHGetHosts(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.SSHGetHosts StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("caHandler.SSHGetHosts unexpected error = %v", err)
|
||||
}
|
||||
if tt.statusCode < http.StatusBadRequest {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), tt.body) {
|
||||
t.Errorf("caHandler.SSHGetHosts Body = %s, wants %s", body, tt.body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SSHBastion(t *testing.T) {
|
||||
bastion := &authority.Bastion{
|
||||
Hostname: "bastion.local",
|
||||
}
|
||||
bastionPort := &authority.Bastion{
|
||||
Hostname: "bastion.local",
|
||||
Port: "2222",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
bastion *authority.Bastion
|
||||
bastionErr error
|
||||
req []byte
|
||||
body []byte
|
||||
statusCode int
|
||||
}{
|
||||
{"ok", bastion, nil, []byte(`{"hostname":"host.local"}`), []byte(`{"hostname":"host.local","bastion":{"hostname":"bastion.local"}}`), http.StatusOK},
|
||||
{"ok", bastionPort, nil, []byte(`{"hostname":"host.local","user":"user"}`), []byte(`{"hostname":"host.local","bastion":{"hostname":"bastion.local","port":"2222"}}`), http.StatusOK},
|
||||
{"empty", nil, nil, []byte(`{"hostname":"host.local"}`), []byte(`{"hostname":"host.local"}`), http.StatusOK},
|
||||
{"bad json", bastion, nil, []byte(`bad json`), nil, http.StatusBadRequest},
|
||||
{"bad request", bastion, nil, []byte(`{"hostname": ""}`), nil, http.StatusBadRequest},
|
||||
{"error", nil, fmt.Errorf("an error"), []byte(`{"hostname":"host.local"}`), nil, http.StatusInternalServerError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockMustAuthority(t, &mockAuthority{
|
||||
getSSHBastion: func(ctx context.Context, user, hostname string) (*authority.Bastion, error) {
|
||||
return tt.bastion, tt.bastionErr
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("POST", "http://example.com/ssh/bastion", bytes.NewReader(tt.req))
|
||||
w := httptest.NewRecorder()
|
||||
SSHBastion(logging.NewResponseLogger(w), req)
|
||||
res := w.Result()
|
||||
|
||||
if res.StatusCode != tt.statusCode {
|
||||
t.Errorf("caHandler.SSHBastion StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Errorf("caHandler.SSHBastion unexpected error = %v", err)
|
||||
}
|
||||
if tt.statusCode < http.StatusBadRequest {
|
||||
if !bytes.Equal(bytes.TrimSpace(body), tt.body) {
|
||||
t.Errorf("caHandler.SSHBastion Body = %s, wants %s", body, tt.body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHPublicKey_MarshalJSON(t *testing.T) {
|
||||
key, err := ssh.NewPublicKey(sshUserKey.Public())
|
||||
assert.FatalError(t, err)
|
||||
keyB64 := base64.StdEncoding.EncodeToString(key.Marshal())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
publicKey *SSHPublicKey
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", &SSHPublicKey{PublicKey: key}, []byte(`"` + keyB64 + `"`), false},
|
||||
{"null", nil, []byte("null"), false},
|
||||
{"null", &SSHPublicKey{PublicKey: nil}, []byte("null"), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.publicKey.MarshalJSON()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SSHPublicKey.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SSHPublicKey.MarshalJSON() = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHPublicKey_UnmarshalJSON(t *testing.T) {
|
||||
key, err := ssh.NewPublicKey(sshUserKey.Public())
|
||||
assert.FatalError(t, err)
|
||||
keyB64 := base64.StdEncoding.EncodeToString(key.Marshal())
|
||||
|
||||
type args struct {
|
||||
data []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *SSHPublicKey
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{[]byte(`"` + keyB64 + `"`)}, &SSHPublicKey{PublicKey: key}, false},
|
||||
{"empty", args{[]byte(`""`)}, &SSHPublicKey{}, false},
|
||||
{"null", args{[]byte(`null`)}, &SSHPublicKey{}, false},
|
||||
{"noString", args{[]byte("123")}, &SSHPublicKey{}, true},
|
||||
{"badB64", args{[]byte(`"bad"`)}, &SSHPublicKey{}, true},
|
||||
{"badKey", args{[]byte(`"Zm9vYmFyCg=="`)}, &SSHPublicKey{}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &SSHPublicKey{}
|
||||
if err := p.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
|
||||
t.Errorf("SSHPublicKey.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !reflect.DeepEqual(p, tt.want) {
|
||||
t.Errorf("SSHPublicKey.UnmarshalJSON() = %v, want %v", p, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
41
api/utils.go
Normal file
41
api/utils.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
// LogError adds to the response writer the given error if it implements
|
||||
// logging.ResponseLogger. If it does not implement it, then writes the error
|
||||
// using the log package.
|
||||
func LogError(rw http.ResponseWriter, err error) {
|
||||
if rl, ok := rw.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"error": err,
|
||||
})
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// JSON writes the passed value into the http.ResponseWriter.
|
||||
func JSON(w http.ResponseWriter, v interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
LogError(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadJSON reads JSON from the request body and stores it in the value
|
||||
// pointed by v.
|
||||
func ReadJSON(r io.Reader, v interface{}) error {
|
||||
if err := json.NewDecoder(r).Decode(v); err != nil {
|
||||
return BadRequest(errors.Wrap(err, "error decoding json"))
|
||||
}
|
||||
return nil
|
||||
}
|
124
api/utils_test.go
Normal file
124
api/utils_test.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
func TestLogError(t *testing.T) {
|
||||
theError := errors.New("the error")
|
||||
type args struct {
|
||||
rw http.ResponseWriter
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
withFields bool
|
||||
}{
|
||||
{"normalLogger", args{httptest.NewRecorder(), theError}, false},
|
||||
{"responseLogger", args{logging.NewResponseLogger(httptest.NewRecorder()), theError}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
LogError(tt.args.rw, tt.args.err)
|
||||
if tt.withFields {
|
||||
if rl, ok := tt.args.rw.(logging.ResponseLogger); ok {
|
||||
fields := rl.Fields()
|
||||
if !reflect.DeepEqual(fields["error"], theError) {
|
||||
t.Errorf("ResponseLogger[\"error\"] = %s, wants %s", fields["error"], theError)
|
||||
}
|
||||
} else {
|
||||
t.Error("ResponseWriter does not implement logging.ResponseLogger")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
type args struct {
|
||||
rw http.ResponseWriter
|
||||
v interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
ok bool
|
||||
}{
|
||||
{"ok", args{httptest.NewRecorder(), map[string]interface{}{"foo": "bar"}}, true},
|
||||
{"fail", args{httptest.NewRecorder(), make(chan int)}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rw := logging.NewResponseLogger(tt.args.rw)
|
||||
JSON(rw, tt.args.v)
|
||||
|
||||
rr, ok := tt.args.rw.(*httptest.ResponseRecorder)
|
||||
if !ok {
|
||||
t.Error("ResponseWriter does not implement *httptest.ResponseRecorder")
|
||||
return
|
||||
}
|
||||
|
||||
fields := rw.Fields()
|
||||
if tt.ok {
|
||||
if body := rr.Body.String(); body != "{\"foo\":\"bar\"}\n" {
|
||||
t.Errorf(`Unexpected body = %v, want {"foo":"bar"}`, body)
|
||||
}
|
||||
if len(fields) != 0 {
|
||||
t.Errorf("ResponseLogger fields = %v, wants 0 elements", fields)
|
||||
}
|
||||
} else {
|
||||
if body := rr.Body.String(); body != "" {
|
||||
t.Errorf("Unexpected body = %s, want empty string", body)
|
||||
}
|
||||
if len(fields) != 1 {
|
||||
t.Errorf("ResponseLogger fields = %v, wants 1 element", fields)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadJSON(t *testing.T) {
|
||||
type args struct {
|
||||
r io.Reader
|
||||
v interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{strings.NewReader(`{"foo":"bar"}`), make(map[string]interface{})}, false},
|
||||
{"fail", args{strings.NewReader(`{"foo"}`), make(map[string]interface{})}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ReadJSON(tt.args.r, &tt.args.v)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ReadJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantErr {
|
||||
e, ok := err.(*Error)
|
||||
if ok {
|
||||
if code := e.StatusCode(); code != 400 {
|
||||
t.Errorf("error.StatusCode() = %v, wants 400", code)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("error type = %T, wants *Error", err)
|
||||
}
|
||||
} else if !reflect.DeepEqual(tt.args.v, map[string]interface{}{"foo": "bar"}) {
|
||||
t.Errorf("ReadJSON value = %v, wants %v", tt.args.v, map[string]interface{}{"foo": "bar"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/api/render"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
)
|
||||
|
||||
// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests
|
||||
type CreateExternalAccountKeyRequest struct {
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
// Validate validates a new ACME EAB Key request body.
|
||||
func (r *CreateExternalAccountKeyRequest) Validate() error {
|
||||
if len(r.Reference) > 256 { // an arbitrary, but sensible (IMO), limit
|
||||
return fmt.Errorf("reference length %d exceeds the maximum (256)", len(r.Reference))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses
|
||||
type GetExternalAccountKeysResponse struct {
|
||||
EAKs []*linkedca.EABKey `json:"eaks"`
|
||||
NextCursor string `json:"nextCursor"`
|
||||
}
|
||||
|
||||
// requireEABEnabled is a middleware that ensures ACME EAB is enabled
|
||||
// before serving requests that act on ACME EAB credentials.
|
||||
func requireEABEnabled(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
prov := linkedca.MustProvisionerFromContext(ctx)
|
||||
|
||||
acmeProvisioner := prov.GetDetails().GetACME()
|
||||
if acmeProvisioner == nil {
|
||||
render.Error(w, admin.NewErrorISE("error getting ACME details for provisioner '%s'", prov.GetName()))
|
||||
return
|
||||
}
|
||||
|
||||
if !acmeProvisioner.RequireEab {
|
||||
render.Error(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner '%s'", prov.GetName()))
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// ACMEAdminResponder is responsible for writing ACME admin responses
|
||||
type ACMEAdminResponder interface {
|
||||
GetExternalAccountKeys(w http.ResponseWriter, r *http.Request)
|
||||
CreateExternalAccountKey(w http.ResponseWriter, r *http.Request)
|
||||
DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// acmeAdminResponder implements ACMEAdminResponder.
|
||||
type acmeAdminResponder struct{}
|
||||
|
||||
// NewACMEAdminResponder returns a new ACMEAdminResponder
|
||||
func NewACMEAdminResponder() ACMEAdminResponder {
|
||||
return &acmeAdminResponder{}
|
||||
}
|
||||
|
||||
// GetExternalAccountKeys writes the response for the EAB keys GET endpoint
|
||||
func (h *acmeAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, _ *http.Request) {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
||||
}
|
||||
|
||||
// CreateExternalAccountKey writes the response for the EAB key POST endpoint
|
||||
func (h *acmeAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, _ *http.Request) {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
||||
}
|
||||
|
||||
// DeleteExternalAccountKey writes the response for the EAB key DELETE endpoint
|
||||
func (h *acmeAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, _ *http.Request) {
|
||||
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
|
||||
}
|
||||
|
||||
func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey {
|
||||
if k == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
eak := &linkedca.EABKey{
|
||||
Id: k.ID,
|
||||
HmacKey: k.HmacKey,
|
||||
Provisioner: k.ProvisionerID,
|
||||
Reference: k.Reference,
|
||||
Account: k.AccountID,
|
||||
CreatedAt: timestamppb.New(k.CreatedAt),
|
||||
BoundAt: timestamppb.New(k.BoundAt),
|
||||
}
|
||||
|
||||
if k.Policy != nil {
|
||||
eak.Policy = &linkedca.Policy{
|
||||
X509: &linkedca.X509Policy{
|
||||
Allow: &linkedca.X509Names{},
|
||||
Deny: &linkedca.X509Names{},
|
||||
},
|
||||
}
|
||||
eak.Policy.X509.Allow.Dns = k.Policy.X509.Allowed.DNSNames
|
||||
eak.Policy.X509.Allow.Ips = k.Policy.X509.Allowed.IPRanges
|
||||
eak.Policy.X509.Deny.Dns = k.Policy.X509.Denied.DNSNames
|
||||
eak.Policy.X509.Deny.Ips = k.Policy.X509.Denied.IPRanges
|
||||
eak.Policy.X509.AllowWildcardNames = k.Policy.X509.AllowWildcardNames
|
||||
}
|
||||
|
||||
return eak
|
||||
}
|
||||
|
||||
func linkedEAKToCertificates(k *linkedca.EABKey) *acme.ExternalAccountKey {
|
||||
if k == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
eak := &acme.ExternalAccountKey{
|
||||
ID: k.Id,
|
||||
ProvisionerID: k.Provisioner,
|
||||
Reference: k.Reference,
|
||||
AccountID: k.Account,
|
||||
HmacKey: k.HmacKey,
|
||||
CreatedAt: k.CreatedAt.AsTime(),
|
||||
BoundAt: k.BoundAt.AsTime(),
|
||||
}
|
||||
|
||||
if policy := k.GetPolicy(); policy != nil {
|
||||
eak.Policy = &acme.Policy{}
|
||||
if x509 := policy.GetX509(); x509 != nil {
|
||||
eak.Policy.X509 = acme.X509Policy{}
|
||||
if allow := x509.GetAllow(); allow != nil {
|
||||
eak.Policy.X509.Allowed = acme.PolicyNames{}
|
||||
eak.Policy.X509.Allowed.DNSNames = allow.Dns
|
||||
eak.Policy.X509.Allowed.IPRanges = allow.Ips
|
||||
}
|
||||
if deny := x509.GetDeny(); deny != nil {
|
||||
eak.Policy.X509.Denied = acme.PolicyNames{}
|
||||
eak.Policy.X509.Denied.DNSNames = deny.Dns
|
||||
eak.Policy.X509.Denied.IPRanges = deny.Ips
|
||||
}
|
||||
eak.Policy.X509.AllowWildcardNames = x509.AllowWildcardNames
|
||||
}
|
||||
}
|
||||
|
||||
return eak
|
||||
}
|
|
@ -1,558 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
)
|
||||
|
||||
func readProtoJSON(r io.ReadCloser, m proto.Message) error {
|
||||
defer r.Close()
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return protojson.Unmarshal(data, m)
|
||||
}
|
||||
|
||||
func mockMustAuthority(t *testing.T, a adminAuthority) {
|
||||
t.Helper()
|
||||
fn := mustAuthority
|
||||
t.Cleanup(func() {
|
||||
mustAuthority = fn
|
||||
})
|
||||
mustAuthority = func(ctx context.Context) adminAuthority {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_requireEABEnabled(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
next http.HandlerFunc
|
||||
err *admin.Error
|
||||
statusCode int
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/prov.GetDetails": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Id: "provID",
|
||||
Name: "provName",
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
err := admin.NewErrorISE("error getting ACME details for provisioner 'provName'")
|
||||
err.Message = "error getting ACME details for provisioner 'provName'"
|
||||
return test{
|
||||
ctx: ctx,
|
||||
err: err,
|
||||
statusCode: 500,
|
||||
}
|
||||
},
|
||||
"fail/prov.GetDetails.GetACME": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Id: "provID",
|
||||
Name: "provName",
|
||||
Details: &linkedca.ProvisionerDetails{},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
err := admin.NewErrorISE("error getting ACME details for provisioner 'provName'")
|
||||
err.Message = "error getting ACME details for provisioner 'provName'"
|
||||
return test{
|
||||
ctx: ctx,
|
||||
err: err,
|
||||
statusCode: 500,
|
||||
}
|
||||
},
|
||||
"ok/eab-disabled": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Id: "provID",
|
||||
Name: "provName",
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_ACME{
|
||||
ACME: &linkedca.ACMEProvisioner{
|
||||
RequireEab: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
err := admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner provName")
|
||||
err.Message = "ACME EAB not enabled for provisioner 'provName'"
|
||||
return test{
|
||||
ctx: ctx,
|
||||
err: err,
|
||||
statusCode: 400,
|
||||
}
|
||||
},
|
||||
"ok/eab-enabled": func(t *testing.T) test {
|
||||
prov := &linkedca.Provisioner{
|
||||
Id: "provID",
|
||||
Name: "provName",
|
||||
Details: &linkedca.ProvisionerDetails{
|
||||
Data: &linkedca.ProvisionerDetails_ACME{
|
||||
ACME: &linkedca.ACMEProvisioner{
|
||||
RequireEab: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := linkedca.NewContextWithProvisioner(context.Background(), prov)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
next: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(nil) // mock response with status 200
|
||||
},
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/foo", nil).WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
requireEABEnabled(tc.next)(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 {
|
||||
err := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))
|
||||
|
||||
assert.Equals(t, tc.err.Type, err.Type)
|
||||
assert.Equals(t, tc.err.Message, err.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, err.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
|
||||
type fields struct {
|
||||
Reference string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "fail/reference-too-long",
|
||||
fields: fields{
|
||||
Reference: strings.Repeat("A", 257),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ok/empty-reference",
|
||||
fields: fields{
|
||||
Reference: "",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
fields: fields{
|
||||
Reference: "my-eab-reference",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &CreateExternalAccountKeyRequest{
|
||||
Reference: tt.fields.Reference,
|
||||
}
|
||||
if err := r.Validate(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CreateExternalAccountKeyRequest.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_CreateExternalAccountKey(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
err *admin.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"ok": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 501,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorNotImplementedType.String(),
|
||||
Status: http.StatusNotImplemented,
|
||||
Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm",
|
||||
Detail: "not implemented",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest("POST", "/foo", nil) // chi routing is prepared in test setup
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
acmeResponder := NewACMEAdminResponder()
|
||||
acmeResponder.CreateExternalAccountKey(w, req)
|
||||
res := w.Result()
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_DeleteExternalAccountKey(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
err *admin.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"ok": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
chiCtx.URLParams.Add("id", "keyID")
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 501,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorNotImplementedType.String(),
|
||||
Status: http.StatusNotImplemented,
|
||||
Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm",
|
||||
Detail: "not implemented",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
acmeResponder := NewACMEAdminResponder()
|
||||
acmeResponder.DeleteExternalAccountKey(w, req)
|
||||
res := w.Result()
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_GetExternalAccountKeys(t *testing.T) {
|
||||
type test struct {
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
req *http.Request
|
||||
err *admin.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"ok": func(t *testing.T) test {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("provisionerName", "provName")
|
||||
req := httptest.NewRequest("GET", "/foo", nil)
|
||||
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 501,
|
||||
req: req,
|
||||
err: &admin.Error{
|
||||
Type: admin.ErrorNotImplementedType.String(),
|
||||
Status: http.StatusNotImplemented,
|
||||
Message: "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm",
|
||||
Detail: "not implemented",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, prep := range tests {
|
||||
tc := prep(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
req := tc.req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
acmeResponder := NewACMEAdminResponder()
|
||||
acmeResponder.GetExternalAccountKeys(w, req)
|
||||
|
||||
res := w.Result()
|
||||
assert.Equals(t, tc.statusCode, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
adminErr := admin.Error{}
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))
|
||||
|
||||
assert.Equals(t, tc.err.Type, adminErr.Type)
|
||||
assert.Equals(t, tc.err.Message, adminErr.Message)
|
||||
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
|
||||
assert.Equals(t, tc.err.Detail, adminErr.Detail)
|
||||
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_eakToLinked(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
k *acme.ExternalAccountKey
|
||||
want *linkedca.EABKey
|
||||
}{
|
||||
{
|
||||
name: "no-key",
|
||||
k: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "no-policy",
|
||||
k: &acme.ExternalAccountKey{
|
||||
ID: "keyID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "ref",
|
||||
AccountID: "accID",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),
|
||||
BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),
|
||||
Policy: nil,
|
||||
},
|
||||
want: &linkedca.EABKey{
|
||||
Id: "keyID",
|
||||
Provisioner: "provID",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
Reference: "ref",
|
||||
Account: "accID",
|
||||
CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),
|
||||
BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),
|
||||
Policy: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with-policy",
|
||||
k: &acme.ExternalAccountKey{
|
||||
ID: "keyID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "ref",
|
||||
AccountID: "accID",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),
|
||||
BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),
|
||||
Policy: &acme.Policy{
|
||||
X509: acme.X509Policy{
|
||||
Allowed: acme.PolicyNames{
|
||||
DNSNames: []string{"*.local"},
|
||||
IPRanges: []string{"10.0.0.0/24"},
|
||||
},
|
||||
Denied: acme.PolicyNames{
|
||||
DNSNames: []string{"badhost.local"},
|
||||
IPRanges: []string{"10.0.0.30"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &linkedca.EABKey{
|
||||
Id: "keyID",
|
||||
Provisioner: "provID",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
Reference: "ref",
|
||||
Account: "accID",
|
||||
CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),
|
||||
BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),
|
||||
Policy: &linkedca.Policy{
|
||||
X509: &linkedca.X509Policy{
|
||||
Allow: &linkedca.X509Names{
|
||||
Dns: []string{"*.local"},
|
||||
Ips: []string{"10.0.0.0/24"},
|
||||
},
|
||||
Deny: &linkedca.X509Names{
|
||||
Dns: []string{"badhost.local"},
|
||||
Ips: []string{"10.0.0.30"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := eakToLinked(tt.k); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("eakToLinked() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_linkedEAKToCertificates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
k *linkedca.EABKey
|
||||
want *acme.ExternalAccountKey
|
||||
}{
|
||||
{
|
||||
name: "no-key",
|
||||
k: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "no-policy",
|
||||
k: &linkedca.EABKey{
|
||||
Id: "keyID",
|
||||
Provisioner: "provID",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
Reference: "ref",
|
||||
Account: "accID",
|
||||
CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),
|
||||
BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),
|
||||
Policy: nil,
|
||||
},
|
||||
want: &acme.ExternalAccountKey{
|
||||
ID: "keyID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "ref",
|
||||
AccountID: "accID",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),
|
||||
BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),
|
||||
Policy: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-x509-policy",
|
||||
k: &linkedca.EABKey{
|
||||
Id: "keyID",
|
||||
Provisioner: "provID",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
Reference: "ref",
|
||||
Account: "accID",
|
||||
CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),
|
||||
BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),
|
||||
Policy: &linkedca.Policy{},
|
||||
},
|
||||
want: &acme.ExternalAccountKey{
|
||||
ID: "keyID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "ref",
|
||||
AccountID: "accID",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),
|
||||
BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),
|
||||
Policy: &acme.Policy{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with-x509-policy",
|
||||
k: &linkedca.EABKey{
|
||||
Id: "keyID",
|
||||
Provisioner: "provID",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
Reference: "ref",
|
||||
Account: "accID",
|
||||
CreatedAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),
|
||||
BoundAt: timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),
|
||||
Policy: &linkedca.Policy{
|
||||
X509: &linkedca.X509Policy{
|
||||
Allow: &linkedca.X509Names{
|
||||
Dns: []string{"*.local"},
|
||||
Ips: []string{"10.0.0.0/24"},
|
||||
},
|
||||
Deny: &linkedca.X509Names{
|
||||
Dns: []string{"badhost.local"},
|
||||
Ips: []string{"10.0.0.30"},
|
||||
},
|
||||
AllowWildcardNames: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &acme.ExternalAccountKey{
|
||||
ID: "keyID",
|
||||
ProvisionerID: "provID",
|
||||
Reference: "ref",
|
||||
AccountID: "accID",
|
||||
HmacKey: []byte{1, 3, 3, 7},
|
||||
CreatedAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),
|
||||
BoundAt: time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),
|
||||
Policy: &acme.Policy{
|
||||
X509: acme.X509Policy{
|
||||
Allowed: acme.PolicyNames{
|
||||
DNSNames: []string{"*.local"},
|
||||
IPRanges: []string{"10.0.0.0/24"},
|
||||
},
|
||||
Denied: acme.PolicyNames{
|
||||
DNSNames: []string{"badhost.local"},
|
||||
IPRanges: []string{"10.0.0.30"},
|
||||
},
|
||||
AllowWildcardNames: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := linkedEAKToCertificates(tt.k); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("linkedEAKToCertificates() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue