Merge branch 'master' into herman/acme-macos-properties

pull/1496/head
Herman Slatman 5 months ago
commit 9b12867e9d
No known key found for this signature in database
GPG Key ID: F4D8A44EA0A75A4F

@ -9,3 +9,7 @@ updates:
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

@ -5,5 +5,3 @@ on:
jobs:
code-scan:
uses: smallstep/workflows/.github/workflows/code-scan.yml@main
secrets:
GITLEAKS_LICENSE_KEY: ${{ secrets.GITLEAKS_LICENSE_KEY }}

@ -12,7 +12,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.1.1
uses: dependabot/fetch-metadata@v1.6.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs

@ -30,19 +30,19 @@ jobs:
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}
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}
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}
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

@ -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

@ -36,11 +36,9 @@ archives:
# Most common use case is to archive as zip on Windows.
# Default is empty.
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
rlcp: true
format_overrides:
- goos: windows
format: zip
wrap_in_directory: "{{ .ProjectName }}_{{ .Version }}"
files:
- README.md
- LICENSE
@ -49,6 +47,7 @@ archives:
<< : *ARCHIVE
id: unversioned
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
wrap_in_directory: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
nfpms:
@ -88,7 +87,6 @@ nfpms:
source:
enabled: true
rlcp: true
name_template: '{{ .ProjectName }}_{{ .Version }}'
checksum:
@ -166,11 +164,11 @@ release:
```
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 step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig.pem \
--signature step-ca_darwin_{{ .Version }}_amd64.tar.gz.sig \
--certificate-identity-regexp "https://github\.com/smallstep/workflows/.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
~/Downloads/step-ca_darwin_{{ .Version }}_amd64.tar.gz
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.
@ -199,6 +197,124 @@ release:
# - glob: ./glob/**/to/**/file/**/*
# - glob: ./glob/foo/to/bar/file/foobar/override_from_previous
winget:
-
# IDs of the archives to use.
# Empty means all IDs.
ids: [ default ]
#
# Default: ProjectName
# Templates: allowed
name: step-ca
# Publisher name.
#
# Templates: allowed
# Required.
publisher: Smallstep
# Your app's description.
#
# Templates: allowed
# Required.
short_description: "A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management."
# License name.
#
# Templates: allowed
# Required.
license: "Apache-2.0"
# Publisher URL.
#
# Templates: allowed
publisher_url: "https://smallstep.com"
# Publisher support URL.
#
# Templates: allowed
publisher_support_url: "https://github.com/smallstep/certificates/discussions"
# URL which is determined by the given Token (github, gitlab or gitea).
#
# Default depends on the client.
# Templates: allowed
url_template: "https://github.com/smallstep/certificates/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
# Git author used to commit to the repository.
commit_author:
name: goreleaserbot
email: goreleaser@smallstep.com
# The project name and current git tag are used in the format string.
#
# Templates: allowed
commit_msg_template: "{{ .PackageIdentifier }}: {{ .Tag }}"
# Your app's homepage.
homepage: "https://github.com/smallstep/certificates"
# Your app's long description.
#
# Templates: allowed
description: ""
# License URL.
#
# Templates: allowed
license_url: "https://github.com/smallstep/certificates/blob/master/LICENSE"
# Release notes URL.
#
# Templates: allowed
release_notes_url: "https://github.com/smallstep/certificates/releases/tag/{{.Version}}"
# Create the PR - for testing
skip_upload: auto
# Tags.
tags:
- certificates
- smallstep
- tls
# Repository to push the generated files to.
repository:
owner: smallstep
name: winget-pkgs
branch: step
# Optionally a token can be provided, if it differs from the token
# provided to GoReleaser
# Templates: allowed
#token: "{{ .Env.GITHUB_PERSONAL_AUTH_TOKEN }}"
# Sets up pull request creation instead of just pushing to the given branch.
# Make sure the 'branch' property is different from base before enabling
# it.
#
# Since: v1.17
pull_request:
# Whether to enable it or not.
enabled: true
check_boxes: true
# Whether to open the PR as a draft or not.
#
# Default: false
# Since: v1.19
# draft: true
# Base can also be another repository, in which case the owner and name
# above will be used as HEAD, allowing cross-repository pull requests.
#
# Since: v1.19
base:
owner: microsoft
name: winget-pkgs
branch: master
scoops:
-
ids: [ default ]
@ -208,9 +324,10 @@ scoops:
# 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:
repository:
owner: smallstep
name: scoop-bucket
branch: main
# Git author used to commit to the repository.
# Defaults are shown.

@ -25,13 +25,63 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
---
## [Unreleased]
## [0.25.0] - 2023-09-26
### Added
- Added support for configuring SCEP decrypters in the provisioner (smallstep/certificates#1414)
- Added support for TPM KMS (smallstep/crypto#253)
- Added support for disableSmallstepExtensions provisioner claim
(smallstep/certificates#1484)
- Added script to migrate a badger DB to MySQL or PostgreSQL
(smallstep/certificates#1477)
- Added AWS public certificates for me-central-1 and ap-southeast-3
(smallstep/certificates#1404)
- Added namespace field to VaultCAS JSON config (smallstep/certificates#1424)
- Added AWS public certificates for me-central-1 and ap-southeast-3
(smallstep/certificates#1404)
- Added unversioned filenames to Github release assets
(smallstep/certificates#1435)
- Send X5C leaf certificate to webhooks (smallstep/certificates#1485)
- Added support for disableSmallstepExtensions claim (smallstep/certificates#1484)
- Added all AWS Identity Document Certificates (smallstep/certificates#1404, smallstep/certificates#1510)
- Added Winget release automation (smallstep/certificates#1519)
- Added CSR to SCEPCHALLENGE webhook request body (smallstep/certificates#1523)
- Added SCEP issuance notification webhook (smallstep/certificates#1544)
- Added ability to disable color in the log text formatter
(smallstep/certificates(#1559)
### Changed
- Changed the Makefile to produce cgo-enabled builds running
`make build GO_ENVS="CGO_ENABLED=1"` (smallstep/certificates#1446)
- Return more detailed errors to ACME clients using device-attest-01
(smallstep/certificates#1495)
- Change SCEP password type to string (smallstep/certificates#1555)
### Removed
- Removed OIDC user regexp check (smallstep/certificates#1481)
- Removed automatic initialization of $STEPPATH (smallstep/certificates#1493)
- Removed db datasource from error msg to prevent leaking of secrets to logs
(smallstep/certificates#1528)
### Fixed
- Improved authentication for ACME requests using kid and provisioner name
(smallstep/certificates#1386).
- Fixed indentation of KMS configuration in helm charts
(smallstep/certificates#1405)
- Fixed simultaneous sign or decrypt operation on a YubiKey
(smallstep/certificates#1476, smallstep/crypto#288)
- Fixed adding certificate templates with ASN.1 functions
(smallstep/certificates#1500, smallstep/crypto#302)
- Fixed a problem when the ca.json is truncated if the encoding of the
configuration fails (e.g., new provisioner with bad template data)
(smallstep/cli#994, smallstep/certificates#1501)
- Fixed provisionerOptionsToLinkedCA missing template and templateData
(smallstep/certificates#1520)
- Fix calculation of webhook signature (smallstep/certificates#1546)
## [v0.24.2] - 2023-05-11

@ -31,7 +31,7 @@ To get up and running quickly, or as an alternative to running your own `step-ca
[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)
[Contributor's Guide](./CONTRIBUTING.md)
[![GitHub release](https://img.shields.io/github/release/smallstep/certificates.svg)](https://github.com/smallstep/certificates/releases/latest)
[![Go Report Card](https://goreportcard.com/badge/github.com/smallstep/certificates)](https://goreportcard.com/report/github.com/smallstep/certificates)

@ -6,7 +6,7 @@ import (
"errors"
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api/render"

@ -7,12 +7,13 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"go.step.sm/crypto/jose"
@ -313,7 +314,7 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) {
"fail/nil-account": func(t *testing.T) test {
return test{
db: &acme.MockDB{},
ctx: context.WithValue(context.Background(), accContextKey, nil),
ctx: context.WithValue(context.Background(), accContextKey, http.NoBody),
statusCode: 400,
err: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"),
}
@ -363,7 +364,7 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) {
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 := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
GetOrdersByAccountID(w, req)
@ -802,7 +803,7 @@ func TestHandler_NewAccount(t *testing.T) {
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 := httptest.NewRequest("GET", "/foo/bar", http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
NewAccount(w, req)
@ -1005,7 +1006,7 @@ func TestHandler_GetOrUpdateAccount(t *testing.T) {
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 := httptest.NewRequest("GET", "/foo/bar", http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
GetOrUpdateAccount(w, req)

@ -9,7 +9,7 @@ import (
"net/http"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api"

@ -15,7 +15,7 @@ import (
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
@ -60,7 +60,7 @@ func TestHandler_GetNonce(t *testing.T) {
}
// Request with chi context
req := httptest.NewRequest("GET", "http://ca.smallstep.com/nonce", nil)
req := httptest.NewRequest("GET", "http://ca.smallstep.com/nonce", http.NoBody)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -175,7 +175,7 @@ func TestHandler_GetDirectory(t *testing.T) {
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 := httptest.NewRequest("GET", "/foo/bar", http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
GetDirectory(w, req)
@ -347,7 +347,7 @@ func TestHandler_GetAuthorization(t *testing.T) {
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 := httptest.NewRequest("GET", "/foo/bar", http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
GetAuthorization(w, req)
@ -489,7 +489,7 @@ func TestHandler_GetCertificate(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
ctx := acme.NewDatabaseContext(tc.ctx, tc.db)
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
GetCertificate(w, req)
@ -747,7 +747,7 @@ func TestHandler_GetChallenge(t *testing.T) {
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 := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
GetChallenge(w, req)

@ -75,7 +75,7 @@ func TestHandler_addNonce(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
ctx := newBaseContext(context.Background(), tc.db)
req := httptest.NewRequest("GET", u, nil).WithContext(ctx)
req := httptest.NewRequest("GET", u, http.NoBody).WithContext(ctx)
w := httptest.NewRecorder()
addNonce(testNext)(w, req)
res := w.Result()
@ -127,7 +127,7 @@ func TestHandler_addDirLink(t *testing.T) {
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
addDirLink(testNext)(w, req)
@ -230,7 +230,7 @@ func TestHandler_verifyContentType(t *testing.T) {
if tc.url != "" {
_u = tc.url
}
req := httptest.NewRequest("GET", _u, nil)
req := httptest.NewRequest("GET", _u, http.NoBody)
req = req.WithContext(tc.ctx)
req.Header.Add("Content-Type", tc.contentType)
w := httptest.NewRecorder()
@ -298,7 +298,7 @@ func TestHandler_isPostAsGet(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
// h := &Handler{}
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
isPostAsGet(testNext)(w, req)
@ -582,7 +582,7 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
// h := &Handler{}
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
verifyAndExtractJWSPayload(tc.next)(w, req)
@ -829,7 +829,7 @@ func TestHandler_lookupJWK(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
ctx := newBaseContext(tc.ctx, tc.db, tc.linker)
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
lookupJWK(tc.next)(w, req)
@ -1028,7 +1028,7 @@ func TestHandler_extractJWK(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
ctx := newBaseContext(tc.ctx, tc.db)
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
extractJWK(tc.next)(w, req)
@ -1403,7 +1403,7 @@ func TestHandler_validateJWS(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
ctx := newBaseContext(tc.ctx, tc.db)
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
validateJWS(tc.next)(w, req)
@ -1585,7 +1585,7 @@ func TestHandler_extractOrLookupJWK(t *testing.T) {
tc := prep(t)
t.Run(name, func(t *testing.T) {
ctx := newBaseContext(tc.ctx, tc.db, tc.linker)
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
extractOrLookupJWK(tc.next)(w, req)
@ -1670,7 +1670,7 @@ func TestHandler_checkPrerequisites(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
ctx := acme.NewPrerequisitesCheckerContext(tc.ctx, tc.prerequisitesChecker)
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
checkPrerequisites(tc.next)(w, req)

@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"go.step.sm/crypto/randutil"
"go.step.sm/crypto/x509util"

@ -8,13 +8,14 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"go.step.sm/crypto/pemutil"
@ -468,7 +469,7 @@ func TestHandler_GetOrder(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme"))
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
GetOrder(w, req)
@ -1827,7 +1828,7 @@ func TestHandler_NewOrder(t *testing.T) {
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.ca)
ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme"))
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
NewOrder(w, req)
@ -2124,7 +2125,7 @@ func TestHandler_FinalizeOrder(t *testing.T) {
tc := run(t)
t.Run(name, func(t *testing.T) {
ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme"))
req := httptest.NewRequest("GET", u, nil)
req := httptest.NewRequest("GET", u, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
FinalizeOrder(w, req)

@ -21,7 +21,7 @@ import (
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
"golang.org/x/crypto/ocsp"
@ -1072,7 +1072,7 @@ func TestHandler_RevokeCert(t *testing.T) {
t.Run(name, func(t *testing.T) {
ctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker("test.ca.smallstep.com", "acme"))
mockMustAuthority(t, tc.ca)
req := httptest.NewRequest("POST", revokeURL, nil)
req := httptest.NewRequest("POST", revokeURL, http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
RevokeCert(w, req)
@ -1094,7 +1094,7 @@ func TestHandler_RevokeCert(t *testing.T) {
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else {
assert.True(t, bytes.Equal(bytes.TrimSpace(body), []byte{}))
assert.Equals(t, int64(0), req.ContentLength)
assert.Equals(t, int64(-1), req.ContentLength)
assert.Equals(t, []string{fmt.Sprintf("<%s/acme/%s/directory>;rel=\"index\"", baseURL.String(), escProvName)}, res.Header["Link"])
}
})

@ -26,7 +26,7 @@ import (
"time"
"github.com/fxamacker/cbor/v2"
"github.com/google/go-tpm/tpm2"
"github.com/google/go-tpm/legacy/tpm2"
"golang.org/x/exp/slices"
"github.com/smallstep/go-attestation/attest"
@ -384,7 +384,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
prov := MustProvisionerFromContext(ctx)
if !prov.IsAttestationFormatEnabled(ctx, provisioner.ACMEAttestationFormat(format)) {
if format != "apple" && format != "step" && format != "tpm" {
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "unsupported attestation object format %q", format).WithAdditionalErrorDetail())
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "unsupported attestation object format %q", format))
}
return storeError(ctx, db, ch, true,
@ -409,7 +409,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
if len(data.Nonce) != 0 {
sum := sha256.Sum256([]byte(ch.Token))
if subtle.ConstantTimeCompare(data.Nonce, sum[:]) != 1 {
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "challenge token does not match"))
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "challenge token does not match"))
}
}
@ -421,9 +421,9 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
subproblem := NewSubproblemWithIdentifier(
ErrorRejectedIdentifierType,
Identifier{Type: "permanent-identifier", Value: ch.Value},
"challenge identifier %q doesn't match any of the attested hardware identifiers %s", ch.Value, []string{data.UDID, data.SerialNumber},
"challenge identifier %q doesn't match any of the attested hardware identifiers %q", ch.Value, []string{data.UDID, data.SerialNumber},
)
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").WithAdditionalErrorDetail().AddSubproblems(subproblem))
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem))
}
// Update attestation key fingerprint to compare against the CSR
@ -451,7 +451,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
Identifier{Type: "permanent-identifier", Value: ch.Value},
"challenge identifier %q doesn't match the attested hardware identifier %q", ch.Value, data.SerialNumber,
)
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").WithAdditionalErrorDetail().AddSubproblems(subproblem))
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem))
}
// Update attestation key fingerprint to compare against the CSR
@ -479,15 +479,15 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
subproblem := NewSubproblemWithIdentifier(
ErrorRejectedIdentifierType,
Identifier{Type: "permanent-identifier", Value: ch.Value},
"challenge identifier %q doesn't match any of the attested hardware identifiers %s", ch.Value, data.PermanentIdentifiers,
"challenge identifier %q doesn't match any of the attested hardware identifiers %q", ch.Value, data.PermanentIdentifiers,
)
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").WithAdditionalErrorDetail().AddSubproblems(subproblem))
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem))
}
// Update attestation key fingerprint to compare against the CSR
az.Fingerprint = data.Fingerprint
default:
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "unsupported attestation object format %q", format).WithAdditionalErrorDetail())
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "unsupported attestation object format %q", format))
}
// Update and store the challenge.
@ -533,38 +533,38 @@ const (
func doTPMAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*tpmAttestationData, error) {
ver, ok := att.AttStatement["ver"].(string)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "ver not present").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "ver not present")
}
if ver != "2.0" {
return nil, NewError(ErrorBadAttestationStatementType, "version %q is not supported", ver).WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "version %q is not supported", ver)
}
x5c, ok := att.AttStatement["x5c"].([]interface{})
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c not present").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c not present")
}
if len(x5c) == 0 {
return nil, NewError(ErrorBadAttestationStatementType, "x5c is empty").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c is empty")
}
akCertBytes, ok := x5c[0].([]byte)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c is malformed")
}
akCert, err := x509.ParseCertificate(akCertBytes)
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c is malformed")
}
intermediates := x509.NewCertPool()
for _, v := range x5c[1:] {
intCertBytes, vok := v.([]byte)
if !vok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c is malformed")
}
intCert, err := x509.ParseCertificate(intCertBytes)
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c is malformed")
}
intermediates.AddCert(intCert)
}
@ -602,19 +602,19 @@ func doTPMAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
})
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is not valid").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c is not valid")
}
// validate additional AK certificate requirements
if err := validateAKCertificate(akCert); err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "AK certificate is not valid").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "AK certificate is not valid")
}
// TODO(hs): implement revocation check; Verify() doesn't perform CRL check nor OCSP lookup.
sans, err := x509util.ParseSubjectAlternativeNames(akCert)
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed parsing AK certificate Subject Alternative Names").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "failed parsing AK certificate Subject Alternative Names")
}
permanentIdentifiers := make([]string, len(sans.PermanentIdentifiers))
@ -625,37 +625,37 @@ func doTPMAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
// extract and validate pubArea, sig, certInfo and alg properties from the request body
pubArea, ok := att.AttStatement["pubArea"].([]byte)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "invalid pubArea in attestation statement").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "invalid pubArea in attestation statement")
}
if len(pubArea) == 0 {
return nil, NewError(ErrorBadAttestationStatementType, "pubArea is empty").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "pubArea is empty")
}
sig, ok := att.AttStatement["sig"].([]byte)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "invalid sig in attestation statement").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "invalid sig in attestation statement")
}
if len(sig) == 0 {
return nil, NewError(ErrorBadAttestationStatementType, "sig is empty").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "sig is empty")
}
certInfo, ok := att.AttStatement["certInfo"].([]byte)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "invalid certInfo in attestation statement").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "invalid certInfo in attestation statement")
}
if len(certInfo) == 0 {
return nil, NewError(ErrorBadAttestationStatementType, "certInfo is empty").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "certInfo is empty")
}
alg, ok := att.AttStatement["alg"].(int64)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "invalid alg in attestation statement").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "invalid alg in attestation statement")
}
// only RS256 and ES256 are allowed
coseAlg := coseAlgorithmIdentifier(alg)
if coseAlg != coseAlgRS256 && coseAlg != coseAlgES256 {
return nil, NewError(ErrorBadAttestationStatementType, "invalid alg %d in attestation statement", alg).WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "invalid alg %d in attestation statement", alg)
}
// set the hash algorithm to use to SHA256
@ -673,13 +673,13 @@ func doTPMAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
Hash: hash,
}
if err = certificationParameters.Verify(verifyOpts); err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "invalid certification parameters").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "invalid certification parameters")
}
// decode the "certInfo" data. This won't fail, as it's also done as part of Verify().
tpmCertInfo, err := tpm2.DecodeAttestationData(certInfo)
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed decoding attestation data").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "failed decoding attestation data")
}
keyAuth, err := KeyAuthorization(ch.Token, jwk)
@ -691,18 +691,18 @@ func doTPMAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
// verify the WebAuthn object contains the expect key authorization digest, which is carried
// within the encoded `certInfo` property of the attestation statement.
if subtle.ConstantTimeCompare(hashedKeyAuth[:], []byte(tpmCertInfo.ExtraData)) == 0 {
return nil, NewError(ErrorBadAttestationStatementType, "key authorization invalid").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "key authorization invalid")
}
// decode the (attested) public key and determine its fingerprint. This won't fail, as it's also done as part of Verify().
pub, err := tpm2.DecodePublic(pubArea)
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed decoding pubArea").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "failed decoding pubArea")
}
publicKey, err := pub.Key()
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed getting public key").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "failed getting public key")
}
data := &tpmAttestationData{
@ -862,30 +862,30 @@ func doAppleAttestationFormat(_ context.Context, prov Provisioner, _ *Challenge,
x5c, ok := att.AttStatement["x5c"].([]interface{})
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c not present").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c not present")
}
if len(x5c) == 0 {
return nil, NewError(ErrorBadAttestationStatementType, "x5c is empty").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c is empty")
}
der, ok := x5c[0].([]byte)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c is malformed")
}
leaf, err := x509.ParseCertificate(der)
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c is malformed")
}
intermediates := x509.NewCertPool()
for _, v := range x5c[1:] {
der, ok = v.([]byte)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c is malformed")
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c is malformed")
}
intermediates.AddCert(cert)
}
@ -896,7 +896,7 @@ func doAppleAttestationFormat(_ context.Context, prov Provisioner, _ *Challenge,
CurrentTime: time.Now().Truncate(time.Second),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}); err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is not valid").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c is not valid")
}
data := &appleAttestationData{
@ -982,28 +982,28 @@ func doStepAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
// Extract x5c and verify certificate
x5c, ok := att.AttStatement["x5c"].([]interface{})
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c not present").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c not present")
}
if len(x5c) == 0 {
return nil, NewError(ErrorRejectedIdentifierType, "x5c is empty").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorRejectedIdentifierType, "x5c is empty")
}
der, ok := x5c[0].([]byte)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c is malformed")
}
leaf, err := x509.ParseCertificate(der)
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c is malformed")
}
intermediates := x509.NewCertPool()
for _, v := range x5c[1:] {
der, ok = v.([]byte)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "x5c is malformed")
}
cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c is malformed")
}
intermediates.AddCert(cert)
}
@ -1013,7 +1013,7 @@ func doStepAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
CurrentTime: time.Now().Truncate(time.Second),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}); err != nil {
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is not valid").WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "x5c is not valid")
}
// Verify proof of possession of private key validating the key
@ -1023,10 +1023,10 @@ func doStepAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
var sig []byte
csig, ok := att.AttStatement["sig"].([]byte)
if !ok {
return nil, NewError(ErrorBadAttestationStatementType, "sig not present").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "sig not present")
}
if err := cbor.Unmarshal(csig, &sig); err != nil {
return nil, NewError(ErrorBadAttestationStatementType, "sig is malformed").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "sig is malformed")
}
keyAuth, err := KeyAuthorization(ch.Token, jwk)
if err != nil {
@ -1036,23 +1036,23 @@ func doStepAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
switch pub := leaf.PublicKey.(type) {
case *ecdsa.PublicKey:
if pub.Curve != elliptic.P256() {
return nil, WrapError(ErrorBadAttestationStatementType, err, "unsupported elliptic curve %s", pub.Curve).WithAdditionalErrorDetail()
return nil, WrapDetailedError(ErrorBadAttestationStatementType, err, "unsupported elliptic curve %s", pub.Curve)
}
sum := sha256.Sum256([]byte(keyAuth))
if !ecdsa.VerifyASN1(pub, sum[:], sig) {
return nil, NewError(ErrorBadAttestationStatementType, "failed to validate signature").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "failed to validate signature")
}
case *rsa.PublicKey:
sum := sha256.Sum256([]byte(keyAuth))
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, sum[:], sig); err != nil {
return nil, NewError(ErrorBadAttestationStatementType, "failed to validate signature").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "failed to validate signature")
}
case ed25519.PublicKey:
if !ed25519.Verify(pub, []byte(keyAuth), sig) {
return nil, NewError(ErrorBadAttestationStatementType, "failed to validate signature").WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "failed to validate signature")
}
default:
return nil, NewError(ErrorBadAttestationStatementType, "unsupported public key type %T", pub).WithAdditionalErrorDetail()
return nil, NewDetailedError(ErrorBadAttestationStatementType, "unsupported public key type %T", pub)
}
// Parse attestation data:

@ -3444,7 +3444,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
},
payload: errorCBORPayload,
},
wantErr: NewErrorISE("error unmarshalling CBOR: cbor: cannot unmarshal positive integer into Go value of type acme.attestationObject"),
wantErr: NewErrorISE("error unmarshalling CBOR: cbor:"),
}
},
"ok/prov.IsAttestationFormatEnabled": func(t *testing.T) test {
@ -3532,7 +3532,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "12345678", updch.Value)
err := NewError(ErrorBadAttestationStatementType, "x5c not present").WithAdditionalErrorDetail()
err := NewDetailedError(ErrorBadAttestationStatementType, "x5c not present")
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
@ -3579,7 +3579,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "serial-number", updch.Value)
err := NewError(ErrorBadAttestationStatementType, "challenge token does not match")
err := NewDetailedError(ErrorBadAttestationStatementType, "challenge token does not match")
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
@ -3628,9 +3628,9 @@ func Test_deviceAttest01Validate(t *testing.T) {
subproblem := NewSubproblemWithIdentifier(
ErrorRejectedIdentifierType,
Identifier{Type: "permanent-identifier", Value: "non-matching-value"},
`challenge identifier "non-matching-value" doesn't match any of the attested hardware identifiers [udid serial-number]`,
`challenge identifier "non-matching-value" doesn't match any of the attested hardware identifiers ["udid" "serial-number"]`,
)
err := NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").WithAdditionalErrorDetail().AddSubproblems(subproblem)
err := NewDetailedError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem)
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
@ -3703,7 +3703,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "12345678", updch.Value)
err := NewError(ErrorBadAttestationStatementType, "x5c not present").WithAdditionalErrorDetail()
err := NewDetailedError(ErrorBadAttestationStatementType, "x5c not present")
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
@ -3757,8 +3757,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "12345678", updch.Value)
err := NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").
WithAdditionalErrorDetail().
err := NewDetailedError(ErrorBadAttestationStatementType, "permanent identifier does not match").
AddSubproblems(NewSubproblemWithIdentifier(
ErrorRejectedIdentifierType,
Identifier{Type: "permanent-identifier", Value: "12345678"},
@ -3853,7 +3852,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "12345678", updch.Value)
err := NewError(ErrorBadAttestationStatementType, `unsupported attestation object format "bogus-format"`).WithAdditionalErrorDetail()
err := NewDetailedError(ErrorBadAttestationStatementType, `unsupported attestation object format "bogus-format"`)
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
@ -4004,8 +4003,9 @@ func Test_deviceAttest01Validate(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())
if assert.Error(t, tc.wantErr) {
assert.ErrorContains(t, err, tc.wantErr.Error())
}
return
}

@ -237,7 +237,7 @@ func Test_deviceAttest01ValidateWithTPMSimulator(t *testing.T) {
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`).WithAdditionalErrorDetail()
err := NewDetailedError(ErrorBadAttestationStatementType, `version "bogus" is not supported`)
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
@ -282,12 +282,11 @@ func Test_deviceAttest01ValidateWithTPMSimulator(t *testing.T) {
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "device.id.99999999", updch.Value)
err := NewError(ErrorBadAttestationStatementType, `permanent identifier does not match`).
WithAdditionalErrorDetail().
err := NewDetailedError(ErrorBadAttestationStatementType, `permanent identifier does not match`).
AddSubproblems(NewSubproblemWithIdentifier(
ErrorRejectedIdentifierType,
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]`,
`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())

@ -298,20 +298,14 @@ func NewError(pt ProblemType, msg string, args ...interface{}) *Error {
return newError(pt, errors.Errorf(msg, args...))
}
// 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
// NewDetailedError creates a new Error that includes the error
// message in the details, providing more information to the
// ACME client.
func NewDetailedError(pt ProblemType, msg string, args ...interface{}) *Error {
return NewError(pt, msg, args...).withDetail()
}
// WithAdditionalErrorDetail adds the underlying error
// to the existing (default) ACME error detail, providing
// more information to the ACME client.
func (e *Error) WithAdditionalErrorDetail() *Error {
// prevent internal server errors from disclosing
// the internal error to the client at all times and
// prevent nil pointers.
func (e *Error) withDetail() *Error {
if e == nil || e.Status >= 500 || e.Err == nil {
return e
}
@ -320,6 +314,13 @@ func (e *Error) WithAdditionalErrorDetail() *Error {
return e
}
// 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
}
// 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.
@ -383,6 +384,10 @@ func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Err
}
}
func WrapDetailedError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
return WrapError(typ, err, msg, args...).withDetail()
}
// WrapErrorISE shortcut to wrap an internal server error type.
func WrapErrorISE(err error, msg string, args ...interface{}) *Error {
return WrapError(ErrorServerInternalType, err, msg, args...)

@ -22,8 +22,7 @@ func TestError_WithAdditionalErrorDetail(t *testing.T) {
"detail": "The server experienced an internal error",
"type": "urn:ietf:params:acme:error:serverInternal",
})
malformedErr := NewError(ErrorMalformedType, "malformed error")
malformedErr.Err = nil
malformedErr := NewError(ErrorMalformedType, "malformed error") // will result in Err == nil behavior
malformedJSON := mustJSON(t, map[string]interface{}{
"detail": "The request message was malformed",
"type": "urn:ietf:params:acme:error:malformed",
@ -37,9 +36,9 @@ func TestError_WithAdditionalErrorDetail(t *testing.T) {
err *Error
want string
}{
{"internal", NewError(ErrorServerInternalType, "").WithAdditionalErrorDetail(), internalJSON},
{"nil err", malformedErr.WithAdditionalErrorDetail(), malformedJSON},
{"detailed", NewError(ErrorBadAttestationStatementType, "invalid property").WithAdditionalErrorDetail(), withDetailJSON},
{"internal", NewDetailedError(ErrorServerInternalType, ""), internalJSON},
{"nil err", malformedErr, malformedJSON},
{"detailed", NewDetailedError(ErrorBadAttestationStatementType, "invalid property"), withDetailJSON},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

@ -8,7 +8,7 @@ import (
"net/url"
"strings"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"

@ -19,12 +19,13 @@ import (
"strings"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"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/models"
"github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/config"
@ -231,6 +232,29 @@ type ProvisionersResponse struct {
NextCursor string
}
const redacted = "*** REDACTED ***"
func scepFromProvisioner(p *provisioner.SCEP) *models.SCEP {
return &models.SCEP{
ID: p.ID,
Type: p.Type,
Name: p.Name,
ForceCN: p.ForceCN,
ChallengePassword: redacted,
Capabilities: p.Capabilities,
IncludeRoot: p.IncludeRoot,
ExcludeIntermediate: p.ExcludeIntermediate,
MinimumPublicKeyLength: p.MinimumPublicKeyLength,
DecrypterCertificate: []byte(redacted),
DecrypterKeyPEM: []byte(redacted),
DecrypterKeyURI: redacted,
DecrypterKeyPassword: redacted,
EncryptionAlgorithmIdentifier: p.EncryptionAlgorithmIdentifier,
Options: p.Options,
Claims: p.Claims,
}
}
// MarshalJSON implements json.Marshaler. It marshals the ProvisionersResponse
// into a byte slice.
//
@ -238,24 +262,22 @@ type ProvisionersResponse struct {
// 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) {
var responseProvisioners provisioner.List
for _, item := range p.Provisioners {
scepProv, ok := item.(*provisioner.SCEP)
if !ok {
responseProvisioners = append(responseProvisioners, item)
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)
responseProvisioners = append(responseProvisioners, scepFromProvisioner(scepProv))
}
var list = struct {
Provisioners []provisioner.Interface `json:"provisioners"`
NextCursor string `json:"nextCursor"`
}{
Provisioners: []provisioner.Interface(p.Provisioners),
Provisioners: []provisioner.Interface(responseProvisioners),
NextCursor: p.NextCursor,
}

@ -26,7 +26,7 @@ import (
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
sassert "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -803,7 +803,7 @@ func Test_CRLGeneration(t *testing.T) {
}
chiCtx := chi.NewRouteContext()
req := httptest.NewRequest("GET", "http://example.com/crl", nil)
req := httptest.NewRequest("GET", "http://example.com/crl", http.NoBody)
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))
for _, tt := range tests {
@ -856,7 +856,7 @@ func Test_caHandler_Route(t *testing.T) {
}
func Test_Health(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/health", nil)
req := httptest.NewRequest("GET", "http://example.com/health", http.NoBody)
w := httptest.NewRecorder()
Health(w, req)
@ -890,7 +890,7 @@ func Test_Root(t *testing.T) {
// Request with chi context
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("sha", "efc7d6b475a56fe587650bcdb999a4a308f815ba44db4bf0371ea68a786ccd36")
req := httptest.NewRequest("GET", "http://example.com/root/efc7d6b475a56fe587650bcdb999a4a308f815ba44db4bf0371ea68a786ccd36", nil)
req := httptest.NewRequest("GET", "http://example.com/root/efc7d6b475a56fe587650bcdb999a4a308f815ba44db4bf0371ea68a786ccd36", http.NoBody)
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))
expected := []byte(`{"ca":"` + strings.ReplaceAll(rootPEM, "\n", `\n`) + `\n"}`)
@ -1105,7 +1105,7 @@ func Test_Renew(t *testing.T) {
return nil
},
})
req := httptest.NewRequest("POST", "http://example.com/renew", nil)
req := httptest.NewRequest("POST", "http://example.com/renew", http.NoBody)
req.TLS = tt.tls
req.Header = tt.header
w := httptest.NewRecorder()
@ -1313,7 +1313,7 @@ func Test_ProvisionerKey(t *testing.T) {
// Request with chi context
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("kid", "oV1p0MJeGQ7qBlK6B-oyfVdBRjh_e7VSK_YSEEqgW00")
req := httptest.NewRequest("GET", "http://example.com/provisioners/oV1p0MJeGQ7qBlK6B-oyfVdBRjh_e7VSK_YSEEqgW00/encrypted-key", nil)
req := httptest.NewRequest("GET", "http://example.com/provisioners/oV1p0MJeGQ7qBlK6B-oyfVdBRjh_e7VSK_YSEEqgW00/encrypted-key", http.NoBody)
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))
tests := []struct {
@ -1381,7 +1381,7 @@ func Test_Roots(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockMustAuthority(t, &mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err})
req := httptest.NewRequest("GET", "http://example.com/roots", nil)
req := httptest.NewRequest("GET", "http://example.com/roots", http.NoBody)
req.TLS = tt.tls
w := httptest.NewRecorder()
Roots(w, req)
@ -1422,7 +1422,7 @@ func Test_caHandler_RootsPEM(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockMustAuthority(t, &mockAuthority{ret1: tt.roots, err: tt.err})
req := httptest.NewRequest("GET", "https://example.com/roots", nil)
req := httptest.NewRequest("GET", "https://example.com/roots", http.NoBody)
w := httptest.NewRecorder()
RootsPEM(w, req)
res := w.Result()
@ -1467,7 +1467,7 @@ func Test_Federation(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockMustAuthority(t, &mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err})
req := httptest.NewRequest("GET", "http://example.com/federation", nil)
req := httptest.NewRequest("GET", "http://example.com/federation", http.NoBody)
req.TLS = tt.tls
w := httptest.NewRecorder()
Federation(w, req)
@ -1569,7 +1569,6 @@ func mustCertificate(t *testing.T, pub, priv interface{}) *x509.Certificate {
}
func TestProvisionersResponse_MarshalJSON(t *testing.T) {
k := map[string]any{
"use": "sig",
"kty": "EC",
@ -1581,9 +1580,9 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
}
key := squarejose.JSONWebKey{}
b, err := json.Marshal(k)
assert.FatalError(t, err)
require.NoError(t, err)
err = json.Unmarshal(b, &key)
assert.FatalError(t, err)
require.NoError(t, err)
r := ProvisionersResponse{
Provisioners: provisioner.List{
@ -1593,6 +1592,12 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
ChallengePassword: "not-so-secret",
MinimumPublicKeyLength: 2048,
EncryptionAlgorithmIdentifier: 2,
IncludeRoot: true,
ExcludeIntermediate: true,
DecrypterCertificate: []byte{1, 2, 3, 4},
DecrypterKeyPEM: []byte{5, 6, 7, 8},
DecrypterKeyURI: "softkms:path=/path/to/private.key",
DecrypterKeyPassword: "super-secret-password",
},
&provisioner.JWK{
EncryptedKey: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg",
@ -1609,7 +1614,14 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
{
"type": "scep",
"name": "scep",
"forceCN": false,
"includeRoot": true,
"excludeIntermediate": true,
"challenge": "*** REDACTED ***",
"decrypterCertificate": []byte("*** REDACTED ***"),
"decrypterKey": "*** REDACTED ***",
"decrypterKeyPEM": []byte("*** REDACTED ***"),
"decrypterKeyPassword": "*** REDACTED ***",
"minimumPublicKeyLength": 2048,
"encryptionAlgorithmIdentifier": 2,
},
@ -1646,6 +1658,12 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
ChallengePassword: "not-so-secret",
MinimumPublicKeyLength: 2048,
EncryptionAlgorithmIdentifier: 2,
IncludeRoot: true,
ExcludeIntermediate: true,
DecrypterCertificate: []byte{1, 2, 3, 4},
DecrypterKeyPEM: []byte{5, 6, 7, 8},
DecrypterKeyURI: "softkms:path=/path/to/private.key",
DecrypterKeyPassword: "super-secret-password",
},
&provisioner.JWK{
EncryptedKey: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg",

@ -0,0 +1,118 @@
package models
import (
"context"
"crypto/x509"
"errors"
"github.com/smallstep/certificates/authority/provisioner"
"golang.org/x/crypto/ssh"
)
var errDummyImplementation = errors.New("dummy implementation")
// SCEP is the SCEP provisioner model used solely in CA API
// responses. All methods for the [provisioner.Interface] interface
// are implemented, but return a dummy error.
// TODO(hs): remove reliance on the interface for the API responses
type SCEP struct {
ID string `json:"-"`
Type string `json:"type"`
Name string `json:"name"`
ForceCN bool `json:"forceCN"`
ChallengePassword string `json:"challenge"`
Capabilities []string `json:"capabilities,omitempty"`
IncludeRoot bool `json:"includeRoot"`
ExcludeIntermediate bool `json:"excludeIntermediate"`
MinimumPublicKeyLength int `json:"minimumPublicKeyLength"`
DecrypterCertificate []byte `json:"decrypterCertificate"`
DecrypterKeyPEM []byte `json:"decrypterKeyPEM"`
DecrypterKeyURI string `json:"decrypterKey"`
DecrypterKeyPassword string `json:"decrypterKeyPassword"`
EncryptionAlgorithmIdentifier int `json:"encryptionAlgorithmIdentifier"`
Options *provisioner.Options `json:"options,omitempty"`
Claims *provisioner.Claims `json:"claims,omitempty"`
}
// GetID returns the provisioner unique identifier.
func (s *SCEP) GetID() string {
if s.ID != "" {
return s.ID
}
return s.GetIDForToken()
}
// GetIDForToken returns an identifier that will be used to load the provisioner
// from a token.
func (s *SCEP) GetIDForToken() string {
return "scep/" + s.Name
}
// GetName returns the name of the provisioner.
func (s *SCEP) GetName() string {
return s.Name
}
// GetType returns the type of provisioner.
func (s *SCEP) GetType() provisioner.Type {
return provisioner.TypeSCEP
}
// GetEncryptedKey returns the base provisioner encrypted key if it's defined.
func (s *SCEP) GetEncryptedKey() (string, string, bool) {
return "", "", false
}
// GetTokenID returns the identifier of the token.
func (s *SCEP) GetTokenID(string) (string, error) {
return "", errDummyImplementation
}
// Init initializes and validates the fields of a SCEP type.
func (s *SCEP) Init(_ provisioner.Config) (err error) {
return errDummyImplementation
}
// AuthorizeSign returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for signing x509 Certificates.
func (s *SCEP) AuthorizeSign(context.Context, string) ([]provisioner.SignOption, error) {
return nil, errDummyImplementation
}
// AuthorizeRevoke returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for revoking x509 Certificates.
func (s *SCEP) AuthorizeRevoke(context.Context, string) error {
return errDummyImplementation
}
// AuthorizeRenew returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for renewing x509 Certificates.
func (s *SCEP) AuthorizeRenew(context.Context, *x509.Certificate) error {
return errDummyImplementation
}
// AuthorizeSSHSign returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for signing SSH Certificates.
func (s *SCEP) AuthorizeSSHSign(context.Context, string) ([]provisioner.SignOption, error) {
return nil, errDummyImplementation
}
// AuthorizeRevoke returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for revoking SSH Certificates.
func (s *SCEP) AuthorizeSSHRevoke(context.Context, string) error {
return errDummyImplementation
}
// AuthorizeSSHRenew returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for renewing SSH Certificates.
func (s *SCEP) AuthorizeSSHRenew(context.Context, string) (*ssh.Certificate, error) {
return nil, errDummyImplementation
}
// AuthorizeSSHRekey returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for rekeying SSH Certificates.
func (s *SCEP) AuthorizeSSHRekey(context.Context, string) (*ssh.Certificate, []provisioner.SignOption, error) {
return nil, nil, errDummyImplementation
}
var _ provisioner.Interface = (*SCEP)(nil)

@ -317,7 +317,7 @@ func SSHSign(w http.ResponseWriter, r *http.Request) {
var identityCertificate []Certificate
if cr := body.IdentityCSR.CertificateRequest; cr != nil {
ctx := authority.NewContextWithSkipTokenReuse(r.Context())
ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)
ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignIdentityMethod)
signOpts, err := a.Authorize(ctx, body.OTT)
if err != nil {
render.Error(w, errs.UnauthorizedErr(err))

@ -12,7 +12,7 @@ import (
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
@ -128,7 +128,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/foo", nil).WithContext(tc.ctx)
req := httptest.NewRequest("GET", "/foo", http.NoBody).WithContext(tc.ctx)
w := httptest.NewRecorder()
requireEABEnabled(tc.next)(w, req)
res := w.Result()
@ -223,7 +223,7 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) {
tc := prep(t)
t.Run(name, func(t *testing.T) {
req := httptest.NewRequest("POST", "/foo", nil) // chi routing is prepared in test setup
req := httptest.NewRequest("POST", "/foo", http.NoBody) // chi routing is prepared in test setup
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
acmeResponder := NewACMEAdminResponder()
@ -276,7 +276,7 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) {
tc := prep(t)
t.Run(name, func(t *testing.T) {
req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup
req := httptest.NewRequest("DELETE", "/foo", http.NoBody) // chi routing is prepared in test setup
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
acmeResponder := NewACMEAdminResponder()
@ -311,7 +311,7 @@ func TestHandler_GetExternalAccountKeys(t *testing.T) {
"ok": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("provisionerName", "provName")
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
return test{
ctx: ctx,

@ -4,7 +4,7 @@ import (
"context"
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"go.step.sm/linkedca"

@ -11,7 +11,7 @@ import (
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/protobuf/types/known/timestamppb"
@ -357,7 +357,7 @@ func TestHandler_GetAdmin(t *testing.T) {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup
req := httptest.NewRequest("GET", "/foo", http.NoBody) // chi routing is prepared in test setup
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
GetAdmin(w, req)
@ -406,7 +406,7 @@ func TestHandler_GetAdmins(t *testing.T) {
}
var tests = map[string]func(t *testing.T) test{
"fail/parse-cursor": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo?limit=A", nil)
req := httptest.NewRequest("GET", "/foo?limit=A", http.NoBody)
return test{
ctx: context.Background(),
req: req,
@ -420,7 +420,7 @@ func TestHandler_GetAdmins(t *testing.T) {
}
},
"fail/auth.GetAdmins": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
auth := &mockAdminAuthority{
MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) {
assert.Equals(t, "", cursor)
@ -442,7 +442,7 @@ func TestHandler_GetAdmins(t *testing.T) {
}
},
"ok": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
createdAt := time.Now()
var deletedAt time.Time
adm1 := &linkedca.Admin{
@ -764,7 +764,7 @@ func TestHandler_DeleteAdmin(t *testing.T) {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup
req := httptest.NewRequest("DELETE", "/foo", http.NoBody) // chi routing is prepared in test setup
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
DeleteAdmin(w, req)

@ -4,7 +4,7 @@ import (
"errors"
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"go.step.sm/linkedca"

@ -11,7 +11,7 @@ import (
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/protobuf/types/known/timestamppb"
@ -72,7 +72,7 @@ func TestHandler_requireAPIEnabled(t *testing.T) {
tc := prep(t)
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup
req := httptest.NewRequest("GET", "/foo", http.NoBody) // chi routing is prepared in test setup
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
requireAPIEnabled(tc.next)(w, req)
@ -113,7 +113,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {
}
var tests = map[string]func(t *testing.T) test{
"fail/missing-authorization-token": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
req.Header["Authorization"] = []string{""}
return test{
ctx: context.Background(),
@ -128,7 +128,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {
}
},
"fail/auth.AuthorizeAdminToken": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
req.Header["Authorization"] = []string{"token"}
auth := &mockAdminAuthority{
MockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) {
@ -153,7 +153,7 @@ func TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {
}
},
"ok": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
req.Header["Authorization"] = []string{"token"}
createdAt := time.Now()
var deletedAt time.Time
@ -324,7 +324,7 @@ func TestHandler_loadProvisionerByName(t *testing.T) {
t.Run(name, func(t *testing.T) {
mockMustAuthority(t, tc.auth)
ctx := admin.NewContext(tc.ctx, tc.adminDB)
req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup
req := httptest.NewRequest("GET", "/foo", http.NoBody) // chi routing is prepared in test setup
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@ -399,7 +399,7 @@ func TestHandler_checkAction(t *testing.T) {
tc := prep(t)
t.Run(name, func(t *testing.T) {
ctx := admin.NewContext(context.Background(), tc.adminDB)
req := httptest.NewRequest("GET", "/foo", nil).WithContext(ctx)
req := httptest.NewRequest("GET", "/foo", http.NoBody).WithContext(ctx)
w := httptest.NewRecorder()
checkAction(tc.next, tc.supportedInStandalone)(w, req)
res := w.Result()
@ -643,7 +643,7 @@ func TestHandler_loadExternalAccountKey(t *testing.T) {
tc := prep(t)
t.Run(name, func(t *testing.T) {
ctx := acme.NewDatabaseContext(tc.ctx, tc.acmeDB)
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
loadExternalAccountKey(tc.next)(w, req)

@ -241,7 +241,7 @@ func TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) {
ctx := admin.NewContext(tc.ctx, tc.adminDB)
par := NewPolicyAdminResponder()
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@ -1164,7 +1164,7 @@ func TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) {
ctx = acme.NewDatabaseContext(ctx, tc.acmeDB)
par := NewPolicyAdminResponder()
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@ -1986,7 +1986,7 @@ func TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) {
ctx = acme.NewDatabaseContext(ctx, tc.acmeDB)
par := NewPolicyAdminResponder()
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
req = req.WithContext(ctx)
w := httptest.NewRecorder()

@ -4,7 +4,7 @@ import (
"fmt"
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"go.step.sm/crypto/sshutil"
"go.step.sm/crypto/x509util"

@ -12,7 +12,7 @@ import (
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/protobuf/encoding/protojson"
@ -37,7 +37,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
}
var tests = map[string]func(t *testing.T) test{
"fail/auth.LoadProvisionerByID": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo?id=provID", nil)
req := httptest.NewRequest("GET", "/foo?id=provID", http.NoBody)
chiCtx := chi.NewRouteContext()
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
@ -61,7 +61,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
}
},
"fail/auth.LoadProvisionerByName": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
@ -86,7 +86,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
}
},
"fail/db.GetProvisioner": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
@ -120,7 +120,7 @@ func TestHandler_GetProvisioner(t *testing.T) {
}
},
"ok": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
@ -208,7 +208,7 @@ func TestHandler_GetProvisioners(t *testing.T) {
}
var tests = map[string]func(t *testing.T) test{
"fail/parse-cursor": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo?limit=X", nil)
req := httptest.NewRequest("GET", "/foo?limit=X", http.NoBody)
return test{
ctx: context.Background(),
statusCode: 400,
@ -222,7 +222,7 @@ func TestHandler_GetProvisioners(t *testing.T) {
}
},
"fail/auth.GetProvisioners": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
auth := &mockAdminAuthority{
MockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) {
assert.Equals(t, "", cursor)
@ -244,7 +244,7 @@ func TestHandler_GetProvisioners(t *testing.T) {
}
},
"ok": func(t *testing.T) test {
req := httptest.NewRequest("GET", "/foo", nil)
req := httptest.NewRequest("GET", "/foo", http.NoBody)
provisioners := provisioner.List{
&provisioner.OIDC{
Type: "OIDC",
@ -481,7 +481,7 @@ func TestHandler_DeleteProvisioner(t *testing.T) {
}
var tests = map[string]func(t *testing.T) test{
"fail/auth.LoadProvisionerByID": func(t *testing.T) test {
req := httptest.NewRequest("DELETE", "/foo?id=provID", nil)
req := httptest.NewRequest("DELETE", "/foo?id=provID", http.NoBody)
chiCtx := chi.NewRouteContext()
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
@ -504,7 +504,7 @@ func TestHandler_DeleteProvisioner(t *testing.T) {
}
},
"fail/auth.LoadProvisionerByName": func(t *testing.T) test {
req := httptest.NewRequest("DELETE", "/foo", nil)
req := httptest.NewRequest("DELETE", "/foo", http.NoBody)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
@ -528,7 +528,7 @@ func TestHandler_DeleteProvisioner(t *testing.T) {
}
},
"fail/auth.RemoveProvisioner": func(t *testing.T) test {
req := httptest.NewRequest("DELETE", "/foo", nil)
req := httptest.NewRequest("DELETE", "/foo", http.NoBody)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
@ -560,7 +560,7 @@ func TestHandler_DeleteProvisioner(t *testing.T) {
}
},
"ok": func(t *testing.T) test {
req := httptest.NewRequest("DELETE", "/foo", nil)
req := httptest.NewRequest("DELETE", "/foo", http.NoBody)
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("name", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)

@ -6,7 +6,7 @@ import (
"net/http"
"net/url"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/smallstep/certificates/api/read"
"github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority/admin"
@ -56,9 +56,7 @@ func validateWebhook(webhook *linkedca.Webhook) error {
}
// kind
switch webhook.Kind {
case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING, linkedca.Webhook_SCEPCHALLENGE:
default:
if _, ok := linkedca.Webhook_Kind_name[int32(webhook.Kind)]; !ok || webhook.Kind == linkedca.Webhook_NO_KIND {
return admin.NewError(admin.ErrorBadRequestType, "webhook kind %q is invalid", webhook.Kind)
}

@ -6,11 +6,12 @@ import (
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/admin"
"github.com/stretchr/testify/assert"
@ -375,7 +376,7 @@ func TestWebhookAdminResponder_DeleteProvisionerWebhook(t *testing.T) {
}
ctx = linkedca.NewContextWithProvisioner(ctx, prov)
ctx = admin.NewContext(ctx, &admin.MockDB{})
req := httptest.NewRequest("DELETE", "/foo", nil).WithContext(ctx)
req := httptest.NewRequest("DELETE", "/foo", http.NoBody).WithContext(ctx)
war := NewWebhookAdminResponder()

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
@ -61,7 +62,9 @@ type Authority struct {
x509Enforcers []provisioner.CertificateEnforcer
// SCEP CA
scepService *scep.Service
scepOptions *scep.Options
validateSCEP bool
scepAuthority *scep.Authority
// SSH CA
sshHostPassword []byte
@ -122,6 +125,7 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) {
var a = &Authority{
config: cfg,
certificates: new(sync.Map),
validateSCEP: true,
}
// Apply options.
@ -261,6 +265,24 @@ func (a *Authority) ReloadAdminResources(ctx context.Context) error {
a.config.AuthorityConfig.Admins = adminList
a.admins = adminClxn
switch {
case a.requiresSCEP() && a.GetSCEP() == nil:
// TODO(hs): try to initialize SCEP here too? It's a bit
// problematic if this method is called as part of an update
// via Admin API and a password needs to be provided.
case a.requiresSCEP() && a.GetSCEP() != nil:
// update the SCEP Authority with the currently active SCEP
// provisioner names and revalidate the configuration.
a.scepAuthority.UpdateProvisioners(a.getSCEPProvisionerNames())
if err := a.scepAuthority.Validate(); err != nil {
log.Printf("failed validating SCEP authority: %v\n", err)
}
case !a.requiresSCEP() && a.GetSCEP() != nil:
// TODO(hs): don't remove the authority if we can't also
// reload it.
//a.scepAuthority = nil
}
return nil
}
@ -640,48 +662,83 @@ func (a *Authority) init() error {
return err
}
// Check if a KMS with decryption capability is required and available
if a.requiresDecrypter() {
if _, ok := a.keyManager.(kmsapi.Decrypter); !ok {
return errors.New("keymanager doesn't provide crypto.Decrypter")
// The SCEP functionality is provided through an instance of
// scep.Authority. It is initialized when the CA is started and
// if it doesn't exist yet. It gets refreshed if it already
// exists. If the SCEP authority is no longer required on reload,
// it gets removed.
// TODO(hs): reloading through SIGHUP doesn't hit these cases. This
// is because an entirely new authority.Authority is created, including
// a new scep.Authority. Look into this to see if we want this to
// keep working like that, or want to reuse a single instance and
// update that.
switch {
case a.requiresSCEP() && a.GetSCEP() == nil:
if a.scepOptions == nil {
options := &scep.Options{
Roots: a.rootX509Certs,
Intermediates: a.intermediateX509Certs,
SignerCert: a.intermediateX509Certs[0],
}
if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey,
Password: a.password,
}); err != nil {
return err
}
// TODO(hs): instead of creating the decrypter here, pass the
// intermediate key + chain down to the SCEP authority,
// and only instantiate it when required there. Is that possible?
// Also with entering passwords?
// TODO(hs): if moving the logic, try improving the logic for the
// decrypter password too? Right now it needs to be entered multiple
// times; I've observed it to be three times maximum, every time
// the intermediate key is read.
_, isRSA := options.Signer.Public().(*rsa.PublicKey)
if km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSA {
if decrypter, err := km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: a.config.IntermediateKey,
Password: a.password,
}); err == nil {
// only pass the decrypter down when it was successfully created,
// meaning it's an RSA key, and `CreateDecrypter` did not fail.
options.Decrypter = decrypter
options.DecrypterCert = options.Intermediates[0]
}
}
a.scepOptions = options
}
}
// TODO: decide if this is a good approach for providing the SCEP functionality
// It currently mirrors the logic for the x509CAService
if a.requiresSCEPService() && a.scepService == nil {
var options scep.Options
// provide the current SCEP provisioner names, so that the provisioners
// can be validated when the CA is started.
a.scepOptions.SCEPProvisionerNames = a.getSCEPProvisionerNames()
// Read intermediate and create X509 signer and decrypter for default CAS.
options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert)
if err != nil {
return err
}
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...)
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey,
Password: a.password,
})
// create a new SCEP authority
scepAuthority, err := scep.New(a, *a.scepOptions)
if err != nil {
return err
}
if km, ok := a.keyManager.(kmsapi.Decrypter); ok {
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: a.config.IntermediateKey,
Password: a.password,
})
if err != nil {
return err
if a.validateSCEP {
// validate the SCEP authority
if err := scepAuthority.Validate(); err != nil {
a.initLogf("failed validating SCEP authority: %v", err)
}
}
a.scepService, err = scep.NewService(ctx, options)
if err != nil {
return err
// set the SCEP authority
a.scepAuthority = scepAuthority
case !a.requiresSCEP() && a.GetSCEP() != nil:
// clear the SCEP authority if it's no longer required
a.scepAuthority = nil
case a.requiresSCEP() && a.GetSCEP() != nil:
// update the SCEP Authority with the currently active SCEP
// provisioner names and revalidate the configuration.
a.scepAuthority.UpdateProvisioners(a.getSCEPProvisionerNames())
if err := a.scepAuthority.Validate(); err != nil {
log.Printf("failed validating SCEP authority: %v\n", err)
}
// TODO: mimick the x509CAService GetCertificateAuthority here too?
}
// Load X509 constraints engine.
@ -833,17 +890,9 @@ func (a *Authority) IsRevoked(sn string) (bool, error) {
return a.db.IsRevoked(sn)
}
// requiresDecrypter returns whether the Authority
// requires a KMS that provides a crypto.Decrypter
// Currently this is only required when SCEP is
// enabled.
func (a *Authority) requiresDecrypter() bool {
return a.requiresSCEPService()
}
// requiresSCEPService iterates over the configured provisioners
// and determines if one of them is a SCEP provisioner.
func (a *Authority) requiresSCEPService() bool {
// requiresSCEP iterates over the configured provisioners
// and determines if at least one of them is a SCEP provisioner.
func (a *Authority) requiresSCEP() bool {
for _, p := range a.config.AuthorityConfig.Provisioners {
if p.GetType() == provisioner.TypeSCEP {
return true
@ -852,13 +901,21 @@ func (a *Authority) requiresSCEPService() bool {
return false
}
// GetSCEPService returns the configured SCEP Service.
//
// TODO: this function is intended to exist temporarily in order to make SCEP
// work more easily. It can be made more correct by using the right
// interfaces/abstractions after it works as expected.
func (a *Authority) GetSCEPService() *scep.Service {
return a.scepService
// getSCEPProvisionerNames returns the names of the SCEP provisioners
// that are currently available in the CA.
func (a *Authority) getSCEPProvisionerNames() (names []string) {
for _, p := range a.config.AuthorityConfig.Provisioners {
if p.GetType() == provisioner.TypeSCEP {
names = append(names, p.GetName())
}
}
return
}
// GetSCEP returns the configured SCEP Authority
func (a *Authority) GetSCEP() *scep.Authority {
return a.scepAuthority
}
func (a *Authority) startCRLGenerator() error {

@ -478,7 +478,7 @@ func testScepAuthority(t *testing.T, opts ...Option) *Authority {
return a
}
func TestAuthority_GetSCEPService(t *testing.T) {
func TestAuthority_GetSCEP(t *testing.T) {
_ = testScepAuthority(t)
p := provisioner.List{
&provisioner.SCEP{
@ -542,7 +542,7 @@ func TestAuthority_GetSCEPService(t *testing.T) {
return
}
if tt.wantService {
if got := a.GetSCEPService(); (got != nil) != tt.wantService {
if got := a.GetSCEP(); (got != nil) != tt.wantService {
t.Errorf("Authority.GetSCEPService() = %v, wantService %v", got, tt.wantService)
}
}

@ -177,7 +177,7 @@ func (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedc
if !adminFound {
return nil, admin.NewError(admin.ErrorUnauthorizedType,
"adminHandler.authorizeToken; unable to load admin with subject(s) %s and provisioner '%s'",
adminSANs, claims.Issuer)
adminSANs, prov.GetName())
}
if strings.HasPrefix(r.URL.Path, "/admin/admins") && (r.Method != "GET") && adm.Type != linkedca.Admin_SUPER_ADMIN {
@ -214,7 +214,7 @@ func (a *Authority) Authorize(ctx context.Context, token string) ([]provisioner.
var opts = []interface{}{errs.WithKeyVal("token", token)}
switch m := provisioner.MethodFromContext(ctx); m {
case provisioner.SignMethod:
case provisioner.SignMethod, provisioner.SignIdentityMethod:
signOpts, err := a.authorizeSign(ctx, token)
return signOpts, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...)
case provisioner.RevokeMethod:

@ -1,6 +1,7 @@
package config
import (
"bytes"
"encoding/json"
"fmt"
"net"
@ -35,6 +36,9 @@ var (
// DefaultEnableSSHCA enable SSH CA features per provisioner or globally
// for all provisioners.
DefaultEnableSSHCA = false
// DefaultDisableSmallstepExtensions is the default value for the
// DisableSmallstepExtensions provisioner claim.
DefaultDisableSmallstepExtensions = false
// DefaultCRLCacheDuration is the default cache duration for the CRL.
DefaultCRLCacheDuration = &provisioner.Duration{Duration: 24 * time.Hour}
// DefaultCRLExpiredDuration is the default duration in which expired
@ -43,18 +47,19 @@ var (
// GlobalProvisionerClaims is the default duration that expired certificates
// remain in the CRL after expiration.
GlobalProvisionerClaims = provisioner.Claims{
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
MinUserSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &provisioner.Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &provisioner.Duration{Duration: 16 * time.Hour},
MinHostSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &DefaultEnableSSHCA,
DisableRenewal: &DefaultDisableRenewal,
AllowRenewalAfterExpiry: &DefaultAllowRenewalAfterExpiry,
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
MinUserSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &provisioner.Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &provisioner.Duration{Duration: 16 * time.Hour},
MinHostSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &DefaultEnableSSHCA,
DisableRenewal: &DefaultDisableRenewal,
AllowRenewalAfterExpiry: &DefaultAllowRenewalAfterExpiry,
DisableSmallstepExtensions: &DefaultDisableSmallstepExtensions,
}
)
@ -254,15 +259,16 @@ func (c *Config) Init() {
// Save saves the configuration to the given filename.
func (c *Config) Save(filename string) error {
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return errors.Wrapf(err, "error opening %s", filename)
}
defer f.Close()
enc := json.NewEncoder(f)
var b bytes.Buffer
enc := json.NewEncoder(&b)
enc.SetIndent("", "\t")
return errors.Wrapf(enc.Encode(c), "error writing %s", filename)
if err := enc.Encode(c); err != nil {
return fmt.Errorf("error encoding configuration: %w", err)
}
if err := os.WriteFile(filename, b.Bytes(), 0600); err != nil {
return fmt.Errorf("error writing %q: %w", filename, err)
}
return nil
}
// Commit saves the current configuration to the same

@ -18,6 +18,7 @@ import (
"github.com/smallstep/certificates/cas"
casapi "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/scep"
)
// Option sets options to the Authority.
@ -205,6 +206,17 @@ func WithX509SignerFunc(fn func() ([]*x509.Certificate, crypto.Signer, error)) O
}
}
// WithFullSCEPOptions defines the options used for SCEP support.
//
// This feature is EXPERIMENTAL and might change at any time.
func WithFullSCEPOptions(options *scep.Options) Option {
return func(a *Authority) error {
a.scepOptions = options
a.validateSCEP = false
return nil
}
}
// WithSSHUserSigner defines the signer used to sign SSH user certificates.
func WithSSHUserSigner(s crypto.Signer) Option {
return func(a *Authority) error {

@ -257,7 +257,7 @@ func (p *ACME) AuthorizeSign(context.Context, string) ([]SignOption, error) {
opts := []SignOption{
p,
// modifiers / withOptions
newProvisionerExtensionOption(TypeACME, p.Name, ""),
newProvisionerExtensionOption(TypeACME, p.Name, "").WithControllerOptions(p.ctl),
newForceCNOption(p.ForceCN),
profileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),
// validators

@ -25,6 +25,8 @@ import (
"github.com/smallstep/certificates/errs"
"github.com/smallstep/certificates/webhook"
_ "embed"
)
// awsIssuer is the string used as issuer in the generated tokens.
@ -50,148 +52,10 @@ const awsMetadataTokenHeader = "X-aws-ec2-metadata-token" //nolint:gosec // no c
const awsMetadataTokenTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds" //nolint:gosec // no credentials here
// awsCertificate is the certificate used to validate the instance identity
// signature.
//
// The first certificate is used in:
//
// ap-northeast-2, ap-south-1, ap-southeast-1, ap-southeast-2
// eu-central-1, eu-north-1, eu-west-1, eu-west-2, eu-west-3
// us-east-1, us-east-2, us-west-1, us-west-2
// ca-central-1, sa-east-1
//
// The second certificate is used in:
//
// eu-south-1
//
// The third certificate is used in:
//
// ap-east-1
//
// The fourth certificate is used in:
//
// af-south-1
//
// The fifth certificate is used in:
//
// me-south-1
//
// The sixth certificate is used in:
//
// me-central-1
//
// The seventh certificate is used in:
// signature. It is embedded in the binary at compile time.
//
// ap-southeast-3
const awsCertificate = `-----BEGIN CERTIFICATE-----
MIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw
FgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu
Y29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC
VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV
BAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN//SRK2knbjySG0ho3yqQM3
e2TDhWO8D2e8+XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD
jbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL
XeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs
77VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh
dHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h
em9uYXdzLmNvbYIJAKnL4UEDMN/FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
BQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF+j9uO6jz7VOmJqO+pRlAbRlvY8T
C1haGgSI/A1uZUKs/Zfnph0oEI0/hu1IIJ/SKBDtN5lvmZ/IzbOPIJWirlsllQIQ
7zvWbGd9c9+Rm3p04oTvhup99la7kZqevJK0QRdD/6NpCKsqP/0=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICNjCCAZ+gAwIBAgIJAOZ3GEIaDcugMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0xOTEwMjQx
NTE5MDlaGA8yMTk5MDMyOTE1MTkwOVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgT
EFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0Ft
YXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
gQCjiPgW3vsXRj4JoA16WQDyoPc/eh3QBARaApJEc4nPIGoUolpAXcjFhWplo2O+
ivgfCsc4AU9OpYdAPha3spLey/bhHPRi1JZHRNqScKP0hzsCNmKhfnZTIEQCFvsp
DRp4zr91/WS06/flJFBYJ6JHhp0KwM81XQG59lV6kkoW7QIDAQABMA0GCSqGSIb3
DQEBCwUAA4GBAGLLrY3P+HH6C57dYgtJkuGZGT2+rMkk2n81/abzTJvsqRqGRrWv
XRKRXlKdM/dfiuYGokDGxiC0Mg6TYy6wvsR2qRhtXW1OtZkiHWcQCnOttz+8vpew
wx8JGMvowtuKB1iMsbwyRqZkFYLcvH+Opfb/Aayi20/ChQLdI6M2R5VU
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICSzCCAbQCCQDtQvkVxRvK9TANBgkqhkiG9w0BAQsFADBqMQswCQYDVQQGEwJV
UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2VhdHRsZTEYMBYGA1UE
ChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1hem9uYXdzLmNvbTAe
Fw0xOTAyMDMwMzAwMDZaFw0yOTAyMDIwMzAwMDZaMGoxCzAJBgNVBAYTAlVTMRMw
EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgwFgYDVQQKEw9B
bWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3MuY29tMIGfMA0G
CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1kkHXYTfc7gY5Q55JJhjTieHAgacaQkiR
Pity9QPDE3b+NXDh4UdP1xdIw73JcIIG3sG9RhWiXVCHh6KkuCTqJfPUknIKk8vs
M3RXflUpBe8Pf+P92pxqPMCz1Fr2NehS3JhhpkCZVGxxwLC5gaG0Lr4rFORubjYY
Rh84dK98VwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAA6xV9f0HMqXjPHuGILDyaNN
dKcvplNFwDTydVg32MNubAGnecoEBtUPtxBsLoVYXCOb+b5/ZMDubPF9tU/vSXuo
TpYM5Bq57gJzDRaBOntQbX9bgHiUxw6XZWaTS/6xjRJDT5p3S1E0mPI3lP/eJv4o
Ezk5zb3eIf10/sqt4756
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICNjCCAZ+gAwIBAgIJAKumfZiRrNvHMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0xOTExMjcw
NzE0MDVaGA8yMTk5MDUwMjA3MTQwNVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgT
EFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0Ft
YXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
gQDFd571nUzVtke3rPyRkYfvs3jh0C0EMzzG72boyUNjnfw1+m0TeFraTLKb9T6F
7TuB/ZEN+vmlYqr2+5Va8U8qLbPF0bRH+FdaKjhgWZdYXxGzQzU3ioy5W5ZM1VyB
7iUsxEAlxsybC3ziPYaHI42UiTkQNahmoroNeqVyHNnBpQIDAQABMA0GCSqGSIb3
DQEBCwUAA4GBAAJLylWyElEgOpW4B1XPyRVD4pAds8Guw2+krgqkY0HxLCdjosuH
RytGDGN+q75aAoXzW5a7SGpxLxk6Hfv0xp3RjDHsoeP0i1d8MD3hAC5ezxS4oukK
s5gbPOnokhKTMPXbTdRn5ZifCbWlx+bYN/mTYKvxho7b5SVg2o1La9aK
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDPDCCAqWgAwIBAgIJAMl6uIV/zqJFMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSAw
HgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzEaMBgGA1UEAwwRZWMyLmFt
YXpvbmF3cy5jb20wIBcNMTkwNDI2MTQzMjQ3WhgPMjE5ODA5MjkxNDMyNDdaMHIx
CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0
dGxlMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzEaMBgGA1UEAwwR
ZWMyLmFtYXpvbmF3cy5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALVN
CDTZEnIeoX1SEYqq6k1BV0ZlpY5y3KnoOreCAE589TwS4MX5+8Fzd6AmACmugeBP
Qk7Hm6b2+g/d4tWycyxLaQlcq81DB1GmXehRkZRgGeRge1ePWd1TUA0I8P/QBT7S
gUePm/kANSFU+P7s7u1NNl+vynyi0wUUrw7/wIZTAgMBAAGjgdcwgdQwHQYDVR0O
BBYEFILtMd+T4YgH1cgc+hVsVOV+480FMIGkBgNVHSMEgZwwgZmAFILtMd+T4YgH
1cgc+hVsVOV+480FoXakdDByMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGlu
Z3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEgMB4GA1UECgwXQW1hem9uIFdlYiBTZXJ2
aWNlcyBMTEMxGjAYBgNVBAMMEWVjMi5hbWF6b25hd3MuY29tggkAyXq4hX/OokUw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQBhkNTBIFgWFd+ZhC/LhRUY
4OjEiykmbEp6hlzQ79T0Tfbn5A4NYDI2icBP0+hmf6qSnIhwJF6typyd1yPK5Fqt
NTpxxcXmUKquX+pHmIkK1LKDO8rNE84jqxrxRsfDi6by82fjVYf2pgjJW8R1FAw+
mL5WQRFexbfB5aXhcMo0AA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXjRrnDjMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
AlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl
MSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MTQxODM5
MzNaGA8yMjAwMDQxNDE4MzkzM1owXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh
c2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv
biBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDc
aTgW/KyA6zyruJQrYy00a6wqLA7eeUzk3bMiTkLsTeDQfrkaZMfBAjGaaOymRo1C
3qzE4rIenmahvUplu9ZmLwL1idWXMRX2RlSvIt+d2SeoKOKQWoc2UOFZMHYxDue7
zkyk1CIRaBukTeY13/RIrlc6X61zJ5BBtZXlHwayjQIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBABTqTy3R6RXKPW45FA+cgo7YZEj/Cnz5YaoUivRRdX2A83BHuBTvJE2+
WX00FTEj4hRVjameE1nENoO8Z7fUVloAFDlDo69fhkJeSvn51D1WRrPnoWGgEfr1
+OfK1bAcKTtfkkkP9r4RdwSjKzO5Zu/B+Wqm3kVEz/QNcz6npmA6
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXbVDG2yMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
AlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl
MSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTAxMDYwMDE1
MzBaGA8yMjAwMDEwNjAwMTUzMFowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh
c2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv
biBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCn
CS/Vbt0gQ1ebWcur2hSO7PnJifE4OPxQ7RgSAlc4/spJp1sDP+ZrS0LO1ZJfKhXf
1R9S3AUwLnsc7b+IuVXdY5LK9RKqu64nyXP5dx170zoL8loEyCSuRR2fs+04i2Qs
WBVP+KFNAn7P5L1EHRjkgTO8kjNKviwRV+OkP9ab5wIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBAI4WUy6+DKh0JDSzQEZNyBgNlSoSuC2owtMxCwGB6nBfzzfcekWvs6eo
fLTSGovrReX7MtVgrcJBZjmPIentw5dWUs+87w/g9lNwUnUt0ZHYyh2tuBG6hVJu
UEwDJ/z3wDd6wQviLOTF3MITawt9P8siR1hXqLJNxpjRQFZrgHqi
-----END CERTIFICATE-----`
//go:embed aws_certificates.pem
var awsCertificate string
// awsSignatureAlgorithm is the signature algorithm used to verify the identity
// document signature.
@ -472,7 +336,7 @@ func (p *AWS) Init(config Config) (err error) {
// AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation.
func (p *AWS) AuthorizeSign(_ context.Context, token string) ([]SignOption, error) {
func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
payload, err := p.authorizeToken(token)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "aws.AuthorizeSign")
@ -499,7 +363,7 @@ func (p *AWS) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
net.ParseIP(doc.PrivateIP),
}),
emailAddressesValidator(nil),
urisValidator(nil),
newURIsValidator(ctx, nil),
)
// Template options
@ -515,7 +379,7 @@ func (p *AWS) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
p,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID),
newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID).WithControllerOptions(p.ctl),
profileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},

@ -0,0 +1,247 @@
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/verify-signature.html
# default certificate for "other regions"
-----BEGIN CERTIFICATE-----
MIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw
FgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu
Y29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC
VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV
BAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN//SRK2knbjySG0ho3yqQM3
e2TDhWO8D2e8+XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD
jbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL
XeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs
77VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh
dHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h
em9uYXdzLmNvbYIJAKnL4UEDMN/FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
BQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF+j9uO6jz7VOmJqO+pRlAbRlvY8T
C1haGgSI/A1uZUKs/Zfnph0oEI0/hu1IIJ/SKBDtN5lvmZ/IzbOPIJWirlsllQIQ
7zvWbGd9c9+Rm3p04oTvhup99la7kZqevJK0QRdD/6NpCKsqP/0=
-----END CERTIFICATE-----
# certificate for eu-south-1
-----BEGIN CERTIFICATE-----
MIICNjCCAZ+gAwIBAgIJAOZ3GEIaDcugMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0xOTEwMjQx
NTE5MDlaGA8yMTk5MDMyOTE1MTkwOVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgT
EFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0Ft
YXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
gQCjiPgW3vsXRj4JoA16WQDyoPc/eh3QBARaApJEc4nPIGoUolpAXcjFhWplo2O+
ivgfCsc4AU9OpYdAPha3spLey/bhHPRi1JZHRNqScKP0hzsCNmKhfnZTIEQCFvsp
DRp4zr91/WS06/flJFBYJ6JHhp0KwM81XQG59lV6kkoW7QIDAQABMA0GCSqGSIb3
DQEBCwUAA4GBAGLLrY3P+HH6C57dYgtJkuGZGT2+rMkk2n81/abzTJvsqRqGRrWv
XRKRXlKdM/dfiuYGokDGxiC0Mg6TYy6wvsR2qRhtXW1OtZkiHWcQCnOttz+8vpew
wx8JGMvowtuKB1iMsbwyRqZkFYLcvH+Opfb/Aayi20/ChQLdI6M2R5VU
-----END CERTIFICATE-----
# certificate for ap-east-1
-----BEGIN CERTIFICATE-----
MIICSzCCAbQCCQDtQvkVxRvK9TANBgkqhkiG9w0BAQsFADBqMQswCQYDVQQGEwJV
UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2VhdHRsZTEYMBYGA1UE
ChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1hem9uYXdzLmNvbTAe
Fw0xOTAyMDMwMzAwMDZaFw0yOTAyMDIwMzAwMDZaMGoxCzAJBgNVBAYTAlVTMRMw
EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgwFgYDVQQKEw9B
bWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3MuY29tMIGfMA0G
CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1kkHXYTfc7gY5Q55JJhjTieHAgacaQkiR
Pity9QPDE3b+NXDh4UdP1xdIw73JcIIG3sG9RhWiXVCHh6KkuCTqJfPUknIKk8vs
M3RXflUpBe8Pf+P92pxqPMCz1Fr2NehS3JhhpkCZVGxxwLC5gaG0Lr4rFORubjYY
Rh84dK98VwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAA6xV9f0HMqXjPHuGILDyaNN
dKcvplNFwDTydVg32MNubAGnecoEBtUPtxBsLoVYXCOb+b5/ZMDubPF9tU/vSXuo
TpYM5Bq57gJzDRaBOntQbX9bgHiUxw6XZWaTS/6xjRJDT5p3S1E0mPI3lP/eJv4o
Ezk5zb3eIf10/sqt4756
-----END CERTIFICATE-----
# certificate for af-south-1
-----BEGIN CERTIFICATE-----
MIICNjCCAZ+gAwIBAgIJAKumfZiRrNvHMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0xOTExMjcw
NzE0MDVaGA8yMTk5MDUwMjA3MTQwNVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgT
EFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0Ft
YXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
gQDFd571nUzVtke3rPyRkYfvs3jh0C0EMzzG72boyUNjnfw1+m0TeFraTLKb9T6F
7TuB/ZEN+vmlYqr2+5Va8U8qLbPF0bRH+FdaKjhgWZdYXxGzQzU3ioy5W5ZM1VyB
7iUsxEAlxsybC3ziPYaHI42UiTkQNahmoroNeqVyHNnBpQIDAQABMA0GCSqGSIb3
DQEBCwUAA4GBAAJLylWyElEgOpW4B1XPyRVD4pAds8Guw2+krgqkY0HxLCdjosuH
RytGDGN+q75aAoXzW5a7SGpxLxk6Hfv0xp3RjDHsoeP0i1d8MD3hAC5ezxS4oukK
s5gbPOnokhKTMPXbTdRn5ZifCbWlx+bYN/mTYKvxho7b5SVg2o1La9aK
-----END CERTIFICATE-----
# certificate for me-south-1
-----BEGIN CERTIFICATE-----
MIIDPDCCAqWgAwIBAgIJAMl6uIV/zqJFMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSAw
HgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzEaMBgGA1UEAwwRZWMyLmFt
YXpvbmF3cy5jb20wIBcNMTkwNDI2MTQzMjQ3WhgPMjE5ODA5MjkxNDMyNDdaMHIx
CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0
dGxlMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzEaMBgGA1UEAwwR
ZWMyLmFtYXpvbmF3cy5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALVN
CDTZEnIeoX1SEYqq6k1BV0ZlpY5y3KnoOreCAE589TwS4MX5+8Fzd6AmACmugeBP
Qk7Hm6b2+g/d4tWycyxLaQlcq81DB1GmXehRkZRgGeRge1ePWd1TUA0I8P/QBT7S
gUePm/kANSFU+P7s7u1NNl+vynyi0wUUrw7/wIZTAgMBAAGjgdcwgdQwHQYDVR0O
BBYEFILtMd+T4YgH1cgc+hVsVOV+480FMIGkBgNVHSMEgZwwgZmAFILtMd+T4YgH
1cgc+hVsVOV+480FoXakdDByMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGlu
Z3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEgMB4GA1UECgwXQW1hem9uIFdlYiBTZXJ2
aWNlcyBMTEMxGjAYBgNVBAMMEWVjMi5hbWF6b25hd3MuY29tggkAyXq4hX/OokUw
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQBhkNTBIFgWFd+ZhC/LhRUY
4OjEiykmbEp6hlzQ79T0Tfbn5A4NYDI2icBP0+hmf6qSnIhwJF6typyd1yPK5Fqt
NTpxxcXmUKquX+pHmIkK1LKDO8rNE84jqxrxRsfDi6by82fjVYf2pgjJW8R1FAw+
mL5WQRFexbfB5aXhcMo0AA==
-----END CERTIFICATE-----
# certificate for cn-north-1, cn-northwest-1
-----BEGIN CERTIFICATE-----
MIIDCzCCAnSgAwIBAgIJALSOMbOoU2svMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0yMzA3MDQw
ODM1MzlaFw0yODA3MDIwODM1MzlaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBX
YXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6
b24gV2ViIFNlcnZpY2VzIExMQzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
uhhUNlqAZdcWWB/OSDVDGk3OA99EFzOn/mJlmciQ/Xwu2dFJWmSCqEAE6gjufCjQ
q3voxAhC2CF+elKtJW/C0Sz/LYo60PUqd6iXF4h+upB9HkOOGuWHXsHBTsvgkgGA
1CGgel4U0Cdq+23eANr8N8m28UzljjSnTlrYCHtzN4sCAwEAAaOB1DCB0TALBgNV
HQ8EBAMCB4AwHQYDVR0OBBYEFBkZu3wT27NnYgrfH+xJz4HJaNJoMIGOBgNVHSME
gYYwgYOAFBkZu3wT27NnYgrfH+xJz4HJaNJooWCkXjBcMQswCQYDVQQGEwJVUzEZ
MBcGA1UECBMQV2FzaGluZ3RvbiBTdGF0ZTEQMA4GA1UEBxMHU2VhdHRsZTEgMB4G
A1UEChMXQW1hem9uIFdlYiBTZXJ2aWNlcyBMTEOCCQC0jjGzqFNrLzASBgNVHRMB
Af8ECDAGAQH/AgEAMA0GCSqGSIb3DQEBCwUAA4GBAECji43p+oPkYqmzll7e8Hgb
oADS0ph+YUz5P/bUCm61wFjlxaTfwKcuTR3ytj7bFLoW5Bm7Sa+TCl3lOGb2taon
2h+9NirRK6JYk87LMNvbS40HGPFumJL2NzEsGUeK+MRiWu+Oh5/lJGii3qw4YByx
SUDlRyNy1jJFstEZjOhs
-----END CERTIFICATE-----
# certificate for eu-central-2
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXjSGFGiMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
AlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl
MSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MTQyMDM1
MTJaGA8yMjAwMDQxNDIwMzUxMlowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh
c2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv
biBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2
mdGdps5Rz2jzYcGNsgETTGUthJRrVqSnUWJXTlVaIbkGPLKO6Or7AfWKFp2sgRJ8
vLsjoBVR5cESVK7cuK1wItjvJyi/opKZAUusJx2hpgU3pUHhlp9ATh/VeVD582jT
d9IY+8t5MDa6Z3fGliByEiXz0LEHdi8MBacLREu1TwIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBAILlpoE3k9o7KdALAxsFJNitVS+g3RMzdbiFM+7MA63Nv5fsf+0xgcjS
NBElvPCDKFvTJl4QQhToy056llO5GvdS9RK+H8xrP2mrqngApoKTApv93vHBixgF
Sn5KrczRO0YSm3OjkqbydU7DFlmkXXR7GYE+5jbHvQHYiT1J5sMu
-----END CERTIFICATE-----
# certificate for ap-south-2
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXjwLj9CMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
AlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl
MSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MjAxNjQ3
NDVaGA8yMjAwMDQyMDE2NDc0NVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh
c2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv
biBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDT
wHu0ND+sFcobrjvcAYm0PNRD8f4R1jAzvoLt2+qGeOTAyO1Httj6cmsYN3AP1hN5
iYuppFiYsl2eNPa/CD0Vg0BAfDFlV5rzjpA0j7TJabVh4kj7JvtD+xYMi6wEQA4x
6SPONY4OeZ2+8o/HS8nucpWDVdPRO6ciWUlMhjmDmwIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBAAy6sgTdRkTqELHBeWj69q60xHyUmsWqHAQNXKVc9ApWGG4onzuqlMbG
ETwUZ9mTq2vxlV0KvuetCDNS5u4cJsxe/TGGbYP0yP2qfMl0cCImzRI5W0gn8gog
dervfeT7nH5ih0TWEy/QDWfkQ601L4erm4yh4YQq8vcqAPSkf04N
-----END CERTIFICATE-----
# certificate for ap-southeast-3
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXbVDG2yMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
AlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl
MSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTAxMDYwMDE1
MzBaGA8yMjAwMDEwNjAwMTUzMFowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh
c2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv
biBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCn
CS/Vbt0gQ1ebWcur2hSO7PnJifE4OPxQ7RgSAlc4/spJp1sDP+ZrS0LO1ZJfKhXf
1R9S3AUwLnsc7b+IuVXdY5LK9RKqu64nyXP5dx170zoL8loEyCSuRR2fs+04i2Qs
WBVP+KFNAn7P5L1EHRjkgTO8kjNKviwRV+OkP9ab5wIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBAI4WUy6+DKh0JDSzQEZNyBgNlSoSuC2owtMxCwGB6nBfzzfcekWvs6eo
fLTSGovrReX7MtVgrcJBZjmPIentw5dWUs+87w/g9lNwUnUt0ZHYyh2tuBG6hVJu
UEwDJ/z3wDd6wQviLOTF3MITawt9P8siR1hXqLJNxpjRQFZrgHqi
-----END CERTIFICATE-----
# certificate for ap-southeast-4
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXjSh40SMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
AlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl
MSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MTQyMjM2
NDJaGA8yMjAwMDQxNDIyMzY0MlowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh
c2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv
biBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDH
ezwQr2VQpQSTW5TXNefiQrP+qWTGAbGsPeMX4hBMjAJUKys2NIRcRZaLM/BCew2F
IPVjNtlaj6Gwn9ipU4Mlz3zIwAMWi1AvGMSreppt+wV6MRtfOjh0Dvj/veJe88aE
ZJMozNgkJFRS+WFWsckQeL56tf6kY6QTlNo8V/0CsQIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBAF7vpPghH0FRo5gu49EArRNPrIvW1egMdZHrzJNqbztLCtV/wcgkqIww
uXYj+1rhlL+/iMpQWjdVGEqIZSeXn5fLmdx50eegFCwND837r9e8XYTiQS143Sxt
9+Yi6BZ7U7YD8kK9NBWoJxFqUeHdpRCs0O7COjT3gwm7ZxvAmssh
-----END CERTIFICATE-----
# certificate for eu-south-2
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXjwLkiaMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
AlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl
MSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MjAxNjQ3
NDhaGA8yMjAwMDQyMDE2NDc0OFowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh
c2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv
biBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDB
/VvR1+45Aey5zn3vPk6xBm5o9grSDL6D2iAuprQnfVXn8CIbSDbWFhA3fi5ippjK
kh3sl8VyCvCOUXKdOaNrYBrPRkrdHdBuL2Tc84RO+3m/rxIUZ2IK1fDlC6sWAjdd
f6sBrV2w2a78H0H8EwuwiSgttURBjwJ7KPPJCqaqrQIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBAKR+FzqQDzun/iMMzcFucmLMl5BxEblrFXOz7IIuOeiGkndmrqUeDCyk
ztLku45s7hxdNy4ltTuVAaE5aNBdw5J8U1mRvsKvHLy2ThH6hAWKwTqtPAJp7M21
GDwgDDOkPSz6XVOehg+hBgiphYp84DUbWVYeP8YqLEJSqscKscWC
-----END CERTIFICATE-----
# certificate for il-central-1
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAX0QQGVLMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
AlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl
MSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTExMTExODI2
MzVaGA8yMjAwMTExMTE4MjYzNVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh
c2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv
biBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDr
c24u3AgFxnoPgzxR6yFXOamcPuxYXhYKWmapb+S8vOy5hpLoRe4RkOrY0cM3bN07
GdEMlin5mU0y1t8y3ct4YewvmkgT42kTyMM+t1K4S0xsqjXxxS716uGYh7eWtkxr
Cihj8AbXN/6pa095h+7TZyl2n83keiNUzM2KoqQVMwIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBADwA6VVEIIZD2YL00F12po40xDLzIc9XvqFPS9iFaWi2ho8wLio7wA49
VYEFZSI9CR3SGB9tL8DUib97mlxmd1AcGShMmMlhSB29vhuhrUNB/FmU7H8s62/j
D6cOR1A1cClIyZUe1yT1ZbPySCs43J+Thr8i8FSRxzDBSZZi5foW
-----END CERTIFICATE-----
# certificate for me-central-1
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXjRrnDjMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
AlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl
MSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MTQxODM5
MzNaGA8yMjAwMDQxNDE4MzkzM1owXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh
c2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv
biBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDc
aTgW/KyA6zyruJQrYy00a6wqLA7eeUzk3bMiTkLsTeDQfrkaZMfBAjGaaOymRo1C
3qzE4rIenmahvUplu9ZmLwL1idWXMRX2RlSvIt+d2SeoKOKQWoc2UOFZMHYxDue7
zkyk1CIRaBukTeY13/RIrlc6X61zJ5BBtZXlHwayjQIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBABTqTy3R6RXKPW45FA+cgo7YZEj/Cnz5YaoUivRRdX2A83BHuBTvJE2+
WX00FTEj4hRVjameE1nENoO8Z7fUVloAFDlDo69fhkJeSvn51D1WRrPnoWGgEfr1
+OfK1bAcKTtfkkkP9r4RdwSjKzO5Zu/B+Wqm3kVEz/QNcz6npmA6
-----END CERTIFICATE-----
# certificate for us-gov-east-1 and us-gov-west-1
-----BEGIN CERTIFICATE-----
MIIDCzCCAnSgAwIBAgIJAIe9Hnq82O7UMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0yMTA3MTQx
NDI3NTdaFw0yNDA3MTMxNDI3NTdaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBX
YXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6
b24gV2ViIFNlcnZpY2VzIExMQzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
qaIcGFFTx/SO1W5G91jHvyQdGP25n1Y91aXCuOOWAUTvSvNGpXrI4AXNrQF+CmIO
C4beBASnHCx082jYudWBBl9Wiza0psYc9flrczSzVLMmN8w/c78F/95NfiQdnUQP
pvgqcMeJo82cgHkLR7XoFWgMrZJqrcUK0gnsQcb6kakCAwEAAaOB1DCB0TALBgNV
HQ8EBAMCB4AwHQYDVR0OBBYEFNWV53gWJz72F5B1ZVY4O/dfFYBPMIGOBgNVHSME
gYYwgYOAFNWV53gWJz72F5B1ZVY4O/dfFYBPoWCkXjBcMQswCQYDVQQGEwJVUzEZ
MBcGA1UECBMQV2FzaGluZ3RvbiBTdGF0ZTEQMA4GA1UEBxMHU2VhdHRsZTEgMB4G
A1UEChMXQW1hem9uIFdlYiBTZXJ2aWNlcyBMTEOCCQCHvR56vNju1DASBgNVHRMB
Af8ECDAGAQH/AgEAMA0GCSqGSIb3DQEBCwUAA4GBACrKjWj460GUPZCGm3/z0dIz
M2BPuH769wcOsqfFZcMKEysSFK91tVtUb1soFwH4/Lb/T0PqNrvtEwD1Nva5k0h2
xZhNNRmDuhOhW1K9wCcnHGRBwY5t4lYL6hNV6hcrqYwGMjTjcAjBG2yMgznSNFle
Rwi/S3BFXISixNx9cILu
-----END CERTIFICATE-----

@ -695,8 +695,9 @@ func TestAWS_AuthorizeSign(t *testing.T) {
assert.Equals(t, []net.IP(v), []net.IP{net.ParseIP("127.0.0.1")})
case emailAddressesValidator:
assert.Equals(t, v, nil)
case urisValidator:
assert.Equals(t, v, nil)
case *urisValidator:
assert.Equals(t, v.uris, nil)
assert.Equals(t, MethodFromContext(v.ctx), SignMethod)
case dnsNamesValidator:
assert.Equals(t, []string(v), []string{"ip-127-0-0-1.us-west-1.compute.internal"})
case *x509NamePolicyValidator:
@ -873,3 +874,27 @@ func TestAWS_AuthorizeRenew(t *testing.T) {
})
}
}
func TestAWS_HardcodedCertificates(t *testing.T) {
certBytes := []byte(awsCertificate)
var certs []*x509.Certificate
for len(certBytes) > 0 {
var block *pem.Block
block, certBytes = pem.Decode(certBytes)
if block == nil {
break
}
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
assert.FatalError(t, err)
// check that the certificate is not expired
assert.True(t, cert.NotAfter.After(time.Now()))
certs = append(certs, cert)
}
assert.Len(t, 14, certs, "expected 14 certificates in aws_certificates.pem")
}

@ -316,7 +316,7 @@ func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, str
// AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation.
func (p *Azure) AuthorizeSign(_ context.Context, token string) ([]SignOption, error) {
func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
_, name, group, subscription, identityObjectID, err := p.authorizeToken(token)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSign")
@ -382,7 +382,7 @@ func (p *Azure) AuthorizeSign(_ context.Context, token string) ([]SignOption, er
dnsNamesValidator([]string{name}),
ipAddressesValidator(nil),
emailAddressesValidator(nil),
urisValidator(nil),
newURIsValidator(ctx, nil),
)
// Enforce SANs in the template.
@ -398,7 +398,7 @@ func (p *Azure) AuthorizeSign(_ context.Context, token string) ([]SignOption, er
p,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID),
newProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID).WithControllerOptions(p.ctl),
profileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},

@ -560,8 +560,9 @@ func TestAzure_AuthorizeSign(t *testing.T) {
assert.Equals(t, v, nil)
case emailAddressesValidator:
assert.Equals(t, v, nil)
case urisValidator:
assert.Equals(t, v, nil)
case *urisValidator:
assert.Equals(t, v.uris, nil)
assert.Equals(t, MethodFromContext(v.ctx), SignMethod)
case dnsNamesValidator:
assert.Equals(t, []string(v), []string{"virtualMachine"})
case *x509NamePolicyValidator:

@ -26,6 +26,9 @@ type Claims struct {
// Renewal properties
DisableRenewal *bool `json:"disableRenewal,omitempty"`
AllowRenewalAfterExpiry *bool `json:"allowRenewalAfterExpiry,omitempty"`
// Other properties
DisableSmallstepExtensions *bool `json:"disableSmallstepExtensions,omitempty"`
}
// Claimer is the type that controls claims. It provides an interface around the
@ -47,20 +50,22 @@ func (c *Claimer) Claims() Claims {
disableRenewal := c.IsDisableRenewal()
allowRenewalAfterExpiry := c.AllowRenewalAfterExpiry()
enableSSHCA := c.IsSSHCAEnabled()
disableSmallstepExtensions := c.IsDisableSmallstepExtensions()
return Claims{
MinTLSDur: &Duration{c.MinTLSCertDuration()},
MaxTLSDur: &Duration{c.MaxTLSCertDuration()},
DefaultTLSDur: &Duration{c.DefaultTLSCertDuration()},
MinUserSSHDur: &Duration{c.MinUserSSHCertDuration()},
MaxUserSSHDur: &Duration{c.MaxUserSSHCertDuration()},
DefaultUserSSHDur: &Duration{c.DefaultUserSSHCertDuration()},
MinHostSSHDur: &Duration{c.MinHostSSHCertDuration()},
MaxHostSSHDur: &Duration{c.MaxHostSSHCertDuration()},
DefaultHostSSHDur: &Duration{c.DefaultHostSSHCertDuration()},
EnableSSHCA: &enableSSHCA,
DisableRenewal: &disableRenewal,
AllowRenewalAfterExpiry: &allowRenewalAfterExpiry,
MinTLSDur: &Duration{c.MinTLSCertDuration()},
MaxTLSDur: &Duration{c.MaxTLSCertDuration()},
DefaultTLSDur: &Duration{c.DefaultTLSCertDuration()},
MinUserSSHDur: &Duration{c.MinUserSSHCertDuration()},
MaxUserSSHDur: &Duration{c.MaxUserSSHCertDuration()},
DefaultUserSSHDur: &Duration{c.DefaultUserSSHCertDuration()},
MinHostSSHDur: &Duration{c.MinHostSSHCertDuration()},
MaxHostSSHDur: &Duration{c.MaxHostSSHCertDuration()},
DefaultHostSSHDur: &Duration{c.DefaultHostSSHCertDuration()},
EnableSSHCA: &enableSSHCA,
DisableRenewal: &disableRenewal,
AllowRenewalAfterExpiry: &allowRenewalAfterExpiry,
DisableSmallstepExtensions: &disableSmallstepExtensions,
}
}
@ -110,6 +115,15 @@ func (c *Claimer) IsDisableRenewal() bool {
return *c.claims.DisableRenewal
}
// IsDisableSmallstepExtensions returns whether Smallstep extensions, such as
// the provisioner extension, should be excluded from the certificate.
func (c *Claimer) IsDisableSmallstepExtensions() bool {
if c.claims == nil || c.claims.DisableSmallstepExtensions == nil {
return *c.global.DisableSmallstepExtensions
}
return *c.claims.DisableSmallstepExtensions
}
// AllowRenewalAfterExpiry returns if the renewal flow is authorized if the
// certificate is expired. If the property is not set within the provisioner
// then the global value from the authority configuration will be used.

@ -223,7 +223,7 @@ func (p *GCP) Init(config Config) (err error) {
// AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation.
func (p *GCP) AuthorizeSign(_ context.Context, token string) ([]SignOption, error) {
func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := p.authorizeToken(token)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "gcp.AuthorizeSign")
@ -254,7 +254,7 @@ func (p *GCP) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
}),
ipAddressesValidator(nil),
emailAddressesValidator(nil),
urisValidator(nil),
newURIsValidator(ctx, nil),
)
// Template SANs
@ -270,7 +270,7 @@ func (p *GCP) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
p,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject, "InstanceID", ce.InstanceID, "InstanceName", ce.InstanceName),
newProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject, "InstanceID", ce.InstanceID, "InstanceName", ce.InstanceName).WithControllerOptions(p.ctl),
profileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},

@ -567,8 +567,9 @@ func TestGCP_AuthorizeSign(t *testing.T) {
assert.Equals(t, v, nil)
case emailAddressesValidator:
assert.Equals(t, v, nil)
case urisValidator:
assert.Equals(t, v, nil)
case *urisValidator:
assert.Equals(t, v.uris, nil)
assert.Equals(t, MethodFromContext(v.ctx), SignMethod)
case dnsNamesValidator:
assert.Equals(t, []string(v), []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"})
case *x509NamePolicyValidator:

@ -150,7 +150,7 @@ func (p *JWK) AuthorizeRevoke(_ context.Context, token string) error {
}
// AuthorizeSign validates the given token.
func (p *JWK) AuthorizeSign(_ context.Context, token string) ([]SignOption, error) {
func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := p.authorizeToken(token, p.ctl.Audiences.Sign)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSign")
@ -187,12 +187,12 @@ func (p *JWK) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
self,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID),
newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID).WithControllerOptions(p.ctl),
profileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),
// validators
commonNameValidator(claims.Subject),
commonNameSliceValidator(append([]string{claims.Subject}, claims.SANs...)),
defaultPublicKeyValidator{},
defaultSANsValidator(claims.SANs),
newDefaultSANsValidator(ctx, claims.SANs),
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
p.ctl.newWebhookController(data, linkedca.Webhook_X509),

@ -309,14 +309,15 @@ func TestJWK_AuthorizeSign(t *testing.T) {
assert.Len(t, 0, v.KeyValuePairs)
case profileDefaultDuration:
assert.Equals(t, time.Duration(v), tt.prov.ctl.Claimer.DefaultTLSCertDuration())
case commonNameValidator:
assert.Equals(t, string(v), "subject")
case commonNameSliceValidator:
assert.Equals(t, []string(v), append([]string{"subject"}, tt.sans...))
case defaultPublicKeyValidator:
case *validityValidator:
assert.Equals(t, v.min, tt.prov.ctl.Claimer.MinTLSCertDuration())
assert.Equals(t, v.max, tt.prov.ctl.Claimer.MaxTLSCertDuration())
case defaultSANsValidator:
assert.Equals(t, []string(v), tt.sans)
case *defaultSANsValidator:
assert.Equals(t, v.sans, tt.sans)
assert.Equals(t, MethodFromContext(v.ctx), SignMethod)
case *x509NamePolicyValidator:
assert.Equals(t, nil, v.policyEngine)
case *WebhookController:

@ -238,7 +238,7 @@ func (p *K8sSA) AuthorizeSign(_ context.Context, token string) ([]SignOption, er
p,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeK8sSA, p.Name, ""),
newProvisionerExtensionOption(TypeK8sSA, p.Name, "").WithControllerOptions(p.ctl),
profileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},

@ -14,6 +14,8 @@ type methodKey struct{}
const (
// SignMethod is the method used to sign X.509 certificates.
SignMethod Method = iota
// SignIdentityMethod is the method used to sign X.509 identity certificates.
SignIdentityMethod
// RevokeMethod is the method used to revoke X.509 certificates.
RevokeMethod
// RenewMethod is the method used to renew X.509 certificates.
@ -33,6 +35,8 @@ func (m Method) String() string {
switch m {
case SignMethod:
return "sign-method"
case SignIdentityMethod:
return "sign-identity-method"
case RevokeMethod:
return "revoke-method"
case RenewMethod:

@ -150,7 +150,7 @@ func (p *Nebula) AuthorizeSign(_ context.Context, token string) ([]SignOption, e
p,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeNebula, p.Name, ""),
newProvisionerExtensionOption(TypeNebula, p.Name, "").WithControllerOptions(p.ctl),
profileLimitDuration{
def: p.ctl.Claimer.DefaultTLSCertDuration(),
notBefore: crt.Details.NotBefore,
@ -389,7 +389,7 @@ func (v nebulaSANsValidator) Valid(req *x509.CertificateRequest) error {
}
}
if len(req.URIs) > 0 {
if err := urisValidator(uris).Valid(req); err != nil {
if err := newURIsValidator(context.Background(), uris).Valid(req); err != nil {
return err
}
}

@ -351,7 +351,7 @@ func (o *OIDC) AuthorizeSign(_ context.Context, token string) ([]SignOption, err
o,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID),
newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID).WithControllerOptions(o.ctl),
profileDefaultDuration(o.ctl.Claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},

@ -2,13 +2,20 @@ package provisioner
import (
"context"
"crypto"
"crypto/rsa"
"crypto/subtle"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"time"
"github.com/pkg/errors"
"go.step.sm/crypto/kms"
kmsapi "go.step.sm/crypto/kms/apiv1"
"go.step.sm/crypto/kms/uri"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/webhook"
@ -29,9 +36,19 @@ type SCEP struct {
// intermediate in the GetCACerts response
IncludeRoot bool `json:"includeRoot,omitempty"`
// ExcludeIntermediate makes the provisioner skip the intermediate CA in the
// GetCACerts response
ExcludeIntermediate bool `json:"excludeIntermediate,omitempty"`
// MinimumPublicKeyLength is the minimum length for public keys in CSRs
MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"`
// TODO(hs): also support a separate signer configuration?
DecrypterCertificate []byte `json:"decrypterCertificate,omitempty"`
DecrypterKeyPEM []byte `json:"decrypterKeyPEM,omitempty"`
DecrypterKeyURI string `json:"decrypterKey,omitempty"`
DecrypterKeyPassword string `json:"decrypterKeyPassword,omitempty"`
// Numerical identifier for the ContentEncryptionAlgorithm as defined in github.com/mozilla-services/pkcs7
// at https://github.com/mozilla-services/pkcs7/blob/33d05740a3526e382af6395d3513e73d4e66d1cb/encrypt.go#L63
// Defaults to 0, being DES-CBC
@ -41,6 +58,12 @@ type SCEP struct {
ctl *Controller
encryptionAlgorithm int
challengeValidationController *challengeValidationController
notificationController *notificationController
keyManager kmsapi.KeyManager
decrypter crypto.Decrypter
decrypterCertificate *x509.Certificate
signer crypto.Signer
signerCertificate *x509.Certificate
}
// GetID returns the provisioner unique identifier.
@ -113,7 +136,8 @@ func newChallengeValidationController(client *http.Client, webhooks []*Webhook)
}
var (
ErrSCEPChallengeInvalid = errors.New("webhook server did not allow request")
ErrSCEPChallengeInvalid = errors.New("webhook server did not allow request")
ErrSCEPNotificationFailed = errors.New("scep notification failed")
)
// Validate executes zero or more configured webhooks to
@ -122,12 +146,15 @@ var (
// that case, the other webhooks will be skipped. If none of
// the webhooks indicates the value of the challenge was accepted,
// an error is returned.
func (c *challengeValidationController) Validate(ctx context.Context, challenge, transactionID string) error {
func (c *challengeValidationController) Validate(ctx context.Context, csr *x509.CertificateRequest, provisionerName, challenge, transactionID string) error {
for _, wh := range c.webhooks {
req := &webhook.RequestBody{
SCEPChallenge: challenge,
SCEPTransactionID: transactionID,
req, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))
if err != nil {
return fmt.Errorf("failed creating new webhook request: %w", err)
}
req.ProvisionerName = provisionerName
req.SCEPChallenge = challenge
req.SCEPTransactionID = transactionID
resp, err := wh.DoWithContext(ctx, c.client, req, nil) // TODO(hs): support templated URL? Requires some refactoring
if err != nil {
return fmt.Errorf("failed executing webhook request: %w", err)
@ -140,6 +167,63 @@ func (c *challengeValidationController) Validate(ctx context.Context, challenge,
return ErrSCEPChallengeInvalid
}
type notificationController struct {
client *http.Client
webhooks []*Webhook
}
// newNotificationController creates a new notificationController
// that performs SCEP notifications through webhooks.
func newNotificationController(client *http.Client, webhooks []*Webhook) *notificationController {
scepHooks := []*Webhook{}
for _, wh := range webhooks {
if wh.Kind != linkedca.Webhook_NOTIFYING.String() {
continue
}
if !isCertTypeOK(wh) {
continue
}
scepHooks = append(scepHooks, wh)
}
return &notificationController{
client: client,
webhooks: scepHooks,
}
}
func (c *notificationController) Success(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {
for _, wh := range c.webhooks {
req, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr), webhook.WithX509Certificate(nil, cert)) // TODO(hs): pass in the x509util.Certifiate too?
if err != nil {
return fmt.Errorf("failed creating new webhook request: %w", err)
}
req.X509Certificate.Raw = cert.Raw // adding the full certificate DER bytes
req.SCEPTransactionID = transactionID
if _, err = wh.DoWithContext(ctx, c.client, req, nil); err != nil {
return fmt.Errorf("failed executing webhook request: %w: %w", ErrSCEPNotificationFailed, err)
}
}
return nil
}
func (c *notificationController) Failure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {
for _, wh := range c.webhooks {
req, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))
if err != nil {
return fmt.Errorf("failed creating new webhook request: %w", err)
}
req.SCEPTransactionID = transactionID
req.SCEPErrorCode = errorCode
req.SCEPErrorDescription = errorDescription
if _, err = wh.DoWithContext(ctx, c.client, req, nil); err != nil {
return fmt.Errorf("failed executing webhook request: %w: %w", ErrSCEPNotificationFailed, err)
}
}
return nil
}
// isCertTypeOK returns whether or not the webhook can be used
// with the SCEP challenge validation webhook controller.
func isCertTypeOK(wh *Webhook) bool {
@ -162,21 +246,139 @@ func (s *SCEP) Init(config Config) (err error) {
if s.MinimumPublicKeyLength == 0 {
s.MinimumPublicKeyLength = 2048
}
if s.MinimumPublicKeyLength%8 != 0 {
return errors.Errorf("%d bits is not exactly divisible by 8", s.MinimumPublicKeyLength)
}
// Set the encryption algorithm to use
s.encryptionAlgorithm = s.EncryptionAlgorithmIdentifier // TODO(hs): we might want to upgrade the default security to AES-CBC?
if s.encryptionAlgorithm < 0 || s.encryptionAlgorithm > 4 {
return errors.New("only encryption algorithm identifiers from 0 to 4 are valid")
}
// Prepare the SCEP challenge validator
s.challengeValidationController = newChallengeValidationController(
config.WebhookClient,
s.GetOptions().GetWebhooks(),
)
// Prepare the SCEP notification controller
s.notificationController = newNotificationController(
config.WebhookClient,
s.GetOptions().GetWebhooks(),
)
// parse the decrypter key PEM contents if available
if decryptionKeyPEM := s.DecrypterKeyPEM; len(decryptionKeyPEM) > 0 {
// try reading the PEM for validation
block, rest := pem.Decode(decryptionKeyPEM)
if len(rest) > 0 {
return errors.New("failed parsing decrypter key: trailing data")
}
if block == nil {
return errors.New("failed parsing decrypter key: no PEM block found")
}
opts := kms.Options{
Type: kmsapi.SoftKMS,
}
if s.keyManager, err = kms.New(context.Background(), opts); err != nil {
return fmt.Errorf("failed initializing kms: %w", err)
}
kmsDecrypter, ok := s.keyManager.(kmsapi.Decrypter)
if !ok {
return fmt.Errorf("%q is not a kmsapi.Decrypter", opts.Type)
}
if s.decrypter, err = kmsDecrypter.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKeyPEM: decryptionKeyPEM,
Password: []byte(s.DecrypterKeyPassword),
PasswordPrompter: kmsapi.NonInteractivePasswordPrompter,
}); err != nil {
return fmt.Errorf("failed creating decrypter: %w", err)
}
if s.signer, err = s.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKeyPEM: decryptionKeyPEM, // TODO(hs): support distinct signer key in the future?
Password: []byte(s.DecrypterKeyPassword),
PasswordPrompter: kmsapi.NonInteractivePasswordPrompter,
}); err != nil {
return fmt.Errorf("failed creating signer: %w", err)
}
}
if decryptionKeyURI := s.DecrypterKeyURI; len(decryptionKeyURI) > 0 {
u, err := uri.Parse(s.DecrypterKeyURI)
if err != nil {
return fmt.Errorf("failed parsing decrypter key: %w", err)
}
var kmsType kmsapi.Type
switch {
case u.Scheme != "":
kmsType = kms.Type(u.Scheme)
default:
kmsType = kmsapi.SoftKMS
}
opts := kms.Options{
Type: kmsType,
URI: s.DecrypterKeyURI,
}
if s.keyManager, err = kms.New(context.Background(), opts); err != nil {
return fmt.Errorf("failed initializing kms: %w", err)
}
kmsDecrypter, ok := s.keyManager.(kmsapi.Decrypter)
if !ok {
return fmt.Errorf("%q is not a kmsapi.Decrypter", opts.Type)
}
if kmsType != "softkms" { // TODO(hs): this should likely become more transparent?
decryptionKeyURI = u.Opaque
}
if s.decrypter, err = kmsDecrypter.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: decryptionKeyURI,
Password: []byte(s.DecrypterKeyPassword),
PasswordPrompter: kmsapi.NonInteractivePasswordPrompter,
}); err != nil {
return fmt.Errorf("failed creating decrypter: %w", err)
}
if s.signer, err = s.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: decryptionKeyURI, // TODO(hs): support distinct signer key in the future?
Password: []byte(s.DecrypterKeyPassword),
PasswordPrompter: kmsapi.NonInteractivePasswordPrompter,
}); err != nil {
return fmt.Errorf("failed creating signer: %w", err)
}
}
// parse the decrypter certificate contents if available
if len(s.DecrypterCertificate) > 0 {
block, rest := pem.Decode(s.DecrypterCertificate)
if len(rest) > 0 {
return errors.New("failed parsing decrypter certificate: trailing data")
}
if block == nil {
return errors.New("failed parsing decrypter certificate: no PEM block found")
}
if s.decrypterCertificate, err = x509.ParseCertificate(block.Bytes); err != nil {
return fmt.Errorf("failed parsing decrypter certificate: %w", err)
}
// the decrypter certificate is also the signer certificate
s.signerCertificate = s.decrypterCertificate
}
// TODO(hs): alternatively, check if the KMS keyManager is a CertificateManager
// and load the certificate corresponding to the decryption key?
// Final validation for the decrypter.
if s.decrypter != nil {
decrypterPublicKey, ok := s.decrypter.Public().(*rsa.PublicKey)
if !ok {
return fmt.Errorf("only RSA keys are supported")
}
if s.decrypterCertificate == nil {
return fmt.Errorf("provisioner %q does not have a decrypter certificate set", s.Name)
}
if !decrypterPublicKey.Equal(s.decrypterCertificate.PublicKey) {
return errors.New("mismatch between decrypter certificate and decrypter public keys")
}
}
// TODO: add other, SCEP specific, options?
s.ctl, err = NewController(s, s.Claims, config, s.Options)
@ -190,7 +392,7 @@ func (s *SCEP) AuthorizeSign(context.Context, string) ([]SignOption, error) {
return []SignOption{
s,
// modifiers / withOptions
newProvisionerExtensionOption(TypeSCEP, s.Name, ""),
newProvisionerExtensionOption(TypeSCEP, s.Name, "").WithControllerOptions(s.ctl),
newForceCNOption(s.ForceCN),
profileDefaultDuration(s.ctl.Claimer.DefaultTLSCertDuration()),
// validators
@ -214,6 +416,15 @@ func (s *SCEP) ShouldIncludeRootInChain() bool {
return s.IncludeRoot
}
// ShouldIncludeIntermediateInChain indicates if the
// CA should include the intermediate CA certificate in the
// GetCACerts response. This is true by default, but can be
// overridden through configuration in case SCEP clients
// don't pick the right recipient.
func (s *SCEP) ShouldIncludeIntermediateInChain() bool {
return !s.ExcludeIntermediate
}
// GetContentEncryptionAlgorithm returns the numeric identifier
// for the pkcs7 package encryption algorithm to use.
func (s *SCEP) GetContentEncryptionAlgorithm() int {
@ -223,13 +434,13 @@ func (s *SCEP) GetContentEncryptionAlgorithm() int {
// ValidateChallenge validates the provided challenge. It starts by
// selecting the validation method to use, then performs validation
// according to that method.
func (s *SCEP) ValidateChallenge(ctx context.Context, challenge, transactionID string) error {
func (s *SCEP) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error {
if s.challengeValidationController == nil {
return fmt.Errorf("provisioner %q wasn't initialized", s.Name)
}
switch s.selectValidationMethod() {
case validationMethodWebhook:
return s.challengeValidationController.Validate(ctx, challenge, transactionID)
return s.challengeValidationController.Validate(ctx, csr, s.Name, challenge, transactionID)
default:
if subtle.ConstantTimeCompare([]byte(s.ChallengePassword), []byte(challenge)) == 0 {
return errors.New("invalid challenge password provided")
@ -238,6 +449,20 @@ func (s *SCEP) ValidateChallenge(ctx context.Context, challenge, transactionID s
}
}
func (s *SCEP) NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {
if s.notificationController == nil {
return fmt.Errorf("provisioner %q wasn't initialized", s.Name)
}
return s.notificationController.Success(ctx, csr, cert, transactionID)
}
func (s *SCEP) NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {
if s.notificationController == nil {
return fmt.Errorf("provisioner %q wasn't initialized", s.Name)
}
return s.notificationController.Failure(ctx, csr, transactionID, errorCode, errorDescription)
}
type validationMethod string
const (
@ -259,3 +484,20 @@ func (s *SCEP) selectValidationMethod() validationMethod {
}
return validationMethodNone
}
// GetDecrypter returns the provisioner specific decrypter,
// used to decrypt SCEP request messages sent by a SCEP client.
// The decrypter consists of a crypto.Decrypter (a private key)
// and a certificate for the public key corresponding to the
// private key.
func (s *SCEP) GetDecrypter() (*x509.Certificate, crypto.Decrypter) {
return s.decrypterCertificate, s.decrypter
}
// GetSigner returns the provisioner specific signer, used to
// sign SCEP response messages for the client. The signer consists
// of a crypto.Signer and a certificate for the public key
// corresponding to the private key.
func (s *SCEP) GetSigner() (*x509.Certificate, crypto.Signer) {
return s.signerCertificate, s.signer
}

@ -2,6 +2,7 @@ package provisioner
import (
"context"
"crypto/x509"
"encoding/json"
"errors"
"net/http"
@ -12,12 +13,19 @@ import (
"github.com/stretchr/testify/require"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/webhook"
)
func Test_challengeValidationController_Validate(t *testing.T) {
dummyCSR := &x509.CertificateRequest{
Raw: []byte{1},
}
type request struct {
Challenge string `json:"scepChallenge"`
TransactionID string `json:"scepTransactionID"`
ProvisionerName string `json:"provisionerName,omitempty"`
Request *webhook.X509CertificateRequest `json:"x509CertificateRequest,omitempty"`
Challenge string `json:"scepChallenge"`
TransactionID string `json:"scepTransactionID"`
}
type response struct {
Allow bool `json:"allow"`
@ -26,6 +34,7 @@ func Test_challengeValidationController_Validate(t *testing.T) {
req := &request{}
err := json.NewDecoder(r.Body).Decode(req)
require.NoError(t, err)
assert.Equal(t, "my-scep-provisioner", req.ProvisionerName)
assert.Equal(t, "not-allowed", req.Challenge)
assert.Equal(t, "transaction-1", req.TransactionID)
b, err := json.Marshal(response{Allow: false})
@ -37,8 +46,12 @@ func Test_challengeValidationController_Validate(t *testing.T) {
req := &request{}
err := json.NewDecoder(r.Body).Decode(req)
require.NoError(t, err)
assert.Equal(t, "my-scep-provisioner", req.ProvisionerName)
assert.Equal(t, "challenge", req.Challenge)
assert.Equal(t, "transaction-1", req.TransactionID)
if assert.NotNil(t, req.Request) {
assert.Equal(t, []byte{1}, req.Request.Raw)
}
b, err := json.Marshal(response{Allow: true})
require.NoError(t, err)
w.WriteHeader(200)
@ -49,8 +62,9 @@ func Test_challengeValidationController_Validate(t *testing.T) {
webhooks []*Webhook
}
type args struct {
challenge string
transactionID string
provisionerName string
challenge string
transactionID string
}
tests := []struct {
name string
@ -62,7 +76,7 @@ func Test_challengeValidationController_Validate(t *testing.T) {
{
name: "fail/no-webhook",
fields: fields{http.DefaultClient, nil},
args: args{"no-webhook", "transaction-1"},
args: args{"my-scep-provisioner", "no-webhook", "transaction-1"},
expErr: errors.New("webhook server did not allow request"),
},
{
@ -73,7 +87,7 @@ func Test_challengeValidationController_Validate(t *testing.T) {
CertType: linkedca.Webhook_SSH.String(),
},
}},
args: args{"wrong-cert-type", "transaction-1"},
args: args{"my-scep-provisioner", "wrong-cert-type", "transaction-1"},
expErr: errors.New("webhook server did not allow request"),
},
{
@ -89,8 +103,9 @@ func Test_challengeValidationController_Validate(t *testing.T) {
},
}},
args: args{
challenge: "wrong-secret-value",
transactionID: "transaction-1",
provisionerName: "my-scep-provisioner",
challenge: "wrong-secret-value",
transactionID: "transaction-1",
},
expErr: errors.New("failed executing webhook request: illegal base64 data at input byte 0"),
},
@ -107,8 +122,9 @@ func Test_challengeValidationController_Validate(t *testing.T) {
},
}},
args: args{
challenge: "not-allowed",
transactionID: "transaction-1",
provisionerName: "my-scep-provisioner",
challenge: "not-allowed",
transactionID: "transaction-1",
},
server: nokServer,
expErr: errors.New("webhook server did not allow request"),
@ -126,8 +142,9 @@ func Test_challengeValidationController_Validate(t *testing.T) {
},
}},
args: args{
challenge: "challenge",
transactionID: "transaction-1",
provisionerName: "my-scep-provisioner",
challenge: "challenge",
transactionID: "transaction-1",
},
server: okServer,
},
@ -141,7 +158,7 @@ func Test_challengeValidationController_Validate(t *testing.T) {
}
ctx := context.Background()
err := c.Validate(ctx, tt.args.challenge, tt.args.transactionID)
err := c.Validate(ctx, dummyCSR, tt.args.provisionerName, tt.args.challenge, tt.args.transactionID)
if tt.expErr != nil {
assert.EqualError(t, err, tt.expErr.Error())
@ -221,9 +238,14 @@ func Test_selectValidationMethod(t *testing.T) {
}
func TestSCEP_ValidateChallenge(t *testing.T) {
dummyCSR := &x509.CertificateRequest{
Raw: []byte{1},
}
type request struct {
Challenge string `json:"scepChallenge"`
TransactionID string `json:"scepTransactionID"`
ProvisionerName string `json:"provisionerName,omitempty"`
Request *webhook.X509CertificateRequest `json:"x509CertificateRequest,omitempty"`
Challenge string `json:"scepChallenge"`
TransactionID string `json:"scepTransactionID"`
}
type response struct {
Allow bool `json:"allow"`
@ -232,8 +254,12 @@ func TestSCEP_ValidateChallenge(t *testing.T) {
req := &request{}
err := json.NewDecoder(r.Body).Decode(req)
require.NoError(t, err)
assert.Equal(t, "SCEP", req.ProvisionerName)
assert.Equal(t, "webhook-challenge", req.Challenge)
assert.Equal(t, "webhook-transaction-1", req.TransactionID)
if assert.NotNil(t, req.Request) {
assert.Equal(t, []byte{1}, req.Request.Raw)
}
b, err := json.Marshal(response{Allow: true})
require.NoError(t, err)
w.WriteHeader(200)
@ -330,7 +356,7 @@ func TestSCEP_ValidateChallenge(t *testing.T) {
require.NoError(t, err)
ctx := context.Background()
err = tt.p.ValidateChallenge(ctx, tt.args.challenge, tt.args.transactionID)
err = tt.p.ValidateChallenge(ctx, dummyCSR, tt.args.challenge, tt.args.transactionID)
if tt.expErr != nil {
assert.EqualError(t, err, tt.expErr.Error())
return

@ -1,6 +1,7 @@
package provisioner
import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
@ -233,16 +234,28 @@ func (v emailAddressesValidator) Valid(req *x509.CertificateRequest) error {
}
// urisValidator validates the URI SANs of a certificate request.
type urisValidator []*url.URL
type urisValidator struct {
ctx context.Context
uris []*url.URL
}
func newURIsValidator(ctx context.Context, uris []*url.URL) *urisValidator {
return &urisValidator{ctx, uris}
}
// Valid checks that certificate request IP Addresses match those configured in
// the bootstrap (token) flow.
func (v urisValidator) Valid(req *x509.CertificateRequest) error {
// SignIdentityMethod does not need to validate URIs.
if MethodFromContext(v.ctx) == SignIdentityMethod {
return nil
}
if len(req.URIs) == 0 {
return nil
}
want := make(map[string]bool)
for _, u := range v {
for _, u := range v.uris {
want[u.String()] = true
}
got := make(map[string]bool)
@ -250,26 +263,33 @@ func (v urisValidator) Valid(req *x509.CertificateRequest) error {
got[u.String()] = true
}
if !reflect.DeepEqual(want, got) {
return errs.Forbidden("certificate request does not contain the valid URIs - got %v, want %v", req.URIs, v)
return errs.Forbidden("certificate request does not contain the valid URIs - got %v, want %v", req.URIs, v.uris)
}
return nil
}
// defaultsSANsValidator stores a set of SANs to eventually validate 1:1 against
// the SANs in an x509 certificate request.
type defaultSANsValidator []string
type defaultSANsValidator struct {
ctx context.Context
sans []string
}
func newDefaultSANsValidator(ctx context.Context, sans []string) *defaultSANsValidator {
return &defaultSANsValidator{ctx, sans}
}
// Valid verifies that the SANs stored in the validator match 1:1 with those
// requested in the x509 certificate request.
func (v defaultSANsValidator) Valid(req *x509.CertificateRequest) (err error) {
dnsNames, ips, emails, uris := x509util.SplitSANs(v)
dnsNames, ips, emails, uris := x509util.SplitSANs(v.sans)
if err = dnsNamesValidator(dnsNames).Valid(req); err != nil {
return
} else if err = emailAddressesValidator(emails).Valid(req); err != nil {
return
} else if err = ipAddressesValidator(ips).Valid(req); err != nil {
return
} else if err = urisValidator(uris).Valid(req); err != nil {
} else if err = newURIsValidator(v.ctx, uris).Valid(req); err != nil {
return
}
return
@ -430,6 +450,7 @@ func (o *forceCNOption) Modify(cert *x509.Certificate, _ SignOptions) error {
type provisionerExtensionOption struct {
Extension
Disabled bool
}
func newProvisionerExtensionOption(typ Type, name, credentialID string, keyValuePairs ...string) *provisionerExtensionOption {
@ -443,7 +464,19 @@ func newProvisionerExtensionOption(typ Type, name, credentialID string, keyValue
}
}
// WithControllerOptions updates the provisionerExtensionOption with options
// from the controller. Currently only the DisableSmallstepExtensions
// provisioner claim is used.
func (o *provisionerExtensionOption) WithControllerOptions(c *Controller) *provisionerExtensionOption {
o.Disabled = c.Claimer.IsDisableSmallstepExtensions()
return o
}
func (o *provisionerExtensionOption) Modify(cert *x509.Certificate, _ SignOptions) error {
if o.Disabled {
return nil
}
ext, err := o.ToExtension()
if err != nil {
return errs.NewError(http.StatusInternalServerError, err, "error creating certificate")

@ -1,6 +1,7 @@
package provisioner
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
@ -227,23 +228,26 @@ func Test_urisValidator_Valid(t *testing.T) {
fu, err := url.Parse("https://unexpected.com")
assert.FatalError(t, err)
signContext := NewContextWithMethod(context.Background(), SignMethod)
signIdentityContext := NewContextWithMethod(context.Background(), SignIdentityMethod)
type args struct {
req *x509.CertificateRequest
}
tests := []struct {
name string
v urisValidator
v *urisValidator
args args
wantErr bool
}{
{"ok0", []*url.URL{}, args{&x509.CertificateRequest{URIs: []*url.URL{}}}, false},
{"ok1", []*url.URL{u1}, args{&x509.CertificateRequest{URIs: []*url.URL{u1}}}, false},
{"ok2", []*url.URL{u1, u2}, args{&x509.CertificateRequest{URIs: []*url.URL{u2, u1}}}, false},
{"ok3", []*url.URL{u2, u1, u3}, args{&x509.CertificateRequest{URIs: []*url.URL{u3, u2, u1}}}, false},
{"ok3", []*url.URL{u2, u1, u3}, args{&x509.CertificateRequest{}}, false},
{"fail1", []*url.URL{u1}, args{&x509.CertificateRequest{URIs: []*url.URL{u2}}}, true},
{"fail2", []*url.URL{u1}, args{&x509.CertificateRequest{URIs: []*url.URL{u2, u1}}}, true},
{"fail3", []*url.URL{u1, u2}, args{&x509.CertificateRequest{URIs: []*url.URL{u1, fu}}}, true},
{"ok0", newURIsValidator(signContext, []*url.URL{}), args{&x509.CertificateRequest{URIs: []*url.URL{}}}, false},
{"ok1", newURIsValidator(signContext, []*url.URL{u1}), args{&x509.CertificateRequest{URIs: []*url.URL{u1}}}, false},
{"ok2", newURIsValidator(signContext, []*url.URL{u1, u2}), args{&x509.CertificateRequest{URIs: []*url.URL{u2, u1}}}, false},
{"ok3", newURIsValidator(signContext, []*url.URL{u2, u1, u3}), args{&x509.CertificateRequest{URIs: []*url.URL{u3, u2, u1}}}, false},
{"ok4", newURIsValidator(signIdentityContext, []*url.URL{u1, u2}), args{&x509.CertificateRequest{URIs: []*url.URL{u1, fu}}}, false},
{"fail1", newURIsValidator(signContext, []*url.URL{u1}), args{&x509.CertificateRequest{URIs: []*url.URL{u2}}}, true},
{"fail2", newURIsValidator(signContext, []*url.URL{u1}), args{&x509.CertificateRequest{URIs: []*url.URL{u2, u1}}}, true},
{"fail3", newURIsValidator(signContext, []*url.URL{u1, u2}), args{&x509.CertificateRequest{URIs: []*url.URL{u1, fu}}}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -257,13 +261,19 @@ func Test_urisValidator_Valid(t *testing.T) {
func Test_defaultSANsValidator_Valid(t *testing.T) {
type test struct {
csr *x509.CertificateRequest
ctx context.Context
expectedSANs []string
err error
}
signContext := NewContextWithMethod(context.Background(), SignMethod)
signIdentityContext := NewContextWithMethod(context.Background(), SignIdentityMethod)
tests := map[string]func() test{
"fail/dnsNamesValidator": func() test {
return test{
csr: &x509.CertificateRequest{DNSNames: []string{"foo", "bar"}},
ctx: signContext,
expectedSANs: []string{"foo"},
err: errors.New("certificate request does not contain the valid DNS names"),
}
@ -271,6 +281,7 @@ func Test_defaultSANsValidator_Valid(t *testing.T) {
"fail/emailAddressesValidator": func() test {
return test{
csr: &x509.CertificateRequest{EmailAddresses: []string{"max@fx.com", "mariano@fx.com"}},
ctx: signContext,
expectedSANs: []string{"dcow@fx.com"},
err: errors.New("certificate request does not contain the valid email addresses"),
}
@ -278,6 +289,7 @@ func Test_defaultSANsValidator_Valid(t *testing.T) {
"fail/ipAddressesValidator": func() test {
return test{
csr: &x509.CertificateRequest{IPAddresses: []net.IP{net.ParseIP("1.1.1.1"), net.ParseIP("127.0.0.1")}},
ctx: signContext,
expectedSANs: []string{"127.0.0.1"},
err: errors.New("certificate request does not contain the valid IP addresses"),
}
@ -289,16 +301,29 @@ func Test_defaultSANsValidator_Valid(t *testing.T) {
assert.FatalError(t, err)
return test{
csr: &x509.CertificateRequest{URIs: []*url.URL{u1, u2}},
ctx: signContext,
expectedSANs: []string{"urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959"},
err: errors.New("certificate request does not contain the valid URIs"),
}
},
"ok/urisBadValidator-SignIdentity": func() test {
u1, err := url.Parse("https://google.com")
assert.FatalError(t, err)
u2, err := url.Parse("urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959")
assert.FatalError(t, err)
return test{
csr: &x509.CertificateRequest{URIs: []*url.URL{u1, u2}},
ctx: signIdentityContext,
expectedSANs: []string{"urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959"},
}
},
"ok": func() test {
u1, err := url.Parse("https://google.com")
assert.FatalError(t, err)
u2, err := url.Parse("urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959")
assert.FatalError(t, err)
return test{
ctx: signContext,
csr: &x509.CertificateRequest{
DNSNames: []string{"foo", "bar"},
EmailAddresses: []string{"max@fx.com", "mariano@fx.com"},
@ -312,7 +337,7 @@ func Test_defaultSANsValidator_Valid(t *testing.T) {
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
if err := defaultSANsValidator(tt.expectedSANs).Valid(tt.csr); err != nil {
if err := newDefaultSANsValidator(tt.ctx, tt.expectedSANs).Valid(tt.csr); err != nil {
if assert.NotNil(t, tt.err, fmt.Sprintf("expected no error, but got err = %s", err.Error())) {
assert.True(t, strings.Contains(err.Error(), tt.err.Error()),
fmt.Sprintf("want err = %s, but got err = %s", tt.err.Error(), err.Error()))
@ -604,14 +629,24 @@ func Test_newProvisionerExtension_Option(t *testing.T) {
t.Fatal(err)
}
// Claims with smallstep extensions disabled.
claimer, err := NewClaimer(&Claims{
DisableSmallstepExtensions: &trueValue,
}, globalProvisionerClaims)
if err != nil {
t.Fatal(err)
}
type test struct {
cert *x509.Certificate
valid func(*x509.Certificate)
modifier *provisionerExtensionOption
cert *x509.Certificate
valid func(*x509.Certificate)
}
tests := map[string]func() test{
"ok/one-element": func() test {
return test{
cert: new(x509.Certificate),
modifier: newProvisionerExtensionOption(TypeJWK, "name", "credentialId", "key", "value"),
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
if assert.Len(t, 1, cert.ExtraExtensions) {
ext := cert.ExtraExtensions[0]
@ -625,7 +660,8 @@ func Test_newProvisionerExtension_Option(t *testing.T) {
},
"ok/replace": func() test {
return test{
cert: &x509.Certificate{ExtraExtensions: []pkix.Extension{{Id: StepOIDProvisioner, Critical: true}, {Id: []int{1, 2, 3}}}},
modifier: newProvisionerExtensionOption(TypeJWK, "name", "credentialId", "key", "value"),
cert: &x509.Certificate{ExtraExtensions: []pkix.Extension{{Id: StepOIDProvisioner, Critical: true}, {Id: []int{1, 2, 3}}}},
valid: func(cert *x509.Certificate) {
if assert.Len(t, 2, cert.ExtraExtensions) {
ext := cert.ExtraExtensions[0]
@ -636,11 +672,22 @@ func Test_newProvisionerExtension_Option(t *testing.T) {
},
}
},
"ok/disabled": func() test {
return test{
modifier: newProvisionerExtensionOption(TypeJWK, "name", "credentialId", "key", "value").WithControllerOptions(&Controller{
Claimer: claimer,
}),
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Len(t, 0, cert.ExtraExtensions)
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
assert.FatalError(t, newProvisionerExtensionOption(TypeJWK, "name", "credentialId", "key", "value").Modify(tt.cert, SignOptions{}))
assert.FatalError(t, tt.modifier.Modify(tt.cert, SignOptions{}))
tt.valid(tt.cert)
})
}

@ -24,22 +24,24 @@ import (
)
var (
defaultDisableRenewal = false
defaultAllowRenewalAfterExpiry = false
defaultEnableSSHCA = true
globalProvisionerClaims = Claims{
MinTLSDur: &Duration{5 * time.Minute},
MaxTLSDur: &Duration{24 * time.Hour},
DefaultTLSDur: &Duration{24 * time.Hour},
MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &Duration{Duration: 16 * time.Hour},
MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &defaultEnableSSHCA,
DisableRenewal: &defaultDisableRenewal,
AllowRenewalAfterExpiry: &defaultAllowRenewalAfterExpiry,
defaultDisableRenewal = false
defaultAllowRenewalAfterExpiry = false
defaultEnableSSHCA = true
defaultDisableSmallstepExtensions = false
globalProvisionerClaims = Claims{
MinTLSDur: &Duration{5 * time.Minute},
MaxTLSDur: &Duration{24 * time.Hour},
DefaultTLSDur: &Duration{24 * time.Hour},
MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &Duration{Duration: 16 * time.Hour},
MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &defaultEnableSSHCA,
DisableRenewal: &defaultDisableRenewal,
AllowRenewalAfterExpiry: &defaultAllowRenewalAfterExpiry,
DisableSmallstepExtensions: &defaultDisableSmallstepExtensions,
}
testAudiences = Audiences{
Sign: []string{"https://ca.smallstep.com/1.0/sign", "https://ca.smallstep.com/sign"},

@ -173,7 +173,9 @@ retry:
if err != nil {
return nil, err
}
sig := hmac.New(sha256.New, secret).Sum(reqBytes)
h := hmac.New(sha256.New, secret)
h.Write(reqBytes)
sig := h.Sum(nil)
req.Header.Set("X-Smallstep-Signature", hex.EncodeToString(sig))
req.Header.Set("X-Smallstep-Webhook-ID", w.ID)

@ -482,7 +482,9 @@ func TestWebhook_Do(t *testing.T) {
secret, err := base64.StdEncoding.DecodeString(tc.webhook.Secret)
assert.FatalError(t, err)
mac := hmac.New(sha256.New, secret).Sum(body)
h := hmac.New(sha256.New, secret)
h.Write(body)
mac := h.Sum(nil)
assert.True(t, hmac.Equal(sig, mac))
switch {

@ -194,7 +194,7 @@ func (p *X5C) AuthorizeRevoke(_ context.Context, token string) error {
}
// AuthorizeSign validates the given token.
func (p *X5C) AuthorizeSign(_ context.Context, token string) ([]SignOption, error) {
func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := p.authorizeToken(token, p.ctl.Audiences.Sign)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "x5c.AuthorizeSign")
@ -237,14 +237,14 @@ func (p *X5C) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
self,
templateOptions,
// modifiers / withOptions
newProvisionerExtensionOption(TypeX5C, p.Name, ""),
newProvisionerExtensionOption(TypeX5C, p.Name, "").WithControllerOptions(p.ctl),
profileLimitDuration{
p.ctl.Claimer.DefaultTLSCertDuration(),
x5cLeaf.NotBefore, x5cLeaf.NotAfter,
},
// validators
commonNameValidator(claims.Subject),
defaultSANsValidator(claims.SANs),
newDefaultSANsValidator(ctx, claims.SANs),
defaultPublicKeyValidator{},
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),

@ -460,7 +460,8 @@ func TestX5C_AuthorizeSign(t *testing.T) {
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil {
ctx := NewContextWithMethod(context.Background(), SignIdentityMethod)
if opts, err := tc.p.AuthorizeSign(ctx, tc.token); err != nil {
if assert.NotNil(t, tc.err) {
var sc render.StatusCodedError
if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") {
@ -489,8 +490,9 @@ func TestX5C_AuthorizeSign(t *testing.T) {
case commonNameValidator:
assert.Equals(t, string(v), "foo")
case defaultPublicKeyValidator:
case defaultSANsValidator:
assert.Equals(t, []string(v), tc.sans)
case *defaultSANsValidator:
assert.Equals(t, v.sans, tc.sans)
assert.Equals(t, MethodFromContext(v.ctx), SignIdentityMethod)
case *validityValidator:
assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())

@ -235,7 +235,7 @@ func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisi
}
if err := certProv.Init(provisionerConfig); err != nil {
return admin.WrapError(admin.ErrorBadRequestType, err, "error validating configuration for provisioner %s", prov.Name)
return admin.WrapError(admin.ErrorBadRequestType, err, "error validating configuration for provisioner %q", prov.Name)
}
// Store to database -- this will set the ID.
@ -646,8 +646,9 @@ func claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) {
}
pc := &provisioner.Claims{
DisableRenewal: &c.DisableRenewal,
AllowRenewalAfterExpiry: &c.AllowRenewalAfterExpiry,
DisableRenewal: &c.DisableRenewal,
AllowRenewalAfterExpiry: &c.AllowRenewalAfterExpiry,
DisableSmallstepExtensions: &c.DisableSmallstepExtensions,
}
var err error
@ -686,6 +687,7 @@ func claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims {
disableRenewal := config.DefaultDisableRenewal
allowRenewalAfterExpiry := config.DefaultAllowRenewalAfterExpiry
disableSmallstepExtensions := config.DefaultDisableSmallstepExtensions
if c.DisableRenewal != nil {
disableRenewal = *c.DisableRenewal
@ -693,10 +695,14 @@ func claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims {
if c.AllowRenewalAfterExpiry != nil {
allowRenewalAfterExpiry = *c.AllowRenewalAfterExpiry
}
if c.DisableSmallstepExtensions != nil {
disableSmallstepExtensions = *c.DisableSmallstepExtensions
}
lc := &linkedca.Claims{
DisableRenewal: disableRenewal,
AllowRenewalAfterExpiry: allowRenewalAfterExpiry,
DisableRenewal: disableRenewal,
AllowRenewalAfterExpiry: allowRenewalAfterExpiry,
DisableSmallstepExtensions: disableSmallstepExtensions,
}
if c.DefaultTLSDur != nil || c.MinTLSDur != nil || c.MaxTLSDur != nil {
@ -748,13 +754,17 @@ func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *
}
if p.X509.Template != "" {
x509Template.Template = []byte(p.SSH.Template)
x509Template.Template = []byte(p.X509.Template)
} else if p.X509.TemplateFile != "" {
filename := step.Abs(p.X509.TemplateFile)
if x509Template.Template, err = os.ReadFile(filename); err != nil {
return nil, nil, nil, errors.Wrap(err, "error reading x509 template")
}
}
if p.X509.TemplateData != nil {
x509Template.Data = p.X509.TemplateData
}
}
if p.SSH != nil && p.SSH.HasTemplate() {
@ -771,6 +781,10 @@ func provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *
return nil, nil, nil, errors.Wrap(err, "error reading ssh template")
}
}
if p.SSH.TemplateData != nil {
sshTemplate.Data = p.SSH.TemplateData
}
}
var webhooks []*linkedca.Webhook
@ -960,7 +974,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
}, nil
case *linkedca.ProvisionerDetails_SCEP:
cfg := d.SCEP
return &provisioner.SCEP{
s := &provisioner.SCEP{
ID: p.Id,
Type: p.Type.String(),
Name: p.Name,
@ -968,11 +982,19 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
ChallengePassword: cfg.Challenge,
Capabilities: cfg.Capabilities,
IncludeRoot: cfg.IncludeRoot,
ExcludeIntermediate: cfg.ExcludeIntermediate,
MinimumPublicKeyLength: int(cfg.MinimumPublicKeyLength),
EncryptionAlgorithmIdentifier: int(cfg.EncryptionAlgorithmIdentifier),
Claims: claims,
Options: options,
}, nil
}
if decrypter := cfg.GetDecrypter(); decrypter != nil {
s.DecrypterCertificate = decrypter.Certificate
s.DecrypterKeyPEM = decrypter.Key
s.DecrypterKeyURI = decrypter.KeyUri
s.DecrypterKeyPassword = string(decrypter.KeyPassword)
}
return s, nil
case *linkedca.ProvisionerDetails_Nebula:
var roots []byte
for i, root := range d.Nebula.GetRoots() {
@ -1227,7 +1249,14 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
Capabilities: p.Capabilities,
MinimumPublicKeyLength: int32(p.MinimumPublicKeyLength),
IncludeRoot: p.IncludeRoot,
ExcludeIntermediate: p.ExcludeIntermediate,
EncryptionAlgorithmIdentifier: int32(p.EncryptionAlgorithmIdentifier),
Decrypter: &linkedca.SCEPDecrypter{
Certificate: p.DecrypterCertificate,
Key: p.DecrypterKeyPEM,
KeyUri: p.DecrypterKeyURI,
KeyPassword: []byte(p.DecrypterKeyPassword),
},
},
},
},

@ -15,8 +15,8 @@ import (
"sync"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/pkg/errors"
"github.com/smallstep/certificates/acme"
acmeAPI "github.com/smallstep/certificates/acme/api"
@ -250,19 +250,14 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
var scepAuthority *scep.Authority
if ca.shouldServeSCEPEndpoints() {
scepPrefix := "scep"
scepAuthority, err = scep.New(auth, scep.AuthorityOptions{
Service: auth.GetSCEPService(),
DNS: dns,
Prefix: scepPrefix,
})
if err != nil {
return nil, errors.Wrap(err, "error creating SCEP authority")
}
// get the SCEP authority configuration. Validation is
// performed within the authority instantiation process.
scepAuthority = auth.GetSCEP()
// According to the RFC (https://tools.ietf.org/html/rfc8894#section-7.10),
// SCEP operations are performed using HTTP, so that's why the API is mounted
// to the insecure mux.
scepPrefix := "scep"
insecureMux.Route("/"+scepPrefix, func(r chi.Router) {
scepAPI.Route(r)
})
@ -584,10 +579,10 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, *tls.Config,
// shouldServeSCEPEndpoints returns if the CA should be
// configured with endpoints for SCEP. This is assumed to be
// true if a SCEPService exists, which is true in case a
// SCEP provisioner was configured.
// true if a SCEPService exists, which is true in case at
// least one SCEP provisioner was configured.
func (ca *CA) shouldServeSCEPEndpoints() bool {
return ca.auth.GetSCEPService() != nil
return ca.auth.GetSCEP() != nil
}
//nolint:unused // useful for debugging

@ -59,9 +59,13 @@ func generateOTT(subject string) string {
return raw
}
func startTestServer(tlsConfig *tls.Config, handler http.Handler) *httptest.Server {
func startTestServer(baseContext context.Context, tlsConfig *tls.Config, handler http.Handler) *httptest.Server {
srv := httptest.NewUnstartedServer(handler)
srv.TLS = tlsConfig
// Base context MUST be set before the start of the server
srv.Config.BaseContext = func(l net.Listener) context.Context {
return baseContext
}
srv.StartTLS()
// Force the use of GetCertificate on IPs
srv.TLS.Certificates = nil
@ -78,11 +82,8 @@ func startCATestServer() *httptest.Server {
panic(err)
}
// Use a httptest.Server instead
srv := startTestServer(ca.srv.TLSConfig, ca.srv.Handler)
baseContext := buildContext(ca.auth, nil, nil, nil)
srv.Config.BaseContext = func(net.Listener) context.Context {
return baseContext
}
srv := startTestServer(baseContext, ca.srv.TLSConfig, ca.srv.Handler)
return srv
}
@ -153,7 +154,7 @@ func TestClient_GetServerTLSConfig_http(t *testing.T) {
if err != nil {
t.Fatalf("Client.GetServerTLSConfig() error = %v", err)
}
srvMTLS := startTestServer(tlsConfig, serverHandler(t, clientDomain))
srvMTLS := startTestServer(context.Background(), tlsConfig, serverHandler(t, clientDomain))
defer srvMTLS.Close()
// Create TLS server
@ -163,7 +164,7 @@ func TestClient_GetServerTLSConfig_http(t *testing.T) {
if err != nil {
t.Fatalf("Client.GetServerTLSConfig() error = %v", err)
}
srvTLS := startTestServer(tlsConfig, serverHandler(t, clientDomain))
srvTLS := startTestServer(context.Background(), tlsConfig, serverHandler(t, clientDomain))
defer srvTLS.Close()
tests := []struct {
@ -258,7 +259,7 @@ func TestClient_GetServerTLSConfig_renew(t *testing.T) {
if err != nil {
t.Fatalf("Client.GetServerTLSConfig() error = %v", err)
}
srvMTLS := startTestServer(tlsConfig, serverHandler(t, clientDomain))
srvMTLS := startTestServer(context.Background(), tlsConfig, serverHandler(t, clientDomain))
defer srvMTLS.Close()
// Start TLS server
@ -268,7 +269,7 @@ func TestClient_GetServerTLSConfig_renew(t *testing.T) {
if err != nil {
t.Fatalf("Client.GetServerTLSConfig() error = %v", err)
}
srvTLS := startTestServer(tlsConfig, serverHandler(t, clientDomain))
srvTLS := startTestServer(context.Background(), tlsConfig, serverHandler(t, clientDomain))
defer srvTLS.Close()
// Transport

@ -71,6 +71,8 @@ func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1
switch {
case req.CSR == nil:
return nil, errors.New("createCertificateRequest `csr` cannot be nil")
case req.Template == nil:
return nil, errors.New("createCertificateRequest `template` cannot be nil")
case req.Lifetime == 0:
return nil, errors.New("createCertificateRequest `lifetime` cannot be 0")
}
@ -87,7 +89,7 @@ func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1
info.ProvisionerName = p.Name
}
cert, chain, err := s.createCertificate(req.CSR, req.Lifetime, info)
cert, chain, err := s.createCertificate(req.CSR, req.Template, req.Lifetime, info)
if err != nil {
return nil, err
}
@ -167,18 +169,18 @@ func (s *StepCAS) GetCertificateAuthority(*apiv1.GetCertificateAuthorityRequest)
}, nil
}
func (s *StepCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.Duration, raInfo *raInfo) (*x509.Certificate, []*x509.Certificate, error) {
sans := make([]string, 0, len(cr.DNSNames)+len(cr.EmailAddresses)+len(cr.IPAddresses)+len(cr.URIs))
sans = append(sans, cr.DNSNames...)
sans = append(sans, cr.EmailAddresses...)
for _, ip := range cr.IPAddresses {
func (s *StepCAS) createCertificate(cr *x509.CertificateRequest, template *x509.Certificate, lifetime time.Duration, raInfo *raInfo) (*x509.Certificate, []*x509.Certificate, error) {
sans := make([]string, 0, len(template.DNSNames)+len(template.EmailAddresses)+len(template.IPAddresses)+len(template.URIs))
sans = append(sans, template.DNSNames...)
sans = append(sans, template.EmailAddresses...)
for _, ip := range template.IPAddresses {
sans = append(sans, ip.String())
}
for _, u := range cr.URIs {
for _, u := range template.URIs {
sans = append(sans, u.String())
}
commonName := cr.Subject.CommonName
commonName := template.Subject.CommonName
if commonName == "" && len(sans) > 0 {
commonName = sans[0]
}

@ -23,6 +23,7 @@ import (
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/cas/apiv1"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/randutil"
@ -631,6 +632,17 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
jwkEnc := testJWKIssuer(t, caURL, testPassword)
x5cBad := testX5CIssuer(t, caURL, "bad-password")
testTemplate := &x509.Certificate{
Subject: testCR.Subject,
DNSNames: testCR.DNSNames,
EmailAddresses: testCR.EmailAddresses,
IPAddresses: testCR.IPAddresses,
URIs: testCR.URIs,
}
testOtherCR, err := x509util.CreateCertificateRequest("Test Certificate", []string{"test.example.com"}, testKey)
require.NoError(t, err)
type fields struct {
iss stepIssuer
client *ca.Client
@ -648,6 +660,15 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
}{
{"ok", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: testCR,
Template: testTemplate,
Lifetime: time.Hour,
}}, &apiv1.CreateCertificateResponse{
Certificate: testCrt,
CertificateChain: []*x509.Certificate{testIssCrt},
}, false},
{"ok with different CSR", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: testOtherCR,
Template: testTemplate,
Lifetime: time.Hour,
}}, &apiv1.CreateCertificateResponse{
Certificate: testCrt,
@ -655,6 +676,7 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
}, false},
{"ok with password", fields{x5cEnc, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: testCR,
Template: testTemplate,
Lifetime: time.Hour,
}}, &apiv1.CreateCertificateResponse{
Certificate: testCrt,
@ -662,6 +684,7 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
}, false},
{"ok jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: testCR,
Template: testTemplate,
Lifetime: time.Hour,
}}, &apiv1.CreateCertificateResponse{
Certificate: testCrt,
@ -669,6 +692,7 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
}, false},
{"ok jwk with password", fields{jwkEnc, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: testCR,
Template: testTemplate,
Lifetime: time.Hour,
}}, &apiv1.CreateCertificateResponse{
Certificate: testCrt,
@ -676,6 +700,7 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
}, false},
{"ok with provisioner", fields{jwk, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: testCR,
Template: testTemplate,
Lifetime: time.Hour,
Provisioner: &apiv1.ProvisionerInfo{ID: "provisioner-id", Type: "ACME"},
}}, &apiv1.CreateCertificateResponse{
@ -684,6 +709,7 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
}, false},
{"ok with server cert", fields{jwk, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: testCR,
Template: testTemplate,
Lifetime: time.Hour,
IsCAServerCert: true,
}}, &apiv1.CreateCertificateResponse{
@ -692,6 +718,12 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
}, false},
{"fail CSR", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: nil,
Template: testTemplate,
Lifetime: time.Hour,
}}, nil, true},
{"fail Template", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: testCR,
Template: nil,
Lifetime: time.Hour,
}}, nil, true},
{"fail lifetime", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{

@ -5,7 +5,6 @@ import (
"fmt"
"html"
"log"
"math/rand"
"net/http"
"os"
"reflect"
@ -52,7 +51,6 @@ var (
func init() {
step.Set("Smallstep CA", Version, BuildTime)
authority.GlobalVersion.Version = Version
rand.Seed(time.Now().UnixNano())
// Add support for asking passwords
pemutil.PromptPassword = func(msg string) ([]byte, error) {
return ui.PromptPassword(msg)

@ -119,7 +119,7 @@ func New(c *Config) (AuthDB, error) {
db, err := nosql.New(c.Type, c.DataSource, opts...)
if err != nil {
return nil, errors.Wrapf(err, "Error opening database of Type %s with source %s", c.Type, c.DataSource)
return nil, errors.Wrapf(err, "Error opening database of Type %s", c.Type)
}
tables := [][]byte{

@ -1,4 +1,4 @@
FROM golang AS builder
FROM golang:bullseye AS builder
WORKDIR /src
COPY . .

111
go.mod

@ -1,65 +1,65 @@
module github.com/smallstep/certificates
go 1.19
go 1.20
require (
cloud.google.com/go/longrunning v0.5.1
cloud.google.com/go/security v1.15.1
cloud.google.com/go/longrunning v0.5.4
cloud.google.com/go/security v1.15.4
github.com/Masterminds/sprig/v3 v3.2.3
github.com/dgraph-io/badger v1.6.2
github.com/dgraph-io/badger/v2 v2.2007.4
github.com/fxamacker/cbor/v2 v2.4.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/fxamacker/cbor/v2 v2.5.0
github.com/go-chi/chi/v5 v5.0.10
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.9
github.com/google/go-tpm v0.3.3
github.com/google/uuid v1.3.0
github.com/google/go-cmp v0.6.0
github.com/google/go-tpm v0.9.0
github.com/google/uuid v1.4.0
github.com/googleapis/gax-go/v2 v2.12.0
github.com/hashicorp/vault/api v1.9.2
github.com/hashicorp/vault/api/auth/approle v0.4.1
github.com/hashicorp/vault/api/auth/kubernetes v0.4.1
github.com/micromdm/scep/v2 v2.1.0
github.com/newrelic/go-agent/v3 v3.23.1
github.com/hashicorp/vault/api v1.10.0
github.com/hashicorp/vault/api/auth/approle v0.5.0
github.com/hashicorp/vault/api/auth/kubernetes v0.5.0
github.com/newrelic/go-agent/v3 v3.28.0
github.com/pkg/errors v0.9.1
github.com/rs/xid v1.5.0
github.com/sirupsen/logrus v1.9.3
github.com/slackhq/nebula v1.6.1
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262
github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738
github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2
github.com/smallstep/nosql v0.6.0
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d
github.com/stretchr/testify v1.8.4
github.com/urfave/cli v1.22.14
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
go.step.sm/cli-utils v0.8.0
go.step.sm/crypto v0.33.0
go.step.sm/linkedca v0.20.0
golang.org/x/crypto v0.11.0
go.step.sm/crypto v0.37.0
go.step.sm/linkedca v0.20.1
golang.org/x/crypto v0.15.0
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0
golang.org/x/net v0.12.0
google.golang.org/api v0.132.0
google.golang.org/grpc v1.56.2
golang.org/x/net v0.18.0
google.golang.org/api v0.151.0
google.golang.org/grpc v1.59.0
google.golang.org/protobuf v1.31.0
gopkg.in/square/go-jose.v2 v2.6.0
)
require (
cloud.google.com/go v0.110.4 // indirect
cloud.google.com/go/compute v1.20.1 // indirect
cloud.google.com/go v0.110.8 // indirect
cloud.google.com/go/compute v1.23.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.0 // indirect
cloud.google.com/go/kms v1.15.0 // indirect
cloud.google.com/go/iam v1.1.3 // indirect
cloud.google.com/go/kms v1.15.5 // indirect
filippo.io/edwards25519 v1.0.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
github.com/aws/aws-sdk-go v1.44.307 // indirect
github.com/aws/aws-sdk-go v1.47.10 // indirect
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
@ -68,23 +68,24 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-piv/piv-go v1.11.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/glog v1.1.0 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
github.com/golang/glog v1.1.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/certificate-transparency-go v1.1.4 // indirect
github.com/google/go-tpm-tools v0.3.12 // indirect
github.com/google/certificate-transparency-go v1.1.6 // indirect
github.com/google/go-tpm-tools v0.4.2 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
@ -105,10 +106,10 @@ require (
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.15.11 // indirect
github.com/klauspost/compress v1.16.3 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
@ -129,22 +130,14 @@ require (
github.com/x448/float16 v0.8.4 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.1.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
// replace github.com/smallstep/nosql => ../nosql
// replace go.step.sm/crypto => ../crypto
// replace go.step.sm/cli-utils => ../cli-utils
// replace go.step.sm/linkedca => ../linkedca
// use github.com/smallstep/pkcs7 fork with patches applied
replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948

1278
go.sum

File diff suppressed because it is too large Load Diff

@ -58,7 +58,7 @@ func TestHealthOKHandling(t *testing.T) {
next: tt.handler,
}
r := httptest.NewRequest("GET", tt.path, nil)
r := httptest.NewRequest("GET", tt.path, http.NoBody)
w := httptest.NewRecorder()
l.ServeHTTP(w, r)
@ -132,7 +132,7 @@ func TestHandlingRegardlessOfOptions(t *testing.T) {
next: tt.handler,
}
r := httptest.NewRequest("GET", tt.path, nil)
r := httptest.NewRequest("GET", tt.path, http.NoBody)
w := httptest.NewRecorder()
l.ServeHTTP(w, r)

@ -3,6 +3,7 @@ package logging
import (
"encoding/json"
"net/http"
"os"
"strings"
"github.com/pkg/errors"
@ -38,6 +39,13 @@ func New(name string, raw json.RawMessage) (*Logger, error) {
var formatter logrus.Formatter
switch strings.ToLower(config.Format) {
case "", "text":
_, noColor := os.LookupEnv("NO_COLOR")
// With EnvironmentOverrideColors set, logrus looks at CLICOLOR and
// CLICOLOR_FORCE
formatter = &logrus.TextFormatter{
DisableColors: noColor,
EnvironmentOverrideColors: true,
}
case "json":
formatter = new(logrus.JSONFormatter)
case "common":

@ -1,6 +1,7 @@
package pki
import (
"fmt"
"io"
"text/template"
@ -49,21 +50,42 @@ func (p *PKI) WriteHelmTemplate(w io.Writer) error {
// to what's in p.GenerateConfig(), but that codepath isn't taken when
// writing the Helm template. The default JWK provisioner is added earlier in
// the process and that's part of the provisioners above.
//
// To prevent name clashes for the default ACME provisioner, we append "-1" to
// the name if it already exists. See https://github.com/smallstep/cli/issues/1018
// for the reason.
//
// TODO(hs): consider refactoring the initialization, so that this becomes
// easier to reason about and maintain.
if p.options.enableACME {
acmeProvisionerName := "acme"
for _, prov := range provisioners {
if prov.GetName() == acmeProvisionerName {
acmeProvisionerName = fmt.Sprintf("%s-1", acmeProvisionerName)
break
}
}
provisioners = append(provisioners, &provisioner.ACME{
Type: "ACME",
Name: "acme",
Name: acmeProvisionerName,
})
}
// Add default SSHPOP provisioner if enabled. Similar to the above, this is
// the same as what happens in p.GenerateConfig().
// the same as what happens in p.GenerateConfig(). To prevent name clashes for the
// default SSHPOP provisioner, we append "-1" to it if it already exists. See
// https://github.com/smallstep/cli/issues/1018 for the reason.
if p.options.enableSSH {
sshProvisionerName := "sshpop"
for _, prov := range provisioners {
if prov.GetName() == sshProvisionerName {
sshProvisionerName = fmt.Sprintf("%s-1", sshProvisionerName)
break
}
}
provisioners = append(provisioners, &provisioner.SSHPOP{
Type: "SSHPOP",
Name: "sshpop",
Name: sshProvisionerName,
Claims: &provisioner.Claims{
EnableSSHCA: &p.options.enableSSH,
},

@ -85,6 +85,13 @@ func TestPKI_WriteHelmTemplate(t *testing.T) {
wantErr: false,
}
},
"ok/with-acme-and-duplicate-provisioner-name": func(t *testing.T) test {
return test{
pki: preparePKI(t, WithProvisioner("acme"), WithACME()),
testFile: "testdata/helm/with-acme-and-duplicate-provisioner-name.yml",
wantErr: false,
}
},
"ok/with-admin": func(t *testing.T) test {
return test{
pki: preparePKI(t, WithAdmin()),
@ -99,6 +106,13 @@ func TestPKI_WriteHelmTemplate(t *testing.T) {
wantErr: false,
}
},
"ok/with-ssh-and-duplicate-provisioner-name": func(t *testing.T) test {
return test{
pki: preparePKI(t, WithProvisioner("sshpop"), WithSSH()),
testFile: "testdata/helm/with-ssh-and-duplicate-provisioner-name.yml",
wantErr: false,
}
},
"ok/with-ssh-and-acme": func(t *testing.T) test {
return test{
pki: preparePKI(t, WithSSH(), WithACME()),

@ -319,7 +319,10 @@ type PKI struct {
func New(o apiv1.Options, opts ...Option) (*PKI, error) {
// TODO(hs): invoking `New` with a context active will use values from
// that CA context while generating the context. Thay may or may not
// be fully expected and/or what we want. Check that.
// be fully expected and/or what we want. This specific behavior was
// changed after not relying on the `init` inside of `step`, resulting in
// the default context being active if `step.Init` isn't called explicitly.
// It can still result in surprising results, though.
currentCtx := step.Contexts().GetCurrent()
caService, err := cas.New(context.Background(), o)
if err != nil {
@ -330,7 +333,7 @@ func New(o apiv1.Options, opts ...Option) (*PKI, error) {
if o.IsCreator {
creator, ok := caService.(apiv1.CertificateAuthorityCreator)
if !ok {
return nil, errors.Errorf("cas type '%s' does not implements CertificateAuthorityCreator", o.Type)
return nil, errors.Errorf("cas type %q does not implement CertificateAuthorityCreator", o.Type)
}
caCreator = creator
}
@ -850,9 +853,16 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) {
// Add default ACME provisioner if enabled
if p.options.enableACME {
// To prevent name clashes for the default ACME provisioner, we append "-1" to
// the name if it already exists. See https://github.com/smallstep/cli/issues/1018
// for the reason.
acmeProvisionerName := "acme"
if p.options.provisioner == acmeProvisionerName {
acmeProvisionerName = fmt.Sprintf("%s-1", acmeProvisionerName)
}
provisioners = append(provisioners, &provisioner.ACME{
Type: "ACME",
Name: "acme",
Name: acmeProvisionerName,
})
}
@ -867,10 +877,16 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) {
EnableSSHCA: &enableSSHCA,
}
// Add default SSHPOP provisioner
// Add default SSHPOP provisioner. To prevent name clashes for the default
// SSHPOP provisioner, we append "-1" to the name if it already exists.
// See https://github.com/smallstep/cli/issues/1018 for the reason.
sshProvisionerName := "sshpop"
if p.options.provisioner == sshProvisionerName {
sshProvisionerName = fmt.Sprintf("%s-1", sshProvisionerName)
}
provisioners = append(provisioners, &provisioner.SSHPOP{
Type: "SSHPOP",
Name: "sshpop",
Name: sshProvisionerName,
Claims: &provisioner.Claims{
EnableSSHCA: &enableSSHCA,
},
@ -910,10 +926,13 @@ func (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) {
if err != nil {
return nil, err
}
defer _db.Shutdown() // free DB resources; unlock BadgerDB file
adminDB, err := admindb.New(_db.(nosql.DB), admin.DefaultAuthorityID)
if err != nil {
return nil, err
}
// Add all the provisioners to the db.
var adminID string
for i, p := range provisioners {

@ -0,0 +1,313 @@
package pki
import (
"context"
"path/filepath"
"testing"
"github.com/smallstep/certificates/authority/admin"
admindb "github.com/smallstep/certificates/authority/admin/db/nosql"
authconfig "github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/nosql"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/cli-utils/step"
)
func withDBDataSource(t *testing.T, dataSource string) func(c *authconfig.Config) error {
return func(c *authconfig.Config) error {
if c == nil || c.DB == nil {
require.Fail(t, "withDBDataSource prerequisites not met")
}
c.DB.DataSource = dataSource
return nil
}
}
func TestPKI_GenerateConfig(t *testing.T) {
var preparePKI = func(t *testing.T, opts ...Option) *PKI {
o := apiv1.Options{
Type: "softcas",
IsCreator: true,
}
// TODO(hs): invoking `New` doesn't perform all operations that are executed
// when `ca init` is executed. Ideally this logic should be handled in one
// place and probably inside of the PKI initialization. For testing purposes
// the missing operations are faked by `setKeyPair`.
p, err := New(o, opts...)
require.NoError(t, err)
// setKeyPair sets a predefined JWK and a default JWK provisioner. This is one
// of the things performed in the `ca init` code that's not part of `New`, but
// performed after that in p.GenerateKeyPairs`. We're currently using the same
// JWK for every test to keep test variance small: we're not testing JWK generation
// here after all. It's a bit dangerous to redefine the function here, but it's
// the simplest way to make this fully testable without refactoring the init now.
// The password for the predefined encrypted key is \x01\x03\x03\x07.
setKeyPair(t, p)
return p
}
type args struct {
opt []ConfigOption
}
type test struct {
pki *PKI
args args
want *authconfig.Config
wantErr bool
}
var tests = map[string]func(t *testing.T) test{
"ok/simple": func(t *testing.T) test {
pki := preparePKI(t)
pki.options.deploymentType = StandaloneDeployment
pki.options.provisioner = "default-prov"
return test{
pki: pki,
args: args{
[]ConfigOption{},
},
want: &authconfig.Config{
Address: "127.0.0.1:9000",
InsecureAddress: "",
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &authconfig.AuthConfig{
DeploymentType: "", // TODO(hs): (why is) this is not set to standalone?
EnableAdmin: false,
Provisioners: provisioner.List{
&provisioner.JWK{
Type: "JWK",
Name: "default-prov",
},
},
},
DB: &db.Config{
Type: "badgerv2",
DataSource: filepath.Join(step.Path(), "db"),
},
},
wantErr: false,
}
},
"ok/with-acme": func(t *testing.T) test {
pki := preparePKI(t)
pki.options.deploymentType = StandaloneDeployment
pki.options.provisioner = "default-prov"
pki.options.enableACME = true
return test{
pki: pki,
args: args{
[]ConfigOption{},
},
want: &authconfig.Config{
Address: "127.0.0.1:9000",
InsecureAddress: "",
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &authconfig.AuthConfig{
DeploymentType: "", // TODO(hs): (why is) this is not set to standalone?
EnableAdmin: false,
Provisioners: provisioner.List{
&provisioner.JWK{
Type: "JWK",
Name: "default-prov",
},
&provisioner.ACME{
Type: "ACME",
Name: "acme",
},
},
},
DB: &db.Config{
Type: "badgerv2",
DataSource: filepath.Join(step.Path(), "db"),
},
},
wantErr: false,
}
},
"ok/with-acme-and-double-provisioner-name": func(t *testing.T) test {
pki := preparePKI(t)
pki.options.deploymentType = StandaloneDeployment
pki.options.provisioner = "acme"
pki.options.enableACME = true
return test{
pki: pki,
args: args{
[]ConfigOption{},
},
want: &authconfig.Config{
Address: "127.0.0.1:9000",
InsecureAddress: "",
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &authconfig.AuthConfig{
DeploymentType: "", // TODO(hs): (why is) this is not set to standalone?
EnableAdmin: false,
Provisioners: provisioner.List{
&provisioner.JWK{
Type: "JWK",
Name: "acme",
},
&provisioner.ACME{
Type: "ACME",
Name: "acme-1",
},
},
},
DB: &db.Config{
Type: "badgerv2",
DataSource: filepath.Join(step.Path(), "db"),
},
},
wantErr: false,
}
},
"ok/with-ssh": func(t *testing.T) test {
pki := preparePKI(t)
pki.options.deploymentType = StandaloneDeployment
pki.options.provisioner = "default-prov"
pki.options.enableSSH = true
return test{
pki: pki,
args: args{
[]ConfigOption{},
},
want: &authconfig.Config{
Address: "127.0.0.1:9000",
InsecureAddress: "",
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &authconfig.AuthConfig{
DeploymentType: "", // TODO(hs): (why is) this is not set to standalone?
EnableAdmin: false,
Provisioners: provisioner.List{
&provisioner.JWK{
Type: "JWK",
Name: "default-prov",
},
&provisioner.SSHPOP{
Type: "SSHPOP",
Name: "sshpop",
},
},
},
DB: &db.Config{
Type: "badgerv2",
DataSource: filepath.Join(step.Path(), "db"),
},
},
wantErr: false,
}
},
"ok/with-ssh-and-double-provisioner-name": func(t *testing.T) test {
pki := preparePKI(t)
pki.options.deploymentType = StandaloneDeployment
pki.options.provisioner = "sshpop"
pki.options.enableSSH = true
return test{
pki: pki,
args: args{
[]ConfigOption{},
},
want: &authconfig.Config{
Address: "127.0.0.1:9000",
InsecureAddress: "",
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &authconfig.AuthConfig{
DeploymentType: "", // TODO(hs): (why is) this is not set to standalone?
EnableAdmin: false,
Provisioners: provisioner.List{
&provisioner.JWK{
Type: "JWK",
Name: "sshpop",
},
&provisioner.SSHPOP{
Type: "SSHPOP",
Name: "sshpop-1",
},
},
},
DB: &db.Config{
Type: "badgerv2",
DataSource: filepath.Join(step.Path(), "db"),
},
},
wantErr: false,
}
},
"ok/with-admin": func(t *testing.T) test {
pki := preparePKI(t)
pki.options.deploymentType = StandaloneDeployment
pki.options.provisioner = "default-prov"
pki.options.enableAdmin = true
tempDir := t.TempDir()
return test{
pki: pki,
args: args{
[]ConfigOption{withDBDataSource(t, filepath.Join(tempDir, "db"))},
},
want: &authconfig.Config{
Address: "127.0.0.1:9000",
InsecureAddress: "",
DNSNames: []string{"127.0.0.1"},
AuthorityConfig: &authconfig.AuthConfig{
DeploymentType: "", // TODO(hs): (why is) this is not set to standalone?
EnableAdmin: true,
Provisioners: provisioner.List{}, // when admin is enabled, provisioner list is expected to be empty
},
DB: &db.Config{
Type: "badgerv2",
DataSource: filepath.Join(tempDir, "db"),
},
},
wantErr: false,
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
got, err := tc.pki.GenerateConfig(tc.args.opt...)
if tc.wantErr {
assert.NotNil(t, err)
assert.Nil(t, got)
return
}
assert.Nil(t, err)
if assert.NotNil(t, got) {
assert.Equal(t, tc.want.Address, got.Address)
assert.Equal(t, tc.want.InsecureAddress, got.InsecureAddress)
assert.Equal(t, tc.want.DNSNames, got.DNSNames)
assert.Equal(t, tc.want.DB, got.DB)
if assert.NotNil(t, tc.want.AuthorityConfig) {
assert.Equal(t, tc.want.AuthorityConfig.DeploymentType, got.AuthorityConfig.DeploymentType)
assert.Equal(t, tc.want.AuthorityConfig.EnableAdmin, got.AuthorityConfig.EnableAdmin)
if numberOfProvisioners := len(tc.want.AuthorityConfig.Provisioners); numberOfProvisioners > 0 {
if assert.Len(t, got.AuthorityConfig.Provisioners, numberOfProvisioners) {
for i, p := range tc.want.AuthorityConfig.Provisioners {
assert.Equal(t, p.GetType(), got.AuthorityConfig.Provisioners[i].GetType())
assert.Equal(t, p.GetName(), got.AuthorityConfig.Provisioners[i].GetName())
}
}
}
if tc.want.AuthorityConfig.EnableAdmin {
_db, err := db.New(tc.want.DB)
require.NoError(t, err)
defer _db.Shutdown()
adminDB, err := admindb.New(_db.(nosql.DB), admin.DefaultAuthorityID)
require.NoError(t, err)
provs, err := adminDB.GetProvisioners(context.Background())
require.NoError(t, err)
assert.NotEmpty(t, provs) // currently about the best we can do in terms of checks
}
}
}
})
}
}

@ -0,0 +1,82 @@
# Helm template
inject:
enabled: true
# Config contains the configuration files ca.json and defaults.json
config:
files:
ca.json:
root: /home/step/certs/root_ca.crt
federateRoots: []
crt: /home/step/certs/intermediate_ca.crt
key: /home/step/secrets/intermediate_ca_key
address: 127.0.0.1:9000
dnsNames:
- 127.0.0.1
logger:
format: json
db:
type: badgerv2
dataSource: /home/step/db
authority:
enableAdmin: false
provisioners:
- {"type":"JWK","name":"acme","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","options":{"x509":{},"ssh":{}}}
- {"type":"ACME","name":"acme-1"}
tls:
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
minVersion: 1.2
maxVersion: 1.3
renegotiation: false
defaults.json:
ca-url: https://127.0.0.1
ca-config: /home/step/config/ca.json
fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3
root: /home/step/certs/root_ca.crt
# Certificates contains the root and intermediate certificate and
# optionally the SSH host and user public keys
certificates:
# intermediate_ca contains the text of the intermediate CA Certificate
intermediate_ca: |
-----BEGIN CERTIFICATE-----
dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5
dGVz
-----END CERTIFICATE-----
# root_ca contains the text of the root CA Certificate
root_ca: |
-----BEGIN CERTIFICATE-----
dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw==
-----END CERTIFICATE-----
# Secrets contains the root and intermediate keys and optionally the SSH
# private keys
secrets:
# ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key
# This value must be base64 encoded.
ca_password:
provisioner_password:
x509:
# intermediate_ca_key contains the contents of your encrypted intermediate CA key
intermediate_ca_key: |
-----BEGIN EC PRIVATE KEY-----
dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0
ZXM=
-----END EC PRIVATE KEY-----
# root_ca_key contains the contents of your encrypted root CA key
# Note that this value can be omitted without impacting the functionality of step-certificates
# If supplied, this should be encrypted using a unique password that is not used for encrypting
# the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.
root_ca_key: |
-----BEGIN EC PRIVATE KEY-----
dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz
-----END EC PRIVATE KEY-----

@ -23,7 +23,7 @@ inject:
authority:
enableAdmin: false
provisioners:
- {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","claims":{"enableSSHCA":true,"disableRenewal":false,"allowRenewalAfterExpiry":false},"options":{"x509":{},"ssh":{}}}
- {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","claims":{"enableSSHCA":true,"disableRenewal":false,"allowRenewalAfterExpiry":false,"disableSmallstepExtensions":false},"options":{"x509":{},"ssh":{}}}
- {"type":"ACME","name":"acme"}
- {"type":"SSHPOP","name":"sshpop","claims":{"enableSSHCA":true}}
tls:

@ -0,0 +1,104 @@
# Helm template
inject:
enabled: true
# Config contains the configuration files ca.json and defaults.json
config:
files:
ca.json:
root: /home/step/certs/root_ca.crt
federateRoots: []
crt: /home/step/certs/intermediate_ca.crt
key: /home/step/secrets/intermediate_ca_key
ssh:
hostKey: /home/step/secrets/ssh_host_ca_key
userKey: /home/step/secrets/ssh_user_ca_key
address: 127.0.0.1:9000
dnsNames:
- 127.0.0.1
logger:
format: json
db:
type: badgerv2
dataSource: /home/step/db
authority:
enableAdmin: false
provisioners:
- {"type":"JWK","name":"sshpop","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","claims":{"enableSSHCA":true,"disableRenewal":false,"allowRenewalAfterExpiry":false,"disableSmallstepExtensions":false},"options":{"x509":{},"ssh":{}}}
- {"type":"SSHPOP","name":"sshpop-1","claims":{"enableSSHCA":true}}
tls:
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
minVersion: 1.2
maxVersion: 1.3
renegotiation: false
defaults.json:
ca-url: https://127.0.0.1
ca-config: /home/step/config/ca.json
fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3
root: /home/step/certs/root_ca.crt
# Certificates contains the root and intermediate certificate and
# optionally the SSH host and user public keys
certificates:
# intermediate_ca contains the text of the intermediate CA Certificate
intermediate_ca: |
-----BEGIN CERTIFICATE-----
dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5
dGVz
-----END CERTIFICATE-----
# root_ca contains the text of the root CA Certificate
root_ca: |
-----BEGIN CERTIFICATE-----
dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw==
-----END CERTIFICATE-----
# ssh_host_ca contains the text of the public ssh key for the SSH root CA
ssh_host_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms=
# ssh_user_ca contains the text of the public ssh key for the SSH root CA
ssh_user_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs=
# Secrets contains the root and intermediate keys and optionally the SSH
# private keys
secrets:
# ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key
# This value must be base64 encoded.
ca_password:
provisioner_password:
x509:
# intermediate_ca_key contains the contents of your encrypted intermediate CA key
intermediate_ca_key: |
-----BEGIN EC PRIVATE KEY-----
dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0
ZXM=
-----END EC PRIVATE KEY-----
# root_ca_key contains the contents of your encrypted root CA key
# Note that this value can be omitted without impacting the functionality of step-certificates
# If supplied, this should be encrypted using a unique password that is not used for encrypting
# the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.
root_ca_key: |
-----BEGIN EC PRIVATE KEY-----
dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz
-----END EC PRIVATE KEY-----
ssh:
# ssh_host_ca_key contains the contents of your encrypted SSH Host CA key
host_ca_key: |
-----BEGIN EC PRIVATE KEY-----
ZmFrZSBzc2ggaG9zdCBrZXkgYnl0ZXM=
-----END EC PRIVATE KEY-----
# ssh_user_ca_key contains the contents of your encrypted SSH User CA key
user_ca_key: |
-----BEGIN EC PRIVATE KEY-----
ZmFrZSBzc2ggdXNlciBrZXkgYnl0ZXM=
-----END EC PRIVATE KEY-----

@ -23,7 +23,7 @@ inject:
authority:
enableAdmin: false
provisioners:
- {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","claims":{"enableSSHCA":true,"disableRenewal":false,"allowRenewalAfterExpiry":false},"options":{"x509":{},"ssh":{}}}
- {"type":"JWK","name":"step-cli","key":{"use":"sig","kty":"EC","kid":"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I","crv":"P-256","alg":"ES256","x":"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY","y":"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI"},"encryptedKey":"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA","claims":{"enableSSHCA":true,"disableRenewal":false,"allowRenewalAfterExpiry":false,"disableSmallstepExtensions":false},"options":{"x509":{},"ssh":{}}}
- {"type":"SSHPOP","name":"sshpop","claims":{"enableSSHCA":true}}
tls:
cipherSuites:

@ -12,12 +12,13 @@ import (
"net/url"
"strings"
"github.com/go-chi/chi"
microscep "github.com/micromdm/scep/v2/scep"
"go.mozilla.org/pkcs7"
"github.com/go-chi/chi/v5"
"github.com/smallstep/pkcs7"
smallscep "github.com/smallstep/scep"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/api/log"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/scep"
)
@ -150,11 +151,14 @@ func decodeRequest(r *http.Request) (request, error) {
defer r.Body.Close()
method := r.Method
query := r.URL.Query()
query, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
return request{}, fmt.Errorf("failed parsing URL query: %w", err)
}
var operation string
if _, ok := query["operation"]; ok {
operation = query.Get("operation")
operation := query.Get("operation")
if operation == "" {
return request{}, errors.New("no operation provided")
}
switch method {
@ -166,14 +170,10 @@ func decodeRequest(r *http.Request) (request, error) {
Message: []byte{},
}, nil
case opnPKIOperation:
var message string
if _, ok := query["message"]; ok {
message = query.Get("message")
}
// TODO: verify this; right type of encoding? Needs additional transformations?
decodedMessage, err := base64.StdEncoding.DecodeString(message)
message := query.Get("message")
decodedMessage, err := decodeMessage(message, r)
if err != nil {
return request{}, err
return request{}, fmt.Errorf("failed decoding message: %w", err)
}
return request{
Operation: operation,
@ -185,7 +185,7 @@ func decodeRequest(r *http.Request) (request, error) {
case http.MethodPost:
body, err := io.ReadAll(io.LimitReader(r.Body, maxPayloadSize))
if err != nil {
return request{}, err
return request{}, fmt.Errorf("failed reading request body: %w", err)
}
return request{
Operation: operation,
@ -196,6 +196,77 @@ func decodeRequest(r *http.Request) (request, error) {
}
}
func decodeMessage(message string, r *http.Request) ([]byte, error) {
if message == "" {
return nil, errors.New("message must not be empty")
}
// decode the message, which should be base64 standard encoded. Any characters that
// were escaped in the original query, were unescaped as part of url.ParseQuery, so
// that doesn't need to be performed here. Return early if successful.
decodedMessage, err := base64.StdEncoding.DecodeString(message)
if err == nil {
return decodedMessage, nil
}
// only interested in corrupt input errors below this. This type of error is the
// most likely to return, but better safe than sorry.
var cie base64.CorruptInputError
if !errors.As(err, &cie) {
return nil, fmt.Errorf("failed base64 decoding message: %w", err)
}
// the below code is a workaround for macOS when it sends a GET PKIOperation, which seems to result
// in a query with the '+' and '/' not being percent encoded; only the padding ('=') is encoded.
// When that is unescaped in the code before this, this results in invalid base64. The workaround
// is to obtain the original query, extract the message, apply transformation(s) to make it valid
// base64 and try decoding it again. If it succeeds, the happy path can be followed with the patched
// message. Otherwise we still return an error.
rawQuery, err := parseRawQuery(r.URL.RawQuery)
if err != nil {
return nil, fmt.Errorf("failed to parse raw query: %w", err)
}
rawMessage := rawQuery.Get("message")
if rawMessage == "" {
return nil, errors.New("no message in raw query")
}
rawMessage = strings.ReplaceAll(rawMessage, "%3D", "=") // apparently the padding arrives encoded; the others (+, /) not?
decodedMessage, err = base64.StdEncoding.DecodeString(rawMessage)
if err != nil {
return nil, fmt.Errorf("failed base64 decoding raw message: %w", err)
}
return decodedMessage, nil
}
// parseRawQuery parses a URL query into url.Values. It skips
// unescaping keys and values. This code is based on url.ParseQuery.
func parseRawQuery(query string) (url.Values, error) {
m := make(url.Values)
err := parseRawQueryWithoutUnescaping(m, query)
return m, err
}
// parseRawQueryWithoutUnescaping parses the raw query into url.Values, skipping
// unescaping of the parts. This code is based on url.parseQuery.
func parseRawQueryWithoutUnescaping(m url.Values, query string) (err error) {
for query != "" {
var key string
key, query, _ = strings.Cut(query, "&")
if strings.Contains(key, ";") {
return errors.New("invalid semicolon separator in query")
}
if key == "" {
continue
}
key, value, _ := strings.Cut(key, "=")
m[key] = append(m[key], value)
}
return err
}
// lookupProvisioner loads the provisioner associated with the request.
// Responds 404 if the provisioner does not exist.
func lookupProvisioner(next http.HandlerFunc) http.HandlerFunc {
@ -208,7 +279,7 @@ func lookupProvisioner(next http.HandlerFunc) http.HandlerFunc {
}
ctx := r.Context()
auth := scep.MustFromContext(ctx)
auth := authority.MustFromContext(ctx)
p, err := auth.LoadProvisionerByName(provisionerName)
if err != nil {
fail(w, err)
@ -221,7 +292,7 @@ func lookupProvisioner(next http.HandlerFunc) http.HandlerFunc {
return
}
ctx = context.WithValue(ctx, scep.ProvisionerContextKey, scep.Provisioner(prov))
ctx = scep.NewProvisionerContext(ctx, scep.Provisioner(prov))
next(w, r.WithContext(ctx))
}
}
@ -249,7 +320,7 @@ func GetCACert(ctx context.Context) (Response, error) {
// create degenerate pkcs7 certificate structure, according to
// https://tools.ietf.org/html/rfc8894#section-4.2.1.2, because
// not signed or encrypted data has to be returned.
data, err := microscep.DegenerateCertificates(certs)
data, err := smallscep.DegenerateCertificates(certs)
if err != nil {
return Response{}, err
}
@ -274,16 +345,16 @@ func GetCACaps(ctx context.Context) (Response, error) {
// PKIOperation performs PKI operations and returns a SCEP response
func PKIOperation(ctx context.Context, req request) (Response, error) {
// parse the message using microscep implementation
microMsg, err := microscep.ParsePKIMessage(req.Message)
// parse the message using smallscep implementation
microMsg, err := smallscep.ParsePKIMessage(req.Message)
if err != nil {
// return the error, because we can't use the msg for creating a CertRep
return Response{}, err
}
// this is essentially doing the same as microscep.ParsePKIMessage, but
// this is essentially doing the same as smallscep.ParsePKIMessage, but
// gives us access to the p7 itself in scep.PKIMessage. Essentially a small
// wrapper for the microscep implementation.
// wrapper for the smallscep implementation.
p7, err := pkcs7.Parse(microMsg.Raw)
if err != nil {
return Response{}, err
@ -313,12 +384,12 @@ func PKIOperation(ctx context.Context, req request) (Response, error) {
// even if using the renewal flow as described in the README.md. MicroMDM SCEP client also only does PKCSreq by default, unless
// a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients.
// We'll have to see how it works out.
if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq {
if err := auth.ValidateChallenge(ctx, challengePassword, transactionID); err != nil {
if msg.MessageType == smallscep.PKCSReq || msg.MessageType == smallscep.RenewalReq {
if err := auth.ValidateChallenge(ctx, csr, challengePassword, transactionID); err != nil {
if errors.Is(err, provisioner.ErrSCEPChallengeInvalid) {
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, err)
return createFailureResponse(ctx, csr, msg, smallscep.BadRequest, err)
}
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("failed validating challenge password"))
return createFailureResponse(ctx, csr, msg, smallscep.BadRequest, errors.New("failed validating challenge password"))
}
}
@ -332,7 +403,16 @@ func PKIOperation(ctx context.Context, req request) (Response, error) {
certRep, err := auth.SignCSR(ctx, csr, msg)
if err != nil {
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err))
if notifyErr := auth.NotifyFailure(ctx, csr, transactionID, 0, err.Error()); notifyErr != nil {
// TODO(hs): ignore this error case? It's not critical if the notification fails; but logging it might be good
_ = notifyErr
}
return createFailureResponse(ctx, csr, msg, smallscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err))
}
if notifyErr := auth.NotifySuccess(ctx, csr, certRep.Certificate, transactionID); notifyErr != nil {
// TODO(hs): ignore this error case? It's not critical if the notification fails; but logging it might be good
_ = notifyErr
}
res := Response{
@ -368,7 +448,7 @@ func fail(w http.ResponseWriter, err error) {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
func createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info microscep.FailInfo, failError error) (Response, error) {
func createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info smallscep.FailInfo, failError error) (Response, error) {
auth := scep.MustFromContext(ctx)
certRepMsg, err := auth.CreateFailureResponse(ctx, csr, msg, scep.FailInfoName(info), failError.Error())
if err != nil {

@ -3,15 +3,27 @@ package api
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"net/url"
"strings"
"testing"
"testing/iotest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_decodeRequest(t *testing.T) {
randomB64 := "wx/1mQ49TpdLRfvVjQhXNSe8RB3hjZEarqYp5XVIxpSbvOhQSs8hP2TgucID1IputbA8JC6CbsUpcVae3+8hRNqs5pTsSHP2aNxsw8AHGSX9dZVymSclkUV8irk+ztfEfs7aLA=="
expectedRandom, err := base64.StdEncoding.DecodeString(randomB64)
require.NoError(t, err)
weirdMacOSCase := "wx/1mQ49TpdLRfvVjQhXNSe8RB3hjZEarqYp5XVIxpSbvOhQSs8hP2TgucID1IputbA8JC6CbsUpcVae3+8hRNqs5pTsSHP2aNxsw8AHGSX9dZVymSclkUV8irk+ztfEfs7aLA%3D%3D"
expectedWeirdMacOSCase, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(weirdMacOSCase, "%3D", "="))
require.NoError(t, err)
type args struct {
r *http.Request
}
@ -21,10 +33,26 @@ func Test_decodeRequest(t *testing.T) {
want request
wantErr bool
}{
{
name: "fail/invalid-query",
args: args{
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=bla;message=invalid-separator", http.NoBody),
},
want: request{},
wantErr: true,
},
{
name: "fail/empty-operation",
args: args{
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=", http.NoBody),
},
want: request{},
wantErr: true,
},
{
name: "fail/unsupported-method",
args: args{
r: httptest.NewRequest(http.MethodPatch, "http://scep:8080/?operation=AnUnsupportOperation", nil),
r: httptest.NewRequest(http.MethodPatch, "http://scep:8080/?operation=AnUnsupportOperation", http.NoBody),
},
want: request{},
wantErr: true,
@ -32,7 +60,15 @@ func Test_decodeRequest(t *testing.T) {
{
name: "fail/get-unsupported-operation",
args: args{
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=AnUnsupportOperation", nil),
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=AnUnsupportOperation", http.NoBody),
},
want: request{},
wantErr: true,
},
{
name: "fail/get-PKIOperation-empty-message",
args: args{
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=PKIOperation&message=", http.NoBody),
},
want: request{},
wantErr: true,
@ -40,7 +76,7 @@ func Test_decodeRequest(t *testing.T) {
{
name: "fail/get-PKIOperation",
args: args{
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=PKIOperation&message='somewronginput'", nil),
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=PKIOperation&message='somewronginput'", http.NoBody),
},
want: request{},
wantErr: true,
@ -56,7 +92,7 @@ func Test_decodeRequest(t *testing.T) {
{
name: "ok/get-GetCACert",
args: args{
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=GetCACert", nil),
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=GetCACert", http.NoBody),
},
want: request{
Operation: "GetCACert",
@ -67,7 +103,7 @@ func Test_decodeRequest(t *testing.T) {
{
name: "ok/get-GetCACaps",
args: args{
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=GetCACaps", nil),
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=GetCACaps", http.NoBody),
},
want: request{
Operation: "GetCACaps",
@ -78,7 +114,7 @@ func Test_decodeRequest(t *testing.T) {
{
name: "ok/get-PKIOperation",
args: args{
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=PKIOperation&message=MTIzNA==", nil),
r: httptest.NewRequest(http.MethodGet, "http://scep:8080/?operation=PKIOperation&message=MTIzNA==", http.NoBody),
},
want: request{
Operation: "PKIOperation",
@ -86,6 +122,39 @@ func Test_decodeRequest(t *testing.T) {
},
wantErr: false,
},
{
name: "ok/get-PKIOperation-escaped",
args: args{
r: httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://scep:8080/?operation=PKIOperation&message=%s", url.QueryEscape(randomB64)), http.NoBody),
},
want: request{
Operation: "PKIOperation",
Message: expectedRandom,
},
wantErr: false,
},
{
name: "ok/get-PKIOperation-not-escaped", // bit of a special case, but this is supported because of the macOS case now
args: args{
r: httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://scep:8080/?operation=PKIOperation&message=%s", randomB64), http.NoBody),
},
want: request{
Operation: "PKIOperation",
Message: expectedRandom,
},
wantErr: false,
},
{
name: "ok/get-PKIOperation-weird-macos-case", // a special case for macOS, which seems to result in the message not arriving fully percent-encoded
args: args{
r: httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://scep:8080/?operation=PKIOperation&message=%s", weirdMacOSCase), http.NoBody),
},
want: request{
Operation: "PKIOperation",
Message: expectedWeirdMacOSCase,
},
wantErr: false,
},
{
name: "ok/post-PKIOperation",
args: args{
@ -101,13 +170,14 @@ func Test_decodeRequest(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := decodeRequest(tt.args.r)
if (err != nil) != tt.wantErr {
t.Errorf("decodeRequest() error = %v, wantErr %v", err, tt.wantErr)
if tt.wantErr {
assert.Error(t, err)
assert.Equal(t, tt.want, got)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("decodeRequest() = %v, want %v", got, tt.want)
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

@ -2,14 +2,15 @@ package scep
import (
"context"
"crypto"
"crypto/x509"
"errors"
"fmt"
"net/url"
"sync"
microx509util "github.com/micromdm/scep/v2/cryptoutil/x509util"
microscep "github.com/micromdm/scep/v2/scep"
"go.mozilla.org/pkcs7"
"github.com/smallstep/pkcs7"
smallscep "github.com/smallstep/scep"
smallscepx509util "github.com/smallstep/scep/x509util"
"go.step.sm/crypto/x509util"
@ -18,12 +19,17 @@ import (
// Authority is the layer that handles all SCEP interactions.
type Authority struct {
prefix string
dns string
intermediateCertificate *x509.Certificate
caCerts []*x509.Certificate // TODO(hs): change to use these instead of root and intermediate
service *Service
signAuth SignAuthority
signAuth SignAuthority
roots []*x509.Certificate
intermediates []*x509.Certificate
defaultSigner crypto.Signer
signerCertificate *x509.Certificate
defaultDecrypter crypto.Decrypter
decrypterCertificate *x509.Certificate
scepProvisionerNames []string
provisionersMutex sync.RWMutex
encryptionAlgorithmMutex sync.Mutex
}
type authorityKey struct{}
@ -49,19 +55,6 @@ func MustFromContext(ctx context.Context) *Authority {
}
}
// AuthorityOptions required to create a new SCEP Authority.
type AuthorityOptions struct {
// Service provides the certificate chain, the signer and the decrypter to the Authority
Service *Service
// DNS is the host used to generate accurate SCEP 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 SCEP api is served. This
// prefix is required to generate accurate SCEP links.
Prefix string
}
// SignAuthority is the interface for a signing authority
type SignAuthority interface {
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
@ -69,24 +62,67 @@ type SignAuthority interface {
}
// New returns a new Authority that implements the SCEP interface.
func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) {
authority := &Authority{
prefix: ops.Prefix,
dns: ops.DNS,
signAuth: signAuth,
func New(signAuth SignAuthority, opts Options) (*Authority, error) {
if err := opts.Validate(); err != nil {
return nil, err
}
return &Authority{
signAuth: signAuth, // TODO: provide signAuth through context instead?
roots: opts.Roots,
intermediates: opts.Intermediates,
defaultSigner: opts.Signer,
signerCertificate: opts.SignerCert,
defaultDecrypter: opts.Decrypter,
decrypterCertificate: opts.SignerCert, // the intermediate signer cert is also the decrypter cert (if RSA)
scepProvisionerNames: opts.SCEPProvisionerNames,
}, nil
}
// Validate validates if the SCEP Authority has a valid configuration.
// The validation includes a check if a decrypter is available, either
// an authority wide decrypter, or a provisioner specific decrypter.
func (a *Authority) Validate() error {
if a == nil {
return nil
}
// TODO: this is not really nice to do; the Service should be removed
// in its entirety to make this more interoperable with the rest of
// step-ca, I think.
if ops.Service != nil {
authority.caCerts = ops.Service.certificateChain
// TODO(hs): look into refactoring SCEP into using just caCerts everywhere, if it makes sense for more elaborate SCEP configuration. Keeping it like this for clarity (for now).
authority.intermediateCertificate = ops.Service.certificateChain[0]
authority.service = ops.Service
a.provisionersMutex.RLock()
defer a.provisionersMutex.RUnlock()
noDefaultDecrypterAvailable := a.defaultDecrypter == nil
for _, name := range a.scepProvisionerNames {
p, err := a.LoadProvisionerByName(name)
if err != nil {
return fmt.Errorf("failed loading provisioner %q: %w", name, err)
}
if scepProv, ok := p.(*provisioner.SCEP); ok {
cert, decrypter := scepProv.GetDecrypter()
// TODO(hs): return sentinel/typed error, to be able to ignore/log these cases during init?
if cert == nil && noDefaultDecrypterAvailable {
return fmt.Errorf("SCEP provisioner %q does not have a decrypter certificate", name)
}
if decrypter == nil && noDefaultDecrypterAvailable {
return fmt.Errorf("SCEP provisioner %q does not have decrypter", name)
}
}
}
return authority, nil
return nil
}
// UpdateProvisioners updates the SCEP Authority with the new, and hopefully
// current SCEP provisioners configured. This allows the Authority to be
// validated with the latest data.
func (a *Authority) UpdateProvisioners(scepProvisionerNames []string) {
if a == nil {
return
}
a.provisionersMutex.Lock()
defer a.provisionersMutex.Unlock()
a.scepProvisionerNames = scepProvisionerNames
}
var (
@ -108,87 +144,58 @@ func (a *Authority) LoadProvisionerByName(name string) (provisioner.Interface, e
return a.signAuth.LoadProvisionerByName(name)
}
// GetLinkExplicit returns the requested link from the directory.
func (a *Authority) GetLinkExplicit(provName string, abs bool, baseURL *url.URL, inputs ...string) string {
return a.getLinkExplicit(provName, abs, baseURL, inputs...)
}
// getLinkExplicit returns an absolute or partial path to the given resource and a base
// URL dynamically obtained from the request for which the link is being calculated.
func (a *Authority) getLinkExplicit(provisionerName string, abs bool, baseURL *url.URL, _ ...string) string {
link := "/" + provisionerName
if abs {
// Copy the baseURL value from the pointer. https://github.com/golang/go/issues/38351
u := url.URL{}
if baseURL != nil {
u = *baseURL
}
// If no Scheme is set, then default to http (in case of SCEP)
if u.Scheme == "" {
u.Scheme = "http"
}
// If no Host is set, then use the default (first DNS attr in the ca.json).
if u.Host == "" {
u.Host = a.dns
}
u.Path = a.prefix + link
return u.String()
}
return link
}
// GetCACertificates returns the certificate (chain) for the CA
func (a *Authority) GetCACertificates(ctx context.Context) ([]*x509.Certificate, error) {
// TODO: this should return: the "SCEP Server (RA)" certificate, the issuing CA up to and excl. the root
// Some clients do need the root certificate however; also see: https://github.com/openxpki/openxpki/issues/73
//
// This means we might need to think about if we should use the current intermediate CA
// certificate as the "SCEP Server (RA)" certificate. It might be better to have a distinct
// RA certificate, with a corresponding rsa.PrivateKey, just for SCEP usage, which is signed by
// the intermediate CA. Will need to look how we can provide this nicely within step-ca.
//
// This might also mean that we might want to use a distinct instance of KMS for doing the key operations,
// so that we can use RSA just for SCEP.
//
// Using an RA does not seem to exist in https://tools.ietf.org/html/rfc8894, but is mentioned in
// https://tools.ietf.org/id/draft-nourse-scep-21.html. Will continue using the CA directly for now.
//
// The certificate to use should probably depend on the (configured) provisioner and may
// use a distinct certificate, apart from the intermediate.
p, err := provisionerFromContext(ctx)
if err != nil {
return nil, err
}
if len(a.caCerts) == 0 {
return nil, errors.New("no intermediate certificate available in SCEP authority")
}
certs := []*x509.Certificate{}
certs = append(certs, a.caCerts[0])
// NOTE: we're adding the CA roots here, but they are (highly likely) different than what the RFC means.
// Clients are responsible to select the right cert(s) to use, though.
if p.ShouldIncludeRootInChain() && len(a.caCerts) > 1 {
certs = append(certs, a.caCerts[1])
// GetCACertificates returns the certificate (chain) for the CA.
//
// This methods returns the "SCEP Server (RA)" certificate, the issuing CA up to and excl. the root.
// Some clients do need the root certificate however; also see: https://github.com/openxpki/openxpki/issues/73
//
// In case a provisioner specific decrypter is available, this is used as the "SCEP Server (RA)" certificate
// instead of the CA intermediate directly. This uses a distinct instance of a KMS for doing the SCEP key
// operations, so that RSA can be used for just SCEP.
//
// Using an RA does not seem to exist in https://tools.ietf.org/html/rfc8894, but is mentioned in
// https://tools.ietf.org/id/draft-nourse-scep-21.html.
func (a *Authority) GetCACertificates(ctx context.Context) (certs []*x509.Certificate, err error) {
p := provisionerFromContext(ctx)
// if a provisioner specific RSA decrypter is available, it is returned as
// the first certificate.
if decrypterCertificate, _ := p.GetDecrypter(); decrypterCertificate != nil {
certs = append(certs, decrypterCertificate)
}
// the CA intermediate is added to the chain by default. It's possible to
// exclude it from being added through configuration. This can be useful in
// environments where the SCEP client doesn't select the right RSA decrypter
// certificate, resulting in the wrong recipient in the PKCS7 message.
if p.ShouldIncludeIntermediateInChain() || len(certs) == 0 {
// TODO(hs): ensure logic is in place that checks the signer is the first
// intermediate and that there are no double certificates.
certs = append(certs, a.intermediates...)
}
// the CA roots are added for completeness when configured to do so. Clients
// are responsible to select the right cert(s) to store and use.
if p.ShouldIncludeRootInChain() {
certs = append(certs, a.roots...)
}
return certs, nil
}
// DecryptPKIEnvelope decrypts an enveloped message
func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error {
func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) error {
p7c, err := pkcs7.Parse(msg.P7.Content)
if err != nil {
return fmt.Errorf("error parsing pkcs7 content: %w", err)
}
envelope, err := p7c.Decrypt(a.intermediateCertificate, a.service.decrypter)
cert, decrypter, err := a.selectDecrypter(ctx)
if err != nil {
return fmt.Errorf("failed selecting decrypter: %w", err)
}
envelope, err := p7c.Decrypt(cert, decrypter)
if err != nil {
return fmt.Errorf("error decrypting encrypted pkcs7 content: %w", err)
}
@ -196,30 +203,33 @@ func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error
msg.pkiEnvelope = envelope
switch msg.MessageType {
case microscep.CertRep:
certs, err := microscep.CACerts(msg.pkiEnvelope)
case smallscep.CertRep:
certs, err := smallscep.CACerts(msg.pkiEnvelope)
if err != nil {
return fmt.Errorf("error extracting CA certs from pkcs7 degenerate data: %w", err)
}
msg.CertRepMessage.Certificate = certs[0]
return nil
case microscep.PKCSReq, microscep.UpdateReq, microscep.RenewalReq:
case smallscep.PKCSReq, smallscep.UpdateReq, smallscep.RenewalReq:
csr, err := x509.ParseCertificateRequest(msg.pkiEnvelope)
if err != nil {
return fmt.Errorf("parse CSR from pkiEnvelope: %w", err)
}
// check for challengePassword
cp, err := microx509util.ParseChallengePassword(msg.pkiEnvelope)
if err := csr.CheckSignature(); err != nil {
return fmt.Errorf("invalid CSR signature; %w", err)
}
// extract the challenge password
cp, err := smallscepx509util.ParseChallengePassword(msg.pkiEnvelope)
if err != nil {
return fmt.Errorf("parse challenge password in pkiEnvelope: %w", err)
}
msg.CSRReqMessage = &microscep.CSRReqMessage{
msg.CSRReqMessage = &smallscep.CSRReqMessage{
RawDecrypted: msg.pkiEnvelope,
CSR: csr,
ChallengePassword: cp,
}
return nil
case microscep.GetCRL, microscep.GetCert, microscep.CertPoll:
case smallscep.GetCRL, smallscep.GetCert, smallscep.CertPoll:
return errors.New("not implemented")
}
@ -234,10 +244,7 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
// poll for the status. It seems to be similar as what can happen in ACME, so might want to model
// the implementation after the one in the ACME authority. Requires storage, etc.
p, err := provisionerFromContext(ctx)
if err != nil {
return nil, err
}
p := provisionerFromContext(ctx)
// check if CSRReqMessage has already been decrypted
if msg.CSRReqMessage.CSR == nil {
@ -305,22 +312,15 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
cert := certChain[0]
// and create a degenerate cert structure
deg, err := microscep.DegenerateCertificates([]*x509.Certificate{cert})
deg, err := smallscep.DegenerateCertificates([]*x509.Certificate{cert})
if err != nil {
return nil, err
return nil, fmt.Errorf("failed generating degenerate certificate: %w", err)
}
// apparently the pkcs7 library uses a global default setting for the content encryption
// algorithm to use when en- or decrypting data. We need to restore the current setting after
// the cryptographic operation, so that other usages of the library are not influenced by
// this call to Encrypt(). We are not required to use the same algorithm the SCEP client uses.
encryptionAlgorithmToRestore := pkcs7.ContentEncryptionAlgorithm
pkcs7.ContentEncryptionAlgorithm = p.GetContentEncryptionAlgorithm()
e7, err := pkcs7.Encrypt(deg, msg.P7.Certificates)
e7, err := a.encrypt(deg, msg.P7.Certificates, p.GetContentEncryptionAlgorithm())
if err != nil {
return nil, err
return nil, fmt.Errorf("failed encrypting degenerate certificate: %w", err)
}
pkcs7.ContentEncryptionAlgorithm = encryptionAlgorithmToRestore
// PKIMessageAttributes to be signed
config := pkcs7.SignerInfoConfig{
@ -331,11 +331,11 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
},
{
Type: oidSCEPpkiStatus,
Value: microscep.SUCCESS,
Value: smallscep.SUCCESS,
},
{
Type: oidSCEPmessageType,
Value: microscep.CertRep,
Value: smallscep.CertRep,
},
{
Type: oidSCEPrecipientNonce,
@ -358,10 +358,13 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
// as the first certificate in the array
signedData.AddCertificate(cert)
authCert := a.intermediateCertificate
signerCert, signer, err := a.selectSigner(ctx)
if err != nil {
return nil, fmt.Errorf("failed selecting signer: %w", err)
}
// sign the attributes
if err := signedData.AddSigner(authCert, a.service.signer, config); err != nil {
if err := signedData.AddSigner(signerCert, signer, config); err != nil {
return nil, err
}
@ -371,8 +374,8 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
}
cr := &CertRepMessage{
PKIStatus: microscep.SUCCESS,
RecipientNonce: microscep.RecipientNonce(msg.SenderNonce),
PKIStatus: smallscep.SUCCESS,
RecipientNonce: smallscep.RecipientNonce(msg.SenderNonce),
Certificate: cert,
degenerate: deg,
}
@ -381,15 +384,37 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
crepMsg := &PKIMessage{
Raw: certRepBytes,
TransactionID: msg.TransactionID,
MessageType: microscep.CertRep,
MessageType: smallscep.CertRep,
CertRepMessage: cr,
}
return crepMsg, nil
}
func (a *Authority) encrypt(content []byte, recipients []*x509.Certificate, algorithm int) ([]byte, error) {
// apparently the pkcs7 library uses a global default setting for the content encryption
// algorithm to use when en- or decrypting data. We need to restore the current setting after
// the cryptographic operation, so that other usages of the library are not influenced by
// this call to Encrypt(). We are not required to use the same algorithm the SCEP client uses.
a.encryptionAlgorithmMutex.Lock()
defer a.encryptionAlgorithmMutex.Unlock()
encryptionAlgorithmToRestore := pkcs7.ContentEncryptionAlgorithm
defer func() {
pkcs7.ContentEncryptionAlgorithm = encryptionAlgorithmToRestore
}()
pkcs7.ContentEncryptionAlgorithm = algorithm
e7, err := pkcs7.Encrypt(content, recipients)
if err != nil {
return nil, err
}
return e7, nil
}
// CreateFailureResponse creates an appropriately signed reply for PKI operations
func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error) {
func (a *Authority) CreateFailureResponse(ctx context.Context, _ *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error) {
config := pkcs7.SignerInfoConfig{
ExtraSignedAttributes: []pkcs7.Attribute{
{
@ -398,7 +423,7 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate
},
{
Type: oidSCEPpkiStatus,
Value: microscep.FAILURE,
Value: smallscep.FAILURE,
},
{
Type: oidSCEPfailInfo,
@ -410,7 +435,7 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate
},
{
Type: oidSCEPmessageType,
Value: microscep.CertRep,
Value: smallscep.CertRep,
},
{
Type: oidSCEPsenderNonce,
@ -428,8 +453,13 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate
return nil, err
}
signerCert, signer, err := a.selectSigner(ctx)
if err != nil {
return nil, fmt.Errorf("failed selecting signer: %w", err)
}
// sign the attributes
if err := signedData.AddSigner(a.intermediateCertificate, a.service.signer, config); err != nil {
if err := signedData.AddSigner(signerCert, signer, config); err != nil {
return nil, err
}
@ -439,16 +469,16 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate
}
cr := &CertRepMessage{
PKIStatus: microscep.FAILURE,
FailInfo: microscep.FailInfo(info),
RecipientNonce: microscep.RecipientNonce(msg.SenderNonce),
PKIStatus: smallscep.FAILURE,
FailInfo: smallscep.FailInfo(info),
RecipientNonce: smallscep.RecipientNonce(msg.SenderNonce),
}
// create a CertRep message from the original
crepMsg := &PKIMessage{
Raw: certRepBytes,
TransactionID: msg.TransactionID,
MessageType: microscep.CertRep,
MessageType: smallscep.CertRep,
CertRepMessage: cr,
}
@ -457,10 +487,7 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate
// GetCACaps returns the CA capabilities
func (a *Authority) GetCACaps(ctx context.Context) []string {
p, err := provisionerFromContext(ctx)
if err != nil {
return defaultCapabilities
}
p := provisionerFromContext(ctx)
caps := p.GetCapabilities()
if len(caps) == 0 {
@ -476,10 +503,63 @@ func (a *Authority) GetCACaps(ctx context.Context) []string {
return caps
}
func (a *Authority) ValidateChallenge(ctx context.Context, challenge, transactionID string) error {
p, err := provisionerFromContext(ctx)
if err != nil {
return err
func (a *Authority) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error {
p := provisionerFromContext(ctx)
return p.ValidateChallenge(ctx, csr, challenge, transactionID)
}
func (a *Authority) NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {
p := provisionerFromContext(ctx)
return p.NotifySuccess(ctx, csr, cert, transactionID)
}
func (a *Authority) NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {
p := provisionerFromContext(ctx)
return p.NotifyFailure(ctx, csr, transactionID, errorCode, errorDescription)
}
func (a *Authority) selectDecrypter(ctx context.Context) (cert *x509.Certificate, decrypter crypto.Decrypter, err error) {
p := provisionerFromContext(ctx)
cert, decrypter = p.GetDecrypter()
switch {
case cert != nil && decrypter != nil:
return
case cert == nil && decrypter != nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a decrypter certificate available", p.GetName())
case cert != nil && decrypter == nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a decrypter available", p.GetName())
}
return p.ValidateChallenge(ctx, challenge, transactionID)
cert, decrypter = a.decrypterCertificate, a.defaultDecrypter
switch {
case cert == nil && decrypter != nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a default decrypter certificate available", p.GetName())
case cert != nil && decrypter == nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a default decrypter available", p.GetName())
}
return
}
func (a *Authority) selectSigner(ctx context.Context) (cert *x509.Certificate, signer crypto.Signer, err error) {
p := provisionerFromContext(ctx)
cert, signer = p.GetSigner()
switch {
case cert != nil && signer != nil:
return
case cert == nil && signer != nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a signer certificate available", p.GetName())
case cert != nil && signer == nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a signer available", p.GetName())
}
cert, signer = a.signerCertificate, a.defaultSigner
switch {
case cert == nil && signer != nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a default signer certificate available", p.GetName())
case cert != nil && signer == nil:
return nil, nil, fmt.Errorf("provisioner %q does not have a default signer available", p.GetName())
}
return
}

@ -0,0 +1,73 @@
package scep
import (
"crypto/x509"
"crypto/x509/pkix"
"testing"
"github.com/smallstep/pkcs7"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/randutil"
)
func generateContent(t *testing.T, size int) []byte {
t.Helper()
b, err := randutil.Bytes(size)
require.NoError(t, err)
return b
}
func generateRecipients(t *testing.T) []*x509.Certificate {
ca, err := minica.New()
require.NoError(t, err)
s, err := keyutil.GenerateSigner("RSA", "", 2048)
require.NoError(t, err)
tmpl := &x509.Certificate{
PublicKey: s.Public(),
Subject: pkix.Name{CommonName: "Test PKCS#7 Encryption"},
}
cert, err := ca.Sign(tmpl)
require.NoError(t, err)
return []*x509.Certificate{cert}
}
func TestAuthority_encrypt(t *testing.T) {
t.Parallel()
a := &Authority{}
recipients := generateRecipients(t)
type args struct {
content []byte
recipients []*x509.Certificate
algorithm int
}
tests := []struct {
name string
args args
wantErr bool
}{
{"alg-0", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmDESCBC}, false},
{"alg-1", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES128CBC}, false},
{"alg-2", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES256CBC}, false},
{"alg-3", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES128GCM}, false},
{"alg-4", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES256GCM}, false},
{"alg-unknown", args{generateContent(t, 32), recipients, 42}, true},
}
for _, tt := range tests {
tc := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := a.encrypt(tc.args.content, tc.args.recipients, tc.args.algorithm)
if tc.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
return
}
assert.NoError(t, err)
assert.NotEmpty(t, got)
})
}
}

@ -1,29 +0,0 @@
package scep
import (
"context"
"errors"
)
// ContextKey is the key type for storing and searching for SCEP request
// essentials in the context of a request.
type ContextKey string
const (
// ProvisionerContextKey provisioner key
ProvisionerContextKey = ContextKey("provisioner")
)
// provisionerFromContext searches the context for a SCEP provisioner.
// Returns the provisioner or an error.
func provisionerFromContext(ctx context.Context) (Provisioner, error) {
val := ctx.Value(ProvisionerContextKey)
if val == nil {
return nil, errors.New("provisioner expected in request context")
}
p, ok := val.(Provisioner)
if !ok || p == nil {
return nil, errors.New("provisioner in context is not a SCEP provisioner")
}
return p, nil
}

@ -1,7 +0,0 @@
package scep
import "crypto/x509"
type DB interface {
StoreCertificate(crt *x509.Certificate) error
}

@ -4,65 +4,78 @@ import (
"crypto"
"crypto/rsa"
"crypto/x509"
"github.com/pkg/errors"
"errors"
)
type Options struct {
// CertificateChain is the issuer certificate, along with any other bundled certificates
// to be returned in the chain for consumers. Configured in the ca.json crt property.
CertificateChain []*x509.Certificate
// Roots contains the (federated) CA roots certificate(s)
Roots []*x509.Certificate `json:"-"`
// Intermediates points issuer certificate, along with any other bundled certificates
// to be returned in the chain for consumers.
Intermediates []*x509.Certificate `json:"-"`
// SignerCert points to the certificate of the CA signer. It usually is the same as the
// first certificate in the CertificateChain.
SignerCert *x509.Certificate `json:"-"`
// Signer signs CSRs in SCEP. Configured in the ca.json key property.
Signer crypto.Signer `json:"-"`
// Decrypter decrypts encrypted SCEP messages. Configured in the ca.json key property.
Decrypter crypto.Decrypter `json:"-"`
// DecrypterCert points to the certificate of the CA decrypter.
DecrypterCert *x509.Certificate `json:"-"`
// SCEPProvisionerNames contains the currently configured SCEP provioner names. These
// are used to be able to load the provisioners when the SCEP authority is being
// validated.
SCEPProvisionerNames []string
}
type comparablePublicKey interface {
Equal(crypto.PublicKey) bool
}
// Validate checks the fields in Options.
func (o *Options) Validate() error {
if o.CertificateChain == nil {
return errors.New("certificate chain not configured correctly")
switch {
case len(o.Intermediates) == 0:
return errors.New("no intermediate certificate available for SCEP authority")
case o.Signer == nil:
return errors.New("no signer available for SCEP authority")
case o.SignerCert == nil:
return errors.New("no signer certificate available for SCEP authority")
}
if len(o.CertificateChain) < 1 {
return errors.New("certificate chain should at least have one certificate")
// check if the signer (intermediate CA) certificate has the same public key as
// the signer. According to the RFC it seems valid to have different keys for
// the intermediate and the CA signing new certificates, so this might change
// in the future.
signerPublicKey := o.Signer.Public().(comparablePublicKey)
if !signerPublicKey.Equal(o.SignerCert.PublicKey) {
return errors.New("mismatch between signer certificate and public key")
}
// According to the RFC: https://tools.ietf.org/html/rfc8894#section-3.1, SCEP
// can be used with something different than RSA, but requires the encryption
// to be performed using the challenge password. An older version of specification
// states that only RSA is supported: https://tools.ietf.org/html/draft-nourse-scep-23#section-2.1.1
// Other algorithms than RSA do not seem to be supported in certnanny/sscep, but it might work
// in micromdm/scep. Currently only RSA is allowed, but it might be an option
// to try other algorithms in the future.
intermediate := o.CertificateChain[0]
if intermediate.PublicKeyAlgorithm != x509.RSA {
return errors.New("only the RSA algorithm is (currently) supported")
}
// TODO: add checks for key usage?
signerPublicKey, ok := o.Signer.Public().(*rsa.PublicKey)
if !ok {
return errors.New("only RSA public keys are (currently) supported as signers")
}
// check if the intermediate ca certificate has the same public key as the signer.
// According to the RFC it seems valid to have different keys for the intermediate
// and the CA signing new certificates, so this might change in the future.
if !signerPublicKey.Equal(intermediate.PublicKey) {
return errors.New("mismatch between certificate chain and signer public keys")
// decrypter can be nil in case a signing only key is used; validation complete.
if o.Decrypter == nil {
return nil
}
// If a decrypter is available, check that it's backed by an RSA key. According to the
// RFC: https://tools.ietf.org/html/rfc8894#section-3.1, SCEP can be used with something
// different than RSA, but requires the encryption to be performed using the challenge
// password in that case. An older version of specification states that only RSA is
// supported: https://tools.ietf.org/html/draft-nourse-scep-23#section-2.1.1. Other
// algorithms do not seem to be supported in certnanny/sscep, but it might work
// in micromdm/scep. Currently only RSA is allowed, but it might be an option
// to try other algorithms in the future.
decrypterPublicKey, ok := o.Decrypter.Public().(*rsa.PublicKey)
if !ok {
return errors.New("only RSA public keys are (currently) supported as decrypters")
return errors.New("only RSA keys are (currently) supported as decrypters")
}
// check if intermediate public key is the same as the decrypter public key.
// In certnanny/sscep it's mentioned that the signing key can be different
// from the decrypting (and encrypting) key. Currently that's not supported.
if !decrypterPublicKey.Equal(intermediate.PublicKey) {
// from the decrypting (and encrypting) key. These options are only used and
// validated when the intermediate CA is also used as the decrypter, though,
// so they should match.
if !decrypterPublicKey.Equal(o.SignerCert.PublicKey) {
return errors.New("mismatch between certificate chain and decrypter public keys")
}

@ -2,20 +2,43 @@ package scep
import (
"context"
"time"
"crypto"
"crypto/x509"
"github.com/smallstep/certificates/authority/provisioner"
)
// Provisioner is an interface that implements a subset of the provisioner.Interface --
// only those methods required by the SCEP api/authority.
// Provisioner is an interface that embeds the
// provisioner.Interface and adds some SCEP specific
// functions.
type Provisioner interface {
AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error)
GetName() string
DefaultTLSCertDuration() time.Duration
provisioner.Interface
GetOptions() *provisioner.Options
GetCapabilities() []string
ShouldIncludeRootInChain() bool
ShouldIncludeIntermediateInChain() bool
GetDecrypter() (*x509.Certificate, crypto.Decrypter)
GetSigner() (*x509.Certificate, crypto.Signer)
GetContentEncryptionAlgorithm() int
ValidateChallenge(ctx context.Context, challenge, transactionID string) error
ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error
NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error
NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error
}
// provisionerKey is the key type for storing and searching a
// SCEP provisioner in the context.
type provisionerKey struct{}
// provisionerFromContext searches the context for a SCEP provisioner.
// Returns the provisioner or panics if no SCEP provisioner is found.
func provisionerFromContext(ctx context.Context) Provisioner {
p, ok := ctx.Value(provisionerKey{}).(Provisioner)
if !ok {
panic("SCEP provisioner expected in request context")
}
return p
}
func NewProvisionerContext(ctx context.Context, p Provisioner) context.Context {
return context.WithValue(ctx, provisionerKey{}, p)
}

@ -5,12 +5,12 @@ import (
"crypto/x509"
"encoding/asn1"
microscep "github.com/micromdm/scep/v2/scep"
"go.mozilla.org/pkcs7"
"github.com/smallstep/pkcs7"
smallscep "github.com/smallstep/scep"
)
// FailInfoName models the name/value of failInfo
type FailInfoName microscep.FailInfo
type FailInfoName smallscep.FailInfo
// FailInfo models a failInfo object consisting of a
// name/identifier and a failInfoText, the latter of
@ -35,10 +35,10 @@ var (
// PKIMessage defines the possible SCEP message types
type PKIMessage struct {
microscep.TransactionID
microscep.MessageType
microscep.SenderNonce
*microscep.CSRReqMessage
smallscep.TransactionID
smallscep.MessageType
smallscep.SenderNonce
*smallscep.CSRReqMessage
*CertRepMessage
@ -57,9 +57,9 @@ type PKIMessage struct {
// CertRepMessage is a type of PKIMessage
type CertRepMessage struct {
microscep.PKIStatus
microscep.RecipientNonce
microscep.FailInfo
smallscep.PKIStatus
smallscep.RecipientNonce
smallscep.FailInfo
Certificate *x509.Certificate

@ -1,28 +0,0 @@
package scep
import (
"context"
"crypto"
"crypto/x509"
)
// Service is a wrapper for crypto.Signer and crypto.Decrypter
type Service struct {
certificateChain []*x509.Certificate
signer crypto.Signer
decrypter crypto.Decrypter
}
// NewService returns a new Service type.
func NewService(_ context.Context, opts Options) (*Service, error) {
if err := opts.Validate(); err != nil {
return nil, err
}
// TODO: should this become similar to the New CertificateAuthorityService as in x509CAService?
return &Service{
certificateChain: opts.CertificateChain,
signer: opts.Signer,
decrypter: opts.Decrypter,
}, nil
}

@ -57,7 +57,9 @@ type DB interface {
type dryRunDB struct{}
func (*dryRunDB) CreateTable([]byte) error { return nil }
func (*dryRunDB) CreateTable([]byte) error { return nil }
//nolint:revive // allow unused parameters to show function signature
func (*dryRunDB) Set(bucket, key, value []byte) error { return nil }
func usage(fs *flag.FlagSet) {

@ -30,6 +30,7 @@ type X509Certificate struct {
PublicKeyAlgorithm string `json:"publicKeyAlgorithm"`
NotBefore time.Time `json:"notBefore"`
NotAfter time.Time `json:"notAfter"`
Raw []byte `json:"raw"`
}
// SSHCertificateRequest is the certificate request sent to webhook servers for
@ -69,7 +70,8 @@ type X5CCertificate struct {
// RequestBody is the body sent to webhook servers.
type RequestBody struct {
Timestamp time.Time `json:"timestamp"`
Timestamp time.Time `json:"timestamp"`
ProvisionerName string `json:"provisionerName,omitempty"`
// Only set after successfully completing acme device-attest-01 challenge
AttestationData *AttestationData `json:"attestationData,omitempty"`
// Set for most provisioners, but not acme or scep
@ -79,9 +81,11 @@ type RequestBody struct {
X509Certificate *X509Certificate `json:"x509Certificate,omitempty"`
SSHCertificateRequest *SSHCertificateRequest `json:"sshCertificateRequest,omitempty"`
SSHCertificate *SSHCertificate `json:"sshCertificate,omitempty"`
// Only set for SCEP challenge validation requests
SCEPChallenge string `json:"scepChallenge,omitempty"`
SCEPTransactionID string `json:"scepTransactionID,omitempty"`
// Only set for SCEP webhook requests
SCEPChallenge string `json:"scepChallenge,omitempty"`
SCEPTransactionID string `json:"scepTransactionID,omitempty"`
SCEPErrorCode int `json:"scepErrorCode,omitempty"`
SCEPErrorDescription string `json:"scepErrorDescription,omitempty"`
// Only set for X5C provisioners
X5CCertificate *X5CCertificate `json:"x5cCertificate,omitempty"`
// Set for X5C, AWS, GCP, and Azure provisioners

Loading…
Cancel
Save