Merge branch 'master' into herman/webhook-request-id

pull/1542/head
Herman Slatman 2 months ago
commit 2a8b80a3e1
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

@ -39,7 +39,6 @@ archives:
format_overrides:
- goos: windows
format: zip
wrap_in_directory: "{{ .ProjectName }}_{{ .Version }}"
files:
- README.md
- LICENSE
@ -48,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:
@ -164,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.
@ -298,7 +298,7 @@ winget:
pull_request:
# Whether to enable it or not.
enabled: true
#check_boxes: true
check_boxes: true
# Whether to open the PR as a draft or not.
#
# Default: false
@ -327,6 +327,7 @@ scoops:
repository:
owner: smallstep
name: scoop-bucket
branch: main
# Git author used to commit to the repository.
# Defaults are shown.

@ -25,10 +25,31 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
---
## [Unreleased]
## [0.25.1] - 2023-11-28
### Added
- Provisioner name in SCEP webhook request body in (smallstep/certificates#1617)
- Support for ASN1 boolean encoding in (smallstep/certificates#1590)
### Changed
- Generation of first provisioner name on `step ca init` in (smallstep/certificates#1566)
- Processing of SCEP Get PKIOperation requests in (smallstep/certificates#1570)
- Support for signing identity certificate during SSH sign by skipping URI validation in (smallstep/certificates#1572)
- Dependency on `micromdm/scep` and `go.mozilla.org/pkcs7` to use Smallstep forks in (smallstep/certificates#1600)
- Make the Common Name validator for JWK provisioners accept values from SANs too in (smallstep/certificates#1609)
### Fixed
- Registration Authority token creation relied on values from CSR. Fixed to rely on template in (smallstep/certificates#1608)
- Use same glibc version for running the CA when built using CGo in (smallstep/certificates#1616)
## [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)
@ -36,12 +57,34 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
(smallstep/certificates#1477)
- Added AWS public certificates for me-central-1 and ap-southeast-3
(smallstep/certificates#1404)
- Add namespace field to VaultCAS JSON config (smallstep/certificates#1424)
- 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
@ -53,6 +96,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
(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)

@ -25,7 +25,7 @@ func TestKeyToID(t *testing.T) {
jwk.Key = "foo"
return test{
jwk: jwk,
err: NewErrorISE("error generating jwk thumbprint: square/go-jose: unknown key type 'string'"),
err: NewErrorISE("error generating jwk thumbprint: go-jose/go-jose: unknown key type 'string'"),
}
},
"ok": func(t *testing.T) test {

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

@ -13,7 +13,7 @@ import (
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"go.step.sm/crypto/jose"

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

@ -422,11 +422,20 @@ func verifyAndExtractJWSPayload(next nextHTTP) nextHTTP {
render.Error(w, acme.NewError(acme.ErrorMalformedType, "verifier and signature algorithm do not match"))
return
}
payload, err := jws.Verify(jwk)
if err != nil {
switch {
case errors.Is(err, jose.ErrCryptoFailure):
payload, err = retryVerificationWithPatchedSignatures(jws, jwk)
if err != nil {
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "error verifying jws with patched signature(s)"))
return
}
case err != nil:
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "error verifying jws"))
return
}
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{
value: payload,
isPostAsGet: len(payload) == 0,
@ -436,6 +445,105 @@ func verifyAndExtractJWSPayload(next nextHTTP) nextHTTP {
}
}
// retryVerificationWithPatchedSignatures retries verification of the JWS using
// the JWK by patching the JWS signatures if they're determined to be too short.
//
// Generally this shouldn't happen, but we've observed this to be the case with
// the macOS ACME client, which seems to omit (at least one) leading null
// byte(s). The error returned is `go-jose/go-jose: error in cryptographic
// primitive`, which is a sentinel error that hides the details of the actual
// underlying error, which is as follows: `go-jose/go-jose: invalid signature
// size, have 63 bytes, wanted 64`, for ES256.
func retryVerificationWithPatchedSignatures(jws *jose.JSONWebSignature, jwk *jose.JSONWebKey) (data []byte, err error) {
originalSignatureValues := make([][]byte, len(jws.Signatures))
patched := false
defer func() {
if patched && err != nil {
for i, sig := range jws.Signatures {
sig.Signature = originalSignatureValues[i]
jws.Signatures[i] = sig
}
}
}()
for i, sig := range jws.Signatures {
var expectedSize int
alg := strings.ToUpper(sig.Header.Algorithm)
switch alg {
case jose.ES256:
expectedSize = 64
case jose.ES384:
expectedSize = 96
case jose.ES512:
expectedSize = 132
default:
// other cases are (currently) ignored
continue
}
switch diff := expectedSize - len(sig.Signature); diff {
case 0:
// expected length; nothing to do; will result in just doing the
// same verification (as done before calling this function) again,
// and thus an error will be returned.
continue
case 1:
patched = true
original := make([]byte, expectedSize-diff)
copy(original, sig.Signature)
originalSignatureValues[i] = original
patchedR := make([]byte, expectedSize)
copy(patchedR[1:], original) // [0x00, R.0:31, S.0:32], for expectedSize 64
sig.Signature = patchedR
jws.Signatures[i] = sig
// verify it with a patched R; return early if successful; continue
// with patching S if not.
data, err = jws.Verify(jwk)
if err == nil {
return
}
patchedS := make([]byte, expectedSize)
halfSize := expectedSize / 2
copy(patchedS, original[:halfSize]) // [R.0:32], for expectedSize 64
copy(patchedS[halfSize+1:], original[halfSize:]) // [R.0:32, 0x00, S.0:31]
sig.Signature = patchedS
jws.Signatures[i] = sig
case 2:
// assumption is currently the Apple case, in which only the
// first null byte of R and/or S are removed, and thus not a case in
// which two first bytes of either R or S are removed.
patched = true
original := make([]byte, expectedSize-diff)
copy(original, sig.Signature)
originalSignatureValues[i] = original
patchedRS := make([]byte, expectedSize)
halfSize := expectedSize / 2
copy(patchedRS[1:], original[:halfSize-1]) // [0x00, R.0:31], for expectedSize 64
copy(patchedRS[halfSize+1:], original[halfSize-1:]) // [0x00, R.0:31, 0x00, S.0:31]
sig.Signature = patchedRS
jws.Signatures[i] = sig
default:
// Technically, there can be multiple null bytes in either R or S,
// so when the difference is larger than 2, there is more than one
// option to pick. Apple's ACME client seems to only cut off the
// first null byte of either R or S, so we don't do anything in this
// case. Will result in just doing the same verification (as done
// before calling this function) again, and thus an error will be
// returned.
// TODO(hs): log this specific case? It might mean some other ACME
// client is doing weird things.
continue
}
}
data, err = jws.Verify(jwk)
return
}
// isPostAsGet asserts that the request is a PostAsGet (empty JWS payload).
func isPostAsGet(next nextHTTP) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) {

@ -6,6 +6,7 @@ import (
"crypto"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@ -14,9 +15,10 @@ import (
"strings"
"testing"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/acme"
tassert "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
)
@ -354,7 +356,7 @@ func TestHandler_parseJWS(t *testing.T) {
return test{
body: strings.NewReader("foo"),
statusCode: 400,
err: acme.NewError(acme.ErrorMalformedType, "failed to parse JWS from request body: square/go-jose: compact JWS format must have three parts"),
err: acme.NewError(acme.ErrorMalformedType, "failed to parse JWS from request body: go-jose/go-jose: compact JWS format must have three parts"),
}
},
"ok": func(t *testing.T) test {
@ -469,7 +471,7 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) {
err: acme.NewErrorISE("jwk expected in request context"),
}
},
"fail/verify-jws-failure": func(t *testing.T) test {
"fail/verify-jws-failure-wrong-jwk": func(t *testing.T) test {
_jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
_pub := _jwk.Public()
@ -478,7 +480,34 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) {
return test{
ctx: ctx,
statusCode: 400,
err: acme.NewError(acme.ErrorMalformedType, "error verifying jws: square/go-jose: error in cryptographic primitive"),
err: acme.NewError(acme.ErrorMalformedType, "error verifying jws: go-jose/go-jose: error in cryptographic primitive"),
}
},
"fail/verify-jws-failure-too-many-signatures": func(t *testing.T) test {
newParsedJWS, err := jose.ParseJWS(raw)
assert.FatalError(t, err)
newParsedJWS.Signatures = append(newParsedJWS.Signatures, newParsedJWS.Signatures...)
ctx := context.WithValue(context.Background(), jwsContextKey, newParsedJWS)
ctx = context.WithValue(ctx, jwkContextKey, pub)
return test{
ctx: ctx,
statusCode: 400,
err: acme.NewError(acme.ErrorMalformedType, "error verifying jws: go-jose/go-jose: too many signatures in payload; expecting only one"),
}
},
"fail/apple-acmeclient-omitting-leading-null-byte-in-signature-with-wrong-jwk": func(t *testing.T) test {
_jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
_pub := _jwk.Public()
appleNullByteCaseBody := `{"payload":"dGVzdC0xMTA1","protected":"eyJhbGciOiJFUzI1NiJ9","signature":"rQPYKYflfKnlgBKqDeWsJH2TJ6iHAnou7sFzXlmYD4ArXqLfYuqotWERKrna2wfzh0pu7USWO2gzlOqRK9qq"}`
appleNullByteCaseJWS, err := jose.ParseJWS(appleNullByteCaseBody)
require.NoError(t, err)
ctx := context.WithValue(context.Background(), jwsContextKey, appleNullByteCaseJWS)
ctx = context.WithValue(ctx, jwkContextKey, &_pub)
return test{
ctx: ctx,
statusCode: 400,
err: acme.NewError(acme.ErrorMalformedType, "error verifying jws: go-jose/go-jose: error in cryptographic primitive"),
}
},
"fail/algorithm-mismatch": func(t *testing.T) test {
@ -577,6 +606,38 @@ func TestHandler_verifyAndExtractJWSPayload(t *testing.T) {
},
}
},
"ok/apple-acmeclient-omitting-leading-null-byte-in-signature": func(t *testing.T) test {
appleNullByteCaseKey := []byte(`{
"kid": "uioinbiTlJICL0MYsb6ar1totfRA2tiPqWgntF8xUdo",
"crv": "P-256",
"alg": "ES256",
"kty": "EC",
"x": "wlz-Kv9X0h32fzLq-cogls9HxoZQqV-GuWxdb2MCeUY",
"y": "xzP6zRrg_jynYljZTxfJuql_QWtdQR6lpJ52q_6Vavg"
}`)
appleNullByteCaseJWK := &jose.JSONWebKey{}
err = json.Unmarshal(appleNullByteCaseKey, appleNullByteCaseJWK)
require.NoError(t, err)
appleNullByteCaseBody := `{"payload":"dGVzdC0xMTA1","protected":"eyJhbGciOiJFUzI1NiJ9","signature":"rQPYKYflfKnlgBKqDeWsJH2TJ6iHAnou7sFzXlmYD4ArXqLfYuqotWERKrna2wfzh0pu7USWO2gzlOqRK9qq"}`
appleNullByteCaseJWS, err := jose.ParseJWS(appleNullByteCaseBody)
require.NoError(t, err)
ctx := context.WithValue(context.Background(), jwsContextKey, appleNullByteCaseJWS)
ctx = context.WithValue(ctx, jwkContextKey, appleNullByteCaseJWK)
return test{
ctx: ctx,
statusCode: 200,
next: func(w http.ResponseWriter, r *http.Request) {
p, err := payloadFromContext(r.Context())
tassert.NoError(t, err)
if tassert.NotNil(t, p) {
tassert.Equal(t, []byte(`test-1105`), p.value)
tassert.False(t, p.isPostAsGet)
tassert.False(t, p.isEmptyJSON)
}
w.Write(testBody)
},
}
},
}
for name, run := range tests {
tc := run(t)
@ -1695,3 +1756,86 @@ func TestHandler_checkPrerequisites(t *testing.T) {
})
}
}
func Test_retryVerificationWithPatchedSignatures(t *testing.T) {
patchedRKey := []byte(`{
"kid": "uioinbiTlJICL0MYsb6ar1totfRA2tiPqWgntF8xUdo",
"crv": "P-256",
"alg": "ES256",
"kty": "EC",
"x": "wlz-Kv9X0h32fzLq-cogls9HxoZQqV-GuWxdb2MCeUY",
"y": "xzP6zRrg_jynYljZTxfJuql_QWtdQR6lpJ52q_6Vavg"
}`)
patchedRJWK := &jose.JSONWebKey{}
err := json.Unmarshal(patchedRKey, patchedRJWK)
require.NoError(t, err)
patchedRBody := `{"payload":"dGVzdC0xMTA1","protected":"eyJhbGciOiJFUzI1NiJ9","signature":"rQPYKYflfKnlgBKqDeWsJH2TJ6iHAnou7sFzXlmYD4ArXqLfYuqotWERKrna2wfzh0pu7USWO2gzlOqRK9qq"}`
patchedR, err := jose.ParseJWS(patchedRBody)
require.NoError(t, err)
patchedSKey := []byte(`{
"kid": "PblXsnK59uTiF5k3mmAN2B6HDPPxqBL_4UGhEG8ZO6g",
"crv": "P-256",
"alg": "ES256",
"kty": "EC",
"x": "T5aM_TOSattXNeUkH1VHZXh8URzdjZTI2zLvVgI0cy0",
"y": "Lf8h8qZnURXIxm6OnQ69kxGC91YtTZRD2GAroEf1UA8"
}`)
patchedSJWK := &jose.JSONWebKey{}
err = json.Unmarshal(patchedSKey, patchedSJWK)
require.NoError(t, err)
patchedSBody := `{"payload":"dGVzdC02Ng","protected":"eyJhbGciOiJFUzI1NiJ9","signature":"krtSKSgVB04oqx6i9QLeal_wZSnjV1_PSIM3AubT0WRIxnhl_yYbVpa3i53p3dUW56TtP6_SUZboH6SvLHMz"}`
patchedS, err := jose.ParseJWS(patchedSBody)
require.NoError(t, err)
patchedRSKey := []byte(`{
"kid": "U8BmBVbZsNUawvhOomJQPa6uYj1rdxCPQWF_nOLVsc4",
"crv": "P-256",
"alg": "ES256",
"kty": "EC",
"x": "Ym0l3GMS6aHBLo-xe73Kub4kafnOBu_QAfOsx5y-bV0",
"y": "wKijX9Cu67HbK94StPcI18WulgRfIMbP2ZU7gQuf3-M"
}`)
patchedRSJWK := &jose.JSONWebKey{}
err = json.Unmarshal(patchedRSKey, patchedRSJWK)
require.NoError(t, err)
patchedRSBody := `{"payload":"dGVzdC05MDY3","protected":"eyJhbGciOiJFUzI1NiJ9","signature":"2r_My19oRg7mWf9I5JTkNYp8otfEMz-yXRA8ltZTAKZxyJLurpVEgicmNItu7lfcCrGrTgI3Obye_gSaIyc"}`
patchedRS, err := jose.ParseJWS(patchedRSBody)
require.NoError(t, err)
patchedRWithWrongJWK, err := jose.ParseJWS(patchedRBody)
require.NoError(t, err)
tests := []struct {
name string
jws *jose.JSONWebSignature
jwk *jose.JSONWebKey
expectedData []byte
expectedSignature string
expectedError error
}{
{"ok/patched-r", patchedR, patchedRJWK, []byte(`test-1105`), `AK0D2CmH5Xyp5YASqg3lrCR9kyeohwJ6Lu7Bc15ZmA-AK16i32LqqLVhESq52tsH84dKbu1EljtoM5TqkSvaqg`, nil},
{"ok/patched-s", patchedS, patchedSJWK, []byte(`test-66`), `krtSKSgVB04oqx6i9QLeal_wZSnjV1_PSIM3AubT0WQASMZ4Zf8mG1aWt4ud6d3VFuek7T-v0lGW6B-kryxzMw`, nil},
{"ok/patched-rs", patchedRS, patchedRSJWK, []byte(`test-9067`), `ANq_zMtfaEYO5ln_SOSU5DWKfKLXxDM_sl0QPJbWUwAApnHIku6ulUSCJyY0i27uV9wKsatOAjc5vJ7-BJojJw`, nil},
{"fail/patched-r-wrong-jwk", patchedRWithWrongJWK, patchedRSJWK, nil, `rQPYKYflfKnlgBKqDeWsJH2TJ6iHAnou7sFzXlmYD4ArXqLfYuqotWERKrna2wfzh0pu7USWO2gzlOqRK9qq`, errors.New("go-jose/go-jose: error in cryptographic primitive")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
expectedSignature, decodeErr := base64.RawURLEncoding.DecodeString(tt.expectedSignature)
require.NoError(t, decodeErr)
data, err := retryVerificationWithPatchedSignatures(tt.jws, tt.jwk)
if tt.expectedError != nil {
tassert.EqualError(t, err, tt.expectedError.Error())
tassert.Equal(t, expectedSignature, tt.jws.Signatures[0].Signature)
tassert.Empty(t, data)
return
}
tassert.NoError(t, err)
tassert.Len(t, tt.jws.Signatures[0].Signature, 64)
tassert.Equal(t, expectedSignature, tt.jws.Signatures[0].Signature)
tassert.Equal(t, tt.expectedData, data)
})
}
}

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

@ -15,7 +15,7 @@ import (
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"go.step.sm/crypto/pemutil"

@ -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"
@ -1283,7 +1283,7 @@ func Test_wrapUnauthorizedError(t *testing.T) {
}
},
"wrap-subject": func(t *testing.T) test {
acmeErr := acme.NewError(acme.ErrorUnauthorizedType, "verification of jws using certificate public key failed: square/go-jose: error in cryptographic primitive")
acmeErr := acme.NewError(acme.ErrorUnauthorizedType, "verification of jws using certificate public key failed: go-jose/go-jose: error in cryptographic primitive")
acmeErr.Status = http.StatusForbidden
acmeErr.Detail = "No authorization provided for name test.example.com"
cert := &x509.Certificate{
@ -1292,7 +1292,7 @@ func Test_wrapUnauthorizedError(t *testing.T) {
},
}
return test{
err: errors.New("square/go-jose: error in cryptographic primitive"),
err: errors.New("go-jose/go-jose: error in cryptographic primitive"),
cert: cert,
unauthorizedIdentifiers: []acme.Identifier{},
msg: "verification of jws using certificate public key failed",

@ -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"
@ -528,6 +528,7 @@ type coseAlgorithmIdentifier int32
const (
coseAlgES256 coseAlgorithmIdentifier = -7
coseAlgRS256 coseAlgorithmIdentifier = -257
coseAlgRS1 coseAlgorithmIdentifier = -65535 // deprecated, but (still) often used in TPMs
)
func doTPMAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*tpmAttestationData, error) {
@ -652,15 +653,16 @@ func doTPMAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
return nil, NewDetailedError(ErrorBadAttestationStatementType, "invalid alg in attestation statement")
}
// only RS256 and ES256 are allowed
coseAlg := coseAlgorithmIdentifier(alg)
if coseAlg != coseAlgRS256 && coseAlg != coseAlgES256 {
var hash crypto.Hash
switch coseAlgorithmIdentifier(alg) {
case coseAlgRS256, coseAlgES256:
hash = crypto.SHA256
case coseAlgRS1:
hash = crypto.SHA1
default:
return nil, NewDetailedError(ErrorBadAttestationStatementType, "invalid alg %d in attestation statement", alg)
}
// set the hash algorithm to use to SHA256
hash := crypto.SHA256
// recreate the generated key certification parameter values and verify
// the attested key using the public key of the AK.
certificationParameters := &attest.CertificationParameters{

@ -354,7 +354,7 @@ func TestKeyAuthorization(t *testing.T) {
return test{
token: "1234",
jwk: jwk,
err: NewErrorISE("error generating JWK thumbprint: square/go-jose: unknown key type 'string'"),
err: NewErrorISE("error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'"),
}
},
"ok": func(t *testing.T) test {
@ -1089,7 +1089,7 @@ func TestHTTP01Validate(t *testing.T) {
},
},
jwk: jwk,
err: NewErrorISE("error generating JWK thumbprint: square/go-jose: unknown key type 'string'"),
err: NewErrorISE("error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'"),
}
},
"ok/key-auth-mismatch": func(t *testing.T) test {
@ -1389,7 +1389,7 @@ func TestDNS01Validate(t *testing.T) {
},
},
jwk: jwk,
err: NewErrorISE("error generating JWK thumbprint: square/go-jose: unknown key type 'string'"),
err: NewErrorISE("error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'"),
}
},
"fail/key-auth-mismatch-store-error": func(t *testing.T) test {
@ -2141,7 +2141,7 @@ func TestTLSALPN01Validate(t *testing.T) {
},
srv: srv,
jwk: jwk,
err: NewErrorISE("error generating JWK thumbprint: square/go-jose: unknown key type 'string'"),
err: NewErrorISE("error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'"),
}
},
"ok/error-no-extension": func(t *testing.T) test {

@ -817,7 +817,7 @@ func Test_doTPMAttestationFormat(t *testing.T) {
"certInfo": params.CreateAttestation,
"pubArea": params.Public,
},
}}, nil, newInternalServerError("failed creating key auth digest: error generating JWK thumbprint: square/go-jose: unknown key type '[]uint8'")},
}}, nil, newInternalServerError("failed creating key auth digest: error generating JWK thumbprint: go-jose/go-jose: unknown key type '[]uint8'")},
{"fail different keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "aDifferentToken"}, jwk, &attestationObject{
Format: "tpm",
AttStatement: map[string]interface{}{

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

@ -94,14 +94,17 @@ func ProvisionerFromContext(ctx context.Context) (v Provisioner, ok bool) {
return
}
// MustLinkerFromContext returns the current provisioner from the given context.
// MustProvisionerFromContext returns the current provisioner from the given context.
// It will panic if it's not in the context.
func MustProvisionerFromContext(ctx context.Context) Provisioner {
if v, ok := ProvisionerFromContext(ctx); !ok {
var (
v Provisioner
ok bool
)
if v, ok = ProvisionerFromContext(ctx); !ok {
panic("acme provisioner is not the context")
} else {
return v
}
return v
}
// MockProvisioner for testing

@ -71,11 +71,14 @@ func DatabaseFromContext(ctx context.Context) (db DB, ok bool) {
// MustDatabaseFromContext returns the current database from the given context.
// It will panic if it's not in the context.
func MustDatabaseFromContext(ctx context.Context) DB {
if db, ok := DatabaseFromContext(ctx); !ok {
var (
db DB
ok bool
)
if db, ok = DatabaseFromContext(ctx); !ok {
panic("acme database is not in the context")
} else {
return db
}
return db
}
// MockDB is an implementation of the DB interface that should only be used as

@ -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"
@ -142,11 +142,14 @@ func LinkerFromContext(ctx context.Context) (v Linker, ok bool) {
// MustLinkerFromContext returns the current linker from the given context. It
// will panic if it's not in the context.
func MustLinkerFromContext(ctx context.Context) Linker {
if v, ok := LinkerFromContext(ctx); !ok {
var (
v Linker
ok bool
)
if v, ok = LinkerFromContext(ctx); !ok {
panic("acme linker is not the context")
} else {
return v
}
return v
}
type baseURLKey struct{}

@ -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"
@ -54,7 +55,7 @@ type Authority interface {
GetRoots() ([]*x509.Certificate, error)
GetFederation() ([]*x509.Certificate, error)
Version() authority.Version
GetCertificateRevocationList() ([]byte, error)
GetCertificateRevocationList() (*authority.CertificateRevocationListInfo, error)
}
// mustAuthority will be replaced on unit tests.
@ -232,6 +233,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.
//
@ -239,24 +263,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,16 +26,13 @@ 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/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/x509util"
"golang.org/x/crypto/ssh"
squarejose "gopkg.in/square/go-jose.v2"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
@ -204,7 +201,7 @@ type mockAuthority struct {
getEncryptedKey func(kid string) (string, error)
getRoots func() ([]*x509.Certificate, error)
getFederation func() ([]*x509.Certificate, error)
getCRL func() ([]byte, error)
getCRL func() (*authority.CertificateRevocationListInfo, error)
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
@ -218,12 +215,12 @@ type mockAuthority struct {
version func() authority.Version
}
func (m *mockAuthority) GetCertificateRevocationList() ([]byte, error) {
func (m *mockAuthority) GetCertificateRevocationList() (*authority.CertificateRevocationListInfo, error) {
if m.getCRL != nil {
return m.getCRL()
}
return m.ret1.([]byte), m.err
return m.ret1.(*authority.CertificateRevocationListInfo), m.err
}
// TODO: remove once Authorize is deprecated.
@ -666,7 +663,7 @@ func TestSignRequest_Validate(t *testing.T) {
}
if err := s.Validate(); err != nil {
if assert.NotNil(t, tt.err) {
assert.HasPrefix(t, err.Error(), tt.err.Error())
assert.True(t, strings.HasPrefix(err.Error(), tt.err.Error()))
}
} else {
assert.Nil(t, tt.err)
@ -800,45 +797,6 @@ func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) (
return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err
}
func Test_CRLGeneration(t *testing.T) {
tests := []struct {
name string
err error
statusCode int
expected []byte
}{
{"empty", nil, http.StatusOK, nil},
}
chiCtx := chi.NewRouteContext()
req := httptest.NewRequest("GET", "http://example.com/crl", http.NoBody)
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockMustAuthority(t, &mockAuthority{ret1: tt.expected, err: tt.err})
w := httptest.NewRecorder()
CRL(w, req)
res := w.Result()
if res.StatusCode != tt.statusCode {
t.Errorf("caHandler.CRL StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Errorf("caHandler.Root unexpected error = %v", err)
}
if tt.statusCode == 200 {
if !bytes.Equal(bytes.TrimSpace(body), tt.expected) {
t.Errorf("caHandler.Root CRL = %s, wants %s", body, tt.expected)
}
}
})
}
}
func Test_caHandler_Route(t *testing.T) {
type fields struct {
Authority Authority
@ -1267,10 +1225,10 @@ func Test_Provisioners(t *testing.T) {
expectedError400 := errs.BadRequest("limit 'abc' is not an integer")
expectedError400Bytes, err := json.Marshal(expectedError400)
assert.FatalError(t, err)
require.NoError(t, err)
expectedError500 := errs.InternalServer("force")
expectedError500Bytes, err := json.Marshal(expectedError500)
assert.FatalError(t, err)
require.NoError(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockMustAuthority(t, tt.fields.Authority)
@ -1337,7 +1295,7 @@ func Test_ProvisionerKey(t *testing.T) {
expected := []byte(`{"key":"` + privKey + `"}`)
expectedError404 := errs.NotFound("force")
expectedError404Bytes, err := json.Marshal(expectedError404)
assert.FatalError(t, err)
require.NoError(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -1577,7 +1535,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",
@ -1587,11 +1544,11 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
"x": "7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8",
"y": "sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y",
}
key := squarejose.JSONWebKey{}
key := jose.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{
@ -1601,6 +1558,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",
@ -1617,7 +1580,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,
},
@ -1640,11 +1610,11 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
}
expBytes, err := json.Marshal(expected)
sassert.NoError(t, err)
assert.NoError(t, err)
br, err := r.MarshalJSON()
sassert.NoError(t, err)
sassert.JSONEq(t, string(expBytes), string(br))
assert.NoError(t, err)
assert.JSONEq(t, string(expBytes), string(br))
keyCopy := key
expList := provisioner.List{
@ -1654,6 +1624,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",
@ -1664,7 +1640,7 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) {
}
// MarshalJSON must not affect the struct properties itself
sassert.Equal(t, expList, r.Provisioners)
assert.Equal(t, expList, r.Provisioners)
}
const (
@ -1683,14 +1659,14 @@ func TestLogSSHCertificate(t *testing.T) {
rl := logging.NewResponseLogger(w)
LogSSHCertificate(rl, cert)
sassert.Equal(t, 200, w.Result().StatusCode)
assert.Equal(t, 200, w.Result().StatusCode)
fields := rl.Fields()
sassert.Equal(t, uint64(14376510277651266987), fields["serial"])
sassert.Equal(t, []string{"herman"}, fields["principals"])
sassert.Equal(t, "ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate", fields["certificate-type"])
sassert.Equal(t, time.Unix(1674129191, 0).Format(time.RFC3339), fields["valid-from"])
sassert.Equal(t, time.Unix(1674186851, 0).Format(time.RFC3339), fields["valid-to"])
sassert.Equal(t, "AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"])
sassert.Equal(t, "SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 (ECDSA-CERT)", fields["public-key"])
assert.Equal(t, uint64(14376510277651266987), fields["serial"])
assert.Equal(t, []string{"herman"}, fields["principals"])
assert.Equal(t, "ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate", fields["certificate-type"])
assert.Equal(t, time.Unix(1674129191, 0).Format(time.RFC3339), fields["valid-from"])
assert.Equal(t, time.Unix(1674186851, 0).Format(time.RFC3339), fields["valid-to"])
assert.Equal(t, "AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI", fields["certificate"])
assert.Equal(t, "SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 (ECDSA-CERT)", fields["public-key"])
}

@ -3,18 +3,32 @@ package api
import (
"encoding/pem"
"net/http"
"time"
"github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/errs"
)
// CRL is an HTTP handler that returns the current CRL in DER or PEM format
func CRL(w http.ResponseWriter, r *http.Request) {
crlBytes, err := mustAuthority(r.Context()).GetCertificateRevocationList()
crlInfo, err := mustAuthority(r.Context()).GetCertificateRevocationList()
if err != nil {
render.Error(w, err)
return
}
if crlInfo == nil {
render.Error(w, errs.New(http.StatusNotFound, "no CRL available"))
return
}
expires := crlInfo.ExpiresAt
if expires.IsZero() {
expires = time.Now()
}
w.Header().Add("Expires", expires.Format(time.RFC1123))
_, formatAsPEM := r.URL.Query()["pem"]
if formatAsPEM {
w.Header().Add("Content-Type", "application/x-pem-file")
@ -22,11 +36,11 @@ func CRL(w http.ResponseWriter, r *http.Request) {
_ = pem.Encode(w, &pem.Block{
Type: "X509 CRL",
Bytes: crlBytes,
Bytes: crlInfo.Data,
})
} else {
w.Header().Add("Content-Type", "application/pkix-crl")
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"")
w.Write(crlBytes)
w.Write(crlInfo.Data)
}
}

@ -0,0 +1,93 @@
package api
import (
"bytes"
"context"
"encoding/pem"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/errs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_CRL(t *testing.T) {
data := []byte{1, 2, 3, 4}
pemData := pem.EncodeToMemory(&pem.Block{
Type: "X509 CRL",
Bytes: data,
})
pemData = bytes.TrimSpace(pemData)
emptyPEMData := pem.EncodeToMemory(&pem.Block{
Type: "X509 CRL",
Bytes: nil,
})
emptyPEMData = bytes.TrimSpace(emptyPEMData)
tests := []struct {
name string
url string
err error
statusCode int
crlInfo *authority.CertificateRevocationListInfo
expectedBody []byte
expectedHeaders http.Header
expectedErrorJSON string
}{
{"ok", "http://example.com/crl", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: data}, data, http.Header{"Content-Type": []string{"application/pkix-crl"}, "Content-Disposition": []string{`attachment; filename="crl.der"`}}, ""},
{"ok/pem", "http://example.com/crl?pem=true", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: data}, pemData, http.Header{"Content-Type": []string{"application/x-pem-file"}, "Content-Disposition": []string{`attachment; filename="crl.pem"`}}, ""},
{"ok/empty", "http://example.com/crl", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: nil}, nil, http.Header{"Content-Type": []string{"application/pkix-crl"}, "Content-Disposition": []string{`attachment; filename="crl.der"`}}, ""},
{"ok/empty-pem", "http://example.com/crl?pem=true", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: nil}, emptyPEMData, http.Header{"Content-Type": []string{"application/x-pem-file"}, "Content-Disposition": []string{`attachment; filename="crl.pem"`}}, ""},
{"fail/internal", "http://example.com/crl", errs.Wrap(http.StatusInternalServerError, errors.New("failure"), "authority.GetCertificateRevocationList"), http.StatusInternalServerError, nil, nil, http.Header{}, `{"status":500,"message":"The certificate authority encountered an Internal Server Error. Please see the certificate authority logs for more info."}`},
{"fail/nil", "http://example.com/crl", nil, http.StatusNotFound, nil, nil, http.Header{}, `{"status":404,"message":"no CRL available"}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockMustAuthority(t, &mockAuthority{ret1: tt.crlInfo, err: tt.err})
chiCtx := chi.NewRouteContext()
req := httptest.NewRequest("GET", tt.url, http.NoBody)
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))
w := httptest.NewRecorder()
CRL(w, req)
res := w.Result()
assert.Equal(t, tt.statusCode, res.StatusCode)
body, err := io.ReadAll(res.Body)
res.Body.Close()
require.NoError(t, err)
if tt.statusCode >= 300 {
assert.JSONEq(t, tt.expectedErrorJSON, string(bytes.TrimSpace(body)))
return
}
// check expected header values
for _, h := range []string{"content-type", "content-disposition"} {
v := tt.expectedHeaders.Get(h)
require.NotEmpty(t, v)
actual := res.Header.Get(h)
assert.Equal(t, v, actual)
}
// check expires header value
assert.NotEmpty(t, res.Header.Get("expires"))
t1, err := time.Parse(time.RFC1123, res.Header.Get("expires"))
if assert.NoError(t, err) {
assert.False(t, t1.IsZero())
}
// check body contents
assert.Equal(t, tt.expectedBody, bytes.TrimSpace(body))
})
}
}

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

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

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

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

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

@ -11,7 +11,7 @@ import (
"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"

@ -92,11 +92,14 @@ func FromContext(ctx context.Context) (db DB, ok bool) {
// MustFromContext returns the current admin database from the given context. It
// will panic if it's not in the context.
func MustFromContext(ctx context.Context) DB {
if db, ok := FromContext(ctx); !ok {
var (
db DB
ok bool
)
if db, ok = FromContext(ctx); !ok {
panic("admin database is not in the context")
} else {
return db
}
return db
}
// MockDB is an implementation of the DB interface that should only be used as

@ -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
@ -101,6 +104,9 @@ type Authority struct {
// If true, do not output initialization logs
quietInit bool
// Called whenever applicable, in order to instrument the authority.
meter Meter
}
// Info contains information about the authority.
@ -122,6 +128,8 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) {
var a = &Authority{
config: cfg,
certificates: new(sync.Map),
validateSCEP: true,
meter: noopMeter{},
}
// Apply options.
@ -130,6 +138,9 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) {
return nil, err
}
}
if a.keyManager != nil {
a.keyManager = &instrumentedKeyManager{a.keyManager, a.meter}
}
if !a.skipInit {
// Initialize authority from options or configuration.
@ -147,6 +158,7 @@ func NewEmbedded(opts ...Option) (*Authority, error) {
a := &Authority{
config: &config.Config{},
certificates: new(sync.Map),
meter: noopMeter{},
}
// Apply options.
@ -155,6 +167,9 @@ func NewEmbedded(opts ...Option) (*Authority, error) {
return nil, err
}
}
if a.keyManager != nil {
a.keyManager = &instrumentedKeyManager{a.keyManager, a.meter}
}
// Validate required options
switch {
@ -197,11 +212,14 @@ func FromContext(ctx context.Context) (a *Authority, ok bool) {
// MustFromContext returns the current authority from the given context. It will
// panic if the authority is not in the context.
func MustFromContext(ctx context.Context) *Authority {
if a, ok := FromContext(ctx); !ok {
var (
a *Authority
ok bool
)
if a, ok = FromContext(ctx); !ok {
panic("authority is not in the context")
} else {
return a
}
return a
}
// ReloadAdminResources reloads admins and provisioners from the DB.
@ -261,6 +279,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
}
@ -312,6 +348,8 @@ func (a *Authority) init() error {
if err != nil {
return err
}
a.keyManager = &instrumentedKeyManager{a.keyManager, a.meter}
}
// Initialize linkedca client if necessary. On a linked RA, the issuer
@ -640,48 +678,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 +906,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 +917,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)
}
}

@ -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:
@ -286,16 +286,16 @@ func (a *Authority) authorizeRevoke(ctx context.Context, token string) error {
// extra extension cannot be found, authorize the renewal by default.
//
// TODO(mariano): should we authorize by default?
func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate) error {
func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate) (provisioner.Interface, error) {
serial := cert.SerialNumber.String()
var opts = []interface{}{errs.WithKeyVal("serialNumber", serial)}
isRevoked, err := a.IsRevoked(serial)
if err != nil {
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
}
if isRevoked {
return errs.Unauthorized("authority.authorizeRenew: certificate has been revoked", opts...)
return nil, errs.Unauthorized("authority.authorizeRenew: certificate has been revoked", opts...)
}
p, err := a.LoadProvisionerByCertificate(cert)
if err != nil {
@ -305,13 +305,13 @@ func (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate)
// returns the noop provisioner if this happens, and it allows
// certificate renewals.
if p, ok = a.provisioners.LoadByCertificate(cert); !ok {
return errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...)
return nil, errs.Unauthorized("authority.authorizeRenew: provisioner not found", opts...)
}
}
if err := p.AuthorizeRenew(ctx, cert); err != nil {
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
}
return nil
return p, nil
}
// authorizeSSHCertificate returns an error if the given certificate is revoked.

@ -876,7 +876,7 @@ func TestAuthority_authorizeRenew(t *testing.T) {
t.Run(name, func(t *testing.T) {
tc := genTestCase(t)
err := tc.auth.authorizeRenew(context.Background(), tc.cert)
_, err := tc.auth.authorizeRenew(context.Background(), tc.cert)
if err != nil {
if assert.NotNil(t, tc.err) {
var sc render.StatusCodedError

@ -83,6 +83,7 @@ type Config struct {
Templates *templates.Templates `json:"templates,omitempty"`
CommonName string `json:"commonName,omitempty"`
CRL *CRLConfig `json:"crl,omitempty"`
MetricsAddress string `json:"metricsAddress,omitempty"`
SkipValidation bool `json:"-"`
// Keeps record of the filename the Config is read from
@ -327,6 +328,12 @@ func (c *Config) Validate() error {
return errors.Errorf("invalid address %s", c.Address)
}
if addr := c.MetricsAddress; addr != "" {
if _, _, err := net.SplitHostPort(addr); err != nil {
return errors.Errorf("invalid metrics address %q", c.Address)
}
}
if c.TLS == nil {
c.TLS = &DefaultTLSOptions
} else {

@ -0,0 +1,87 @@
package authority
import (
"crypto"
"io"
"go.step.sm/crypto/kms"
kmsapi "go.step.sm/crypto/kms/apiv1"
"github.com/smallstep/certificates/authority/provisioner"
)
// Meter wraps the set of defined callbacks for metrics gatherers.
type Meter interface {
// X509Signed is called whenever an X509 certificate is signed.
X509Signed(provisioner.Interface, error)
// X509Renewed is called whenever an X509 certificate is renewed.
X509Renewed(provisioner.Interface, error)
// X509Rekeyed is called whenever an X509 certificate is rekeyed.
X509Rekeyed(provisioner.Interface, error)
// X509WebhookAuthorized is called whenever an X509 authoring webhook is called.
X509WebhookAuthorized(provisioner.Interface, error)
// X509WebhookEnriched is called whenever an X509 enriching webhook is called.
X509WebhookEnriched(provisioner.Interface, error)
// SSHSigned is called whenever an SSH certificate is signed.
SSHSigned(provisioner.Interface, error)
// SSHRenewed is called whenever an SSH certificate is renewed.
SSHRenewed(provisioner.Interface, error)
// SSHRekeyed is called whenever an SSH certificate is rekeyed.
SSHRekeyed(provisioner.Interface, error)
// SSHWebhookAuthorized is called whenever an SSH authoring webhook is called.
SSHWebhookAuthorized(provisioner.Interface, error)
// SSHWebhookEnriched is called whenever an SSH enriching webhook is called.
SSHWebhookEnriched(provisioner.Interface, error)
// KMSSigned is called per KMS signer signature.
KMSSigned(error)
}
// noopMeter implements a noop [Meter].
type noopMeter struct{}
func (noopMeter) SSHRekeyed(provisioner.Interface, error) {}
func (noopMeter) SSHRenewed(provisioner.Interface, error) {}
func (noopMeter) SSHSigned(provisioner.Interface, error) {}
func (noopMeter) SSHWebhookAuthorized(provisioner.Interface, error) {}
func (noopMeter) SSHWebhookEnriched(provisioner.Interface, error) {}
func (noopMeter) X509Rekeyed(provisioner.Interface, error) {}
func (noopMeter) X509Renewed(provisioner.Interface, error) {}
func (noopMeter) X509Signed(provisioner.Interface, error) {}
func (noopMeter) X509WebhookAuthorized(provisioner.Interface, error) {}
func (noopMeter) X509WebhookEnriched(provisioner.Interface, error) {}
func (noopMeter) KMSSigned(error) {}
type instrumentedKeyManager struct {
kms.KeyManager
meter Meter
}
func (i *instrumentedKeyManager) CreateSigner(req *kmsapi.CreateSignerRequest) (s crypto.Signer, err error) {
if s, err = i.KeyManager.CreateSigner(req); err == nil {
s = &instrumentedKMSSigner{s, i.meter}
}
return
}
type instrumentedKMSSigner struct {
crypto.Signer
meter Meter
}
func (i *instrumentedKMSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
signature, err = i.Signer.Sign(rand, digest, opts)
i.meter.KMSSigned(err)
return
}

@ -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.
@ -166,6 +167,15 @@ func WithKeyManager(k kms.KeyManager) Option {
}
}
// WithX509CAService allows the consumer to provide an externally implemented
// API implementation of apiv1.CertificateAuthorityService
func WithX509CAService(svc casapi.CertificateAuthorityService) Option {
return func(a *Authority) error {
a.x509CAService = svc
return nil
}
}
// WithX509Signer defines the signer used to sign X509 certificates.
func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option {
return WithX509SignerChain([]*x509.Certificate{crt}, s)
@ -205,6 +215,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 {
@ -369,3 +390,16 @@ func readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) {
}
return certs, nil
}
// WithMeter is an option that sets the authority's [Meter] to the provided one.
func WithMeter(m Meter) Option {
if m == nil {
m = noopMeter{}
}
return func(a *Authority) (_ error) {
a.meter = m
return
}
}

@ -6,8 +6,8 @@ import (
"reflect"
"testing"
"github.com/go-jose/go-jose/v3"
"github.com/stretchr/testify/assert"
"gopkg.in/square/go-jose.v2"
"go.step.sm/linkedca"

@ -336,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")
@ -363,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

@ -1,4 +1,5 @@
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/verify-signature.html
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/regions-certs.html use RSA format
# default certificate for "other regions"
-----BEGIN CERTIFICATE-----
@ -244,4 +245,20 @@ Af8ECDAGAQH/AgEAMA0GCSqGSIb3DQEBCwUAA4GBACrKjWj460GUPZCGm3/z0dIz
M2BPuH769wcOsqfFZcMKEysSFK91tVtUb1soFwH4/Lb/T0PqNrvtEwD1Nva5k0h2
xZhNNRmDuhOhW1K9wCcnHGRBwY5t4lYL6hNV6hcrqYwGMjTjcAjBG2yMgznSNFle
Rwi/S3BFXISixNx9cILu
-----END CERTIFICATE-----
# certificate for ca-west-1
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAYPou9weMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
AlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl
MSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMjEwMTgwMTM2
MDlaGA8yMjAxMTAxODAxMzYwOVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh
c2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv
biBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK
1kIcG5Q6adBXQM75GldfTSiXl7tn54p10TnspI0ErDdb2B6q2Ji/v4XBVH13ZCMg
qlRHMqV8AWI5iO6gFn2A9sN3AZXTMqwtZeiDdebq3k6Wt7ieYvpXTg0qvgsjQIov
RZWaBDBJy9x8C2hW+w9lMQjFHkJ7Jy/PHCJ69EzebQIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBAGe9Snkz1A6rHBH6/5kDtYvtPYwhx2sXNxztbhkXErFk40Nw5l459NZx
EeudxJBLoCkkSgYjhRcOZ/gvDVtWG7qyb6fAqgoisyAbk8K9LzxSim2S1nmT9vD8
4B/t/VvwQBylc+ej8kRxMH7fquZLp7IXfmtBzyUqu6Dpbne+chG2
-----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:
@ -895,5 +896,5 @@ func TestAWS_HardcodedCertificates(t *testing.T) {
assert.True(t, cert.NotAfter.After(time.Now()))
certs = append(certs, cert)
}
assert.Len(t, 14, certs, "expected 14 certificates in aws_certificates.pem")
assert.Len(t, 15, certs, "expected 15 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.

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

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

@ -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")
@ -190,9 +190,9 @@ func (p *JWK) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
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),

@ -171,10 +171,10 @@ func TestJWK_authorizeToken(t *testing.T) {
{"fail-token", p1, args{failTok}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk token")},
{"fail-key", p1, args{failKey}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk claims")},
{"fail-claims", p1, args{failClaims}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk claims")},
{"fail-signature", p1, args{failSig}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk claims: square/go-jose: error in cryptographic primitive")},
{"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: square/go-jose/jwt: validation failed, invalid issuer claim (iss)")},
{"fail-expired", p1, args{failExp}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: square/go-jose/jwt: validation failed, token is expired (exp)")},
{"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: square/go-jose/jwt: validation failed, token not valid yet (nbf)")},
{"fail-signature", p1, args{failSig}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk claims: go-jose/go-jose: error in cryptographic primitive")},
{"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: go-jose/go-jose/jwt: validation failed, invalid issuer claim (iss)")},
{"fail-expired", p1, args{failExp}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: go-jose/go-jose/jwt: validation failed, token is expired (exp)")},
{"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: go-jose/go-jose/jwt: validation failed, token not valid yet (nbf)")},
{"fail-audience", p1, args{failAud}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk token audience claim (aud)")},
{"fail-subject", p1, args{failSub}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; jwk token subject cannot be empty")},
{"ok", p1, args{t1}, http.StatusOK, nil},
@ -218,7 +218,7 @@ func TestJWK_AuthorizeRevoke(t *testing.T) {
code int
err error
}{
{"fail-signature", p1, args{failSig}, http.StatusUnauthorized, errors.New("jwk.AuthorizeRevoke: jwk.authorizeToken; error parsing jwk claims: square/go-jose: error in cryptographic primitive")},
{"fail-signature", p1, args{failSig}, http.StatusUnauthorized, errors.New("jwk.AuthorizeRevoke: jwk.authorizeToken; error parsing jwk claims: go-jose/go-jose: error in cryptographic primitive")},
{"ok", p1, args{t1}, http.StatusOK, nil},
}
for _, tt := range tests {
@ -266,7 +266,7 @@ func TestJWK_AuthorizeSign(t *testing.T) {
prov: p1,
args: args{failSig},
code: http.StatusUnauthorized,
err: errors.New("jwk.AuthorizeSign: jwk.authorizeToken; error parsing jwk claims: square/go-jose: error in cryptographic primitive"),
err: errors.New("jwk.AuthorizeSign: jwk.authorizeToken; error parsing jwk claims: go-jose/go-jose: error in cryptographic primitive"),
},
{
name: "ok-sans",
@ -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:

@ -97,7 +97,7 @@ func TestK8sSA_authorizeToken(t *testing.T) {
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("k8ssa.authorizeToken; invalid k8sSA token claims: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"),
err: errors.New("k8ssa.authorizeToken; invalid k8sSA token claims: go-jose/go-jose/jwt: validation failed, invalid issuer claim (iss)"),
}
},
"ok": func(t *testing.T) test {

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

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

@ -233,11 +233,11 @@ func TestOIDC_authorizeToken(t *testing.T) {
{"fail-key", p1, args{failKey}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken; cannot validate oidc token`)},
{"fail-token", p1, args{failTok}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken; error parsing oidc token: invalid character '~' looking for beginning of value`)},
{"fail-claims", p1, args{failClaims}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken; error parsing oidc token claims: invalid character '~' looking for beginning of value`)},
{"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: square/go-jose/jwt: validation failed, invalid issuer claim (iss)`)},
{"fail-audience", p1, args{failAud}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: square/go-jose/jwt: validation failed, invalid audience claim (aud)`)},
{"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: go-jose/go-jose/jwt: validation failed, invalid issuer claim (iss)`)},
{"fail-audience", p1, args{failAud}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: go-jose/go-jose/jwt: validation failed, invalid audience claim (aud)`)},
{"fail-signature", p1, args{failSig}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken; cannot validate oidc token`)},
{"fail-expired", p1, args{failExp}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: square/go-jose/jwt: validation failed, token is expired (exp)`)},
{"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: square/go-jose/jwt: validation failed, token not valid yet (nbf)`)},
{"fail-expired", p1, args{failExp}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: go-jose/go-jose/jwt: validation failed, token is expired (exp)`)},
{"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, "", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: go-jose/go-jose/jwt: validation failed, token not valid yet (nbf)`)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

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

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

@ -180,7 +180,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)

@ -484,7 +484,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")
@ -244,7 +244,7 @@ func (p *X5C) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
},
// 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())

@ -10,7 +10,6 @@ import (
"os"
"github.com/pkg/errors"
"gopkg.in/square/go-jose.v2/jwt"
"go.step.sm/cli-utils/step"
"go.step.sm/cli-utils/ui"
@ -146,7 +145,7 @@ func (a *Authority) unsafeLoadProvisionerFromDatabase(crt *x509.Certificate) (pr
// LoadProvisionerByToken returns an interface to the provisioner that
// provisioned the token.
func (a *Authority) LoadProvisionerByToken(token *jwt.JSONWebToken, claims *jwt.Claims) (provisioner.Interface, error) {
func (a *Authority) LoadProvisionerByToken(token *jose.JSONWebToken, claims *jose.Claims) (provisioner.Interface, error) {
a.adminMutex.RLock()
defer a.adminMutex.RUnlock()
p, ok := a.provisioners.LoadByToken(token, claims)
@ -235,7 +234,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.
@ -974,7 +973,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,
@ -982,11 +981,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() {
@ -1241,7 +1248,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),
},
},
},
},

@ -147,6 +147,12 @@ func (a *Authority) GetSSHBastion(ctx context.Context, user, hostname string) (*
// SignSSH creates a signed SSH certificate with the given public key and options.
func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
cert, prov, err := a.signSSH(ctx, key, opts, signOpts...)
a.meter.SSHSigned(prov, err)
return cert, err
}
func (a *Authority) signSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, provisioner.Interface, error) {
var (
certOptions []sshutil.Option
mods []provisioner.SSHCertModifier
@ -155,7 +161,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
// Validate given options.
if err := opts.Validate(); err != nil {
return nil, err
return nil, nil, err
}
// Set backdate with the configured value
@ -184,7 +190,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
// validate the given SSHOptions
case provisioner.SSHCertOptionsValidator:
if err := o.Valid(opts); err != nil {
return nil, errs.BadRequestErr(err, "error validating ssh certificate options")
return nil, prov, errs.BadRequestErr(err, "error validating ssh certificate options")
}
// call webhooks
@ -192,7 +198,7 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
webhookCtl = o
default:
return nil, errs.InternalServer("authority.SignSSH: invalid extra option type %T", o)
return nil, prov, errs.InternalServer("authority.SignSSH: invalid extra option type %T", o)
}
}
@ -205,8 +211,8 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
}
// Call enriching webhooks
if err := callEnrichingWebhooksSSH(ctx, webhookCtl, cr); err != nil {
return nil, errs.ApplyOptions(
if err := a.callEnrichingWebhooksSSH(ctx, prov, webhookCtl, cr); err != nil {
return nil, prov, errs.ApplyOptions(
errs.ForbiddenErr(err, err.Error()),
errs.WithKeyVal("signOptions", signOpts),
)
@ -216,20 +222,21 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
certificate, err := sshutil.NewCertificate(cr, certOptions...)
if err != nil {
var te *sshutil.TemplateError
if errors.As(err, &te) {
return nil, errs.ApplyOptions(
switch {
case errors.As(err, &te):
return nil, prov, errs.ApplyOptions(
errs.BadRequestErr(err, err.Error()),
errs.WithKeyVal("signOptions", signOpts),
)
}
// explicitly check for unmarshaling errors, which are most probably caused by JSON template syntax errors
if strings.HasPrefix(err.Error(), "error unmarshaling certificate") {
return nil, errs.InternalServerErr(templatingError(err),
case strings.HasPrefix(err.Error(), "error unmarshaling certificate"):
// explicitly check for unmarshaling errors, which are most probably caused by JSON template syntax errors
return nil, prov, errs.InternalServerErr(templatingError(err),
errs.WithKeyVal("signOptions", signOpts),
errs.WithMessage("error applying certificate template"),
)
default:
return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH")
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH")
}
// Get actual *ssh.Certificate and continue with provisioner modifiers.
@ -238,13 +245,13 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
// Use SignSSHOptions to modify the certificate validity. It will be later
// checked or set if not defined.
if err := opts.ModifyValidity(certTpl); err != nil {
return nil, errs.BadRequestErr(err, err.Error())
return nil, prov, errs.BadRequestErr(err, err.Error())
}
// Use provisioner modifiers.
for _, m := range mods {
if err := m.Modify(certTpl, opts); err != nil {
return nil, errs.ForbiddenErr(err, "error creating ssh certificate")
return nil, prov, errs.ForbiddenErr(err, "error creating ssh certificate")
}
}
@ -253,32 +260,32 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
switch certTpl.CertType {
case ssh.UserCert:
if a.sshCAUserCertSignKey == nil {
return nil, errs.NotImplemented("authority.SignSSH: user certificate signing is not enabled")
return nil, prov, errs.NotImplemented("authority.SignSSH: user certificate signing is not enabled")
}
signer = a.sshCAUserCertSignKey
case ssh.HostCert:
if a.sshCAHostCertSignKey == nil {
return nil, errs.NotImplemented("authority.SignSSH: host certificate signing is not enabled")
return nil, prov, errs.NotImplemented("authority.SignSSH: host certificate signing is not enabled")
}
signer = a.sshCAHostCertSignKey
default:
return nil, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType)
return nil, prov, errs.InternalServer("authority.SignSSH: unexpected ssh certificate type: %d", certTpl.CertType)
}
// Check if authority is allowed to sign the certificate
if err := a.isAllowedToSignSSHCertificate(certTpl); err != nil {
var ee *errs.Error
if errors.As(err, &ee) {
return nil, ee
return nil, prov, ee
}
return nil, errs.InternalServerErr(err,
return nil, prov, errs.InternalServerErr(err,
errs.WithMessage("authority.SignSSH: error creating ssh certificate"),
)
}
// Send certificate to webhooks for authorization
if err := callAuthorizingWebhooksSSH(ctx, webhookCtl, certificate, certTpl); err != nil {
return nil, errs.ApplyOptions(
if err := a.callAuthorizingWebhooksSSH(ctx, prov, webhookCtl, certificate, certTpl); err != nil {
return nil, prov, errs.ApplyOptions(
errs.ForbiddenErr(err, "authority.SignSSH: error signing certificate"),
)
}
@ -286,21 +293,21 @@ func (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisi
// Sign certificate.
cert, err := sshutil.CreateCertificate(certTpl, signer)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error signing certificate")
return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error signing certificate")
}
// User provisioners validators.
for _, v := range validators {
if err := v.Valid(cert, opts); err != nil {
return nil, errs.ForbiddenErr(err, "error validating ssh certificate")
return nil, prov, errs.ForbiddenErr(err, "error validating ssh certificate")
}
}
if err = a.storeSSHCertificate(prov, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error storing certificate in db")
if err := a.storeSSHCertificate(prov, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {
return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.SignSSH: error storing certificate in db")
}
return cert, nil
return cert, prov, nil
}
// isAllowedToSignSSHCertificate checks if the Authority is allowed to sign the SSH certificate.
@ -310,12 +317,18 @@ func (a *Authority) isAllowedToSignSSHCertificate(cert *ssh.Certificate) error {
// RenewSSH creates a signed SSH certificate using the old SSH certificate as a template.
func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, error) {
cert, prov, err := a.renewSSH(ctx, oldCert)
a.meter.SSHRenewed(prov, err)
return cert, err
}
func (a *Authority) renewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, provisioner.Interface, error) {
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
return nil, errs.BadRequest("cannot renew a certificate without validity period")
return nil, nil, errs.BadRequest("cannot renew a certificate without validity period")
}
if err := a.authorizeSSHCertificate(ctx, oldCert); err != nil {
return nil, err
return nil, nil, err
}
// Attempt to extract the provisioner from the token.
@ -348,36 +361,41 @@ func (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ss
switch certTpl.CertType {
case ssh.UserCert:
if a.sshCAUserCertSignKey == nil {
return nil, errs.NotImplemented("renewSSH: user certificate signing is not enabled")
return nil, prov, errs.NotImplemented("renewSSH: user certificate signing is not enabled")
}
signer = a.sshCAUserCertSignKey
case ssh.HostCert:
if a.sshCAHostCertSignKey == nil {
return nil, errs.NotImplemented("renewSSH: host certificate signing is not enabled")
return nil, prov, errs.NotImplemented("renewSSH: host certificate signing is not enabled")
}
signer = a.sshCAHostCertSignKey
default:
return nil, errs.InternalServer("renewSSH: unexpected ssh certificate type: %d", certTpl.CertType)
return nil, prov, errs.InternalServer("renewSSH: unexpected ssh certificate type: %d", certTpl.CertType)
}
// Sign certificate.
cert, err := sshutil.CreateCertificate(certTpl, signer)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate")
return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate")
}
if err = a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {
return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db")
if err := a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {
return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db")
}
return cert, nil
return cert, prov, nil
}
// RekeySSH creates a signed SSH certificate using the old SSH certificate as a template.
func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
var validators []provisioner.SSHCertValidator
cert, prov, err := a.rekeySSH(ctx, oldCert, pub, signOpts...)
a.meter.SSHRekeyed(prov, err)
return cert, err
}
func (a *Authority) rekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, provisioner.Interface, error) {
var prov provisioner.Interface
var validators []provisioner.SSHCertValidator
for _, op := range signOpts {
switch o := op.(type) {
// Capture current provisioner
@ -387,16 +405,16 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub
case provisioner.SSHCertValidator:
validators = append(validators, o)
default:
return nil, errs.InternalServer("rekeySSH; invalid extra option type %T", o)
return nil, prov, errs.InternalServer("rekeySSH; invalid extra option type %T", o)
}
}
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
return nil, errs.BadRequest("cannot rekey a certificate without validity period")
return nil, prov, errs.BadRequest("cannot rekey a certificate without validity period")
}
if err := a.authorizeSSHCertificate(ctx, oldCert); err != nil {
return nil, err
return nil, prov, err
}
backdate := a.config.AuthorityConfig.Backdate.Duration
@ -423,37 +441,37 @@ func (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub
switch cert.CertType {
case ssh.UserCert:
if a.sshCAUserCertSignKey == nil {
return nil, errs.NotImplemented("rekeySSH; user certificate signing is not enabled")
return nil, prov, errs.NotImplemented("rekeySSH; user certificate signing is not enabled")
}
signer = a.sshCAUserCertSignKey
case ssh.HostCert:
if a.sshCAHostCertSignKey == nil {
return nil, errs.NotImplemented("rekeySSH; host certificate signing is not enabled")
return nil, prov, errs.NotImplemented("rekeySSH; host certificate signing is not enabled")
}
signer = a.sshCAHostCertSignKey
default:
return nil, errs.BadRequest("unexpected certificate type '%d'", cert.CertType)
return nil, prov, errs.BadRequest("unexpected certificate type '%d'", cert.CertType)
}
var err error
// Sign certificate.
cert, err = sshutil.CreateCertificate(cert, signer)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate")
return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate")
}
// Apply validators from provisioner.
for _, v := range validators {
if err := v.Valid(cert, provisioner.SignSSHOptions{Backdate: backdate}); err != nil {
return nil, errs.ForbiddenErr(err, "error validating ssh certificate")
return nil, prov, errs.ForbiddenErr(err, "error validating ssh certificate")
}
}
if err = a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {
return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db")
if err := a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {
return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db")
}
return cert, nil
return cert, prov, nil
}
func (a *Authority) storeSSHCertificate(prov provisioner.Interface, cert *ssh.Certificate) error {
@ -653,28 +671,36 @@ func (a *Authority) getAddUserCommand(principal string) string {
return strings.ReplaceAll(cmd, "<principal>", principal)
}
func callEnrichingWebhooksSSH(ctx context.Context, webhookCtl webhookController, cr sshutil.CertificateRequest) error {
func (a *Authority) callEnrichingWebhooksSSH(ctx context.Context, prov provisioner.Interface, webhookCtl webhookController, cr sshutil.CertificateRequest) (err error) {
if webhookCtl == nil {
return nil
return
}
whEnrichReq, err := webhook.NewRequestBody(
var whEnrichReq *webhook.RequestBody
if whEnrichReq, err = webhook.NewRequestBody(
webhook.WithSSHCertificateRequest(cr),
)
if err != nil {
return err
); err == nil {
err = webhookCtl.Enrich(ctx, whEnrichReq)
a.meter.SSHWebhookEnriched(prov, err)
}
return webhookCtl.Enrich(ctx, whEnrichReq)
return
}
func callAuthorizingWebhooksSSH(ctx context.Context, webhookCtl webhookController, cert *sshutil.Certificate, certTpl *ssh.Certificate) error {
func (a *Authority) callAuthorizingWebhooksSSH(ctx context.Context, prov provisioner.Interface, webhookCtl webhookController, cert *sshutil.Certificate, certTpl *ssh.Certificate) (err error) {
if webhookCtl == nil {
return nil
return
}
whAuthBody, err := webhook.NewRequestBody(
var whAuthBody *webhook.RequestBody
if whAuthBody, err = webhook.NewRequestBody(
webhook.WithSSHCertificate(cert, certTpl),
)
if err != nil {
return err
); err == nil {
err = webhookCtl.Authorize(ctx, whAuthBody)
a.meter.SSHWebhookAuthorized(prov, err)
}
return webhookCtl.Authorize(ctx, whAuthBody)
return
}

@ -99,9 +99,15 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
return a.SignWithContext(context.Background(), csr, signOpts, extraOpts...)
}
// SignWithContext creates a signed certificate from a certificate signing request,
// taking the provided context.Context.
// SignWithContext creates a signed certificate from a certificate signing
// request, taking the provided context.Context.
func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
chain, prov, err := a.signX509(ctx, csr, signOpts, extraOpts...)
a.meter.X509Signed(prov, err)
return chain, err
}
func (a *Authority) signX509(ctx context.Context, csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, provisioner.Interface, error) {
var (
certOptions []x509util.Option
certValidators []provisioner.CertificateValidator
@ -109,9 +115,9 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
certEnforcers []provisioner.CertificateEnforcer
)
opts := []interface{}{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)}
opts := []any{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)}
if err := csr.CheckSignature(); err != nil {
return nil, errs.ApplyOptions(
return nil, nil, errs.ApplyOptions(
errs.BadRequestErr(err, "invalid certificate request"),
opts...,
)
@ -120,10 +126,12 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
// Set backdate with the configured value
signOpts.Backdate = a.config.AuthorityConfig.Backdate.Duration
var prov provisioner.Interface
var pInfo *casapi.ProvisionerInfo
var attData *provisioner.AttestationData
var webhookCtl webhookController
var (
prov provisioner.Interface
pInfo *casapi.ProvisionerInfo
attData *provisioner.AttestationData
webhookCtl webhookController
)
for _, op := range extraOpts {
switch k := op.(type) {
// Capture current provisioner
@ -141,8 +149,8 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
// Validate the given certificate request.
case provisioner.CertificateRequestValidator:
if err := k.Valid(csr); err != nil {
return nil, errs.ApplyOptions(
errs.ForbiddenErr(err, "error validating certificate"),
return nil, prov, errs.ApplyOptions(
errs.ForbiddenErr(err, "error validating certificate request"),
opts...,
)
}
@ -168,45 +176,46 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
webhookCtl = k
default:
return nil, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]interface{}{k}, opts...)...)
return nil, prov, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]any{k}, opts...)...)
}
}
if err := callEnrichingWebhooksX509(ctx, webhookCtl, attData, csr); err != nil {
return nil, errs.ApplyOptions(
if err := a.callEnrichingWebhooksX509(ctx, prov, webhookCtl, attData, csr); err != nil {
return nil, prov, errs.ApplyOptions(
errs.ForbiddenErr(err, err.Error()),
errs.WithKeyVal("csr", csr),
errs.WithKeyVal("signOptions", signOpts),
)
}
cert, err := x509util.NewCertificate(csr, certOptions...)
crt, err := x509util.NewCertificate(csr, certOptions...)
if err != nil {
var te *x509util.TemplateError
if errors.As(err, &te) {
return nil, errs.ApplyOptions(
switch {
case errors.As(err, &te):
return nil, prov, errs.ApplyOptions(
errs.BadRequestErr(err, err.Error()),
errs.WithKeyVal("csr", csr),
errs.WithKeyVal("signOptions", signOpts),
)
}
// explicitly check for unmarshaling errors, which are most probably caused by JSON template (syntax) errors
if strings.HasPrefix(err.Error(), "error unmarshaling certificate") {
return nil, errs.InternalServerErr(templatingError(err),
case strings.HasPrefix(err.Error(), "error unmarshaling certificate"):
// explicitly check for unmarshaling errors, which are most probably caused by JSON template (syntax) errors
return nil, prov, errs.InternalServerErr(templatingError(err),
errs.WithKeyVal("csr", csr),
errs.WithKeyVal("signOptions", signOpts),
errs.WithMessage("error applying certificate template"),
)
default:
return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign", opts...)
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign", opts...)
}
// Certificate modifiers before validation
leaf := cert.GetCertificate()
leaf := crt.GetCertificate()
// Set default subject
if err := withDefaultASN1DN(a.config.AuthorityConfig.Template).Modify(leaf, signOpts); err != nil {
return nil, errs.ApplyOptions(
return nil, prov, errs.ApplyOptions(
errs.ForbiddenErr(err, "error creating certificate"),
opts...,
)
@ -214,7 +223,7 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
for _, m := range certModifiers {
if err := m.Modify(leaf, signOpts); err != nil {
return nil, errs.ApplyOptions(
return nil, prov, errs.ApplyOptions(
errs.ForbiddenErr(err, "error creating certificate"),
opts...,
)
@ -224,7 +233,7 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
// Certificate validation.
for _, v := range certValidators {
if err := v.Valid(leaf, signOpts); err != nil {
return nil, errs.ApplyOptions(
return nil, prov, errs.ApplyOptions(
errs.ForbiddenErr(err, "error validating certificate"),
opts...,
)
@ -233,8 +242,8 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
// Certificate modifiers after validation
for _, m := range certEnforcers {
if err := m.Enforce(leaf); err != nil {
return nil, errs.ApplyOptions(
if err = m.Enforce(leaf); err != nil {
return nil, prov, errs.ApplyOptions(
errs.ForbiddenErr(err, "error creating certificate"),
opts...,
)
@ -243,8 +252,8 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
// Process injected modifiers after validation
for _, m := range a.x509Enforcers {
if err := m.Enforce(leaf); err != nil {
return nil, errs.ApplyOptions(
if err = m.Enforce(leaf); err != nil {
return nil, prov, errs.ApplyOptions(
errs.ForbiddenErr(err, "error creating certificate"),
opts...,
)
@ -252,12 +261,12 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
}
// Check if authority is allowed to sign the certificate
if err := a.isAllowedToSignX509Certificate(leaf); err != nil {
if err = a.isAllowedToSignX509Certificate(leaf); err != nil {
var ee *errs.Error
if errors.As(err, &ee) {
return nil, errs.ApplyOptions(ee, opts...)
return nil, prov, errs.ApplyOptions(ee, opts...)
}
return nil, errs.InternalServerErr(err,
return nil, prov, errs.InternalServerErr(err,
errs.WithKeyVal("csr", csr),
errs.WithKeyVal("signOptions", signOpts),
errs.WithMessage("error creating certificate"),
@ -265,8 +274,8 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
}
// Send certificate to webhooks for authorization
if err := callAuthorizingWebhooksX509(ctx, webhookCtl, cert, leaf, attData); err != nil {
return nil, errs.ApplyOptions(
if err := a.callAuthorizingWebhooksX509(ctx, prov, webhookCtl, crt, leaf, attData); err != nil {
return nil, prov, errs.ApplyOptions(
errs.ForbiddenErr(err, "error creating certificate"),
opts...,
)
@ -274,6 +283,7 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
// Sign certificate
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
Template: leaf,
CSR: csr,
@ -282,23 +292,22 @@ func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRe
Provisioner: pInfo,
})
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error creating certificate", opts...)
return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error creating certificate", opts...)
}
fullchain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...)
chain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...)
// Wrap provisioner with extra information.
prov = wrapProvisioner(prov, attData)
// Wrap provisioner with extra information, if not nil
if prov != nil {
prov = wrapProvisioner(prov, attData)
}
// Store certificate in the db.
if err = a.storeCertificate(prov, fullchain); err != nil {
if !errors.Is(err, db.ErrNotImplemented) {
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Sign; error storing certificate in db", opts...)
}
if err := a.storeCertificate(prov, chain); err != nil && !errors.Is(err, db.ErrNotImplemented) {
return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error storing certificate in db", opts...)
}
return fullchain, nil
return chain, prov, nil
}
// isAllowedToSignX509Certificate checks if the Authority is allowed
@ -346,14 +355,25 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5
// of rekey), and 'NotBefore/NotAfter' (the validity duration of the new
// certificate should be equal to the old one, but starting 'now').
func (a *Authority) RenewContext(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
chain, prov, err := a.renewContext(ctx, oldCert, pk)
if pk == nil {
a.meter.X509Renewed(prov, err)
} else {
a.meter.X509Rekeyed(prov, err)
}
return chain, err
}
func (a *Authority) renewContext(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, provisioner.Interface, error) {
isRekey := (pk != nil)
opts := []errs.Option{
errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String()),
}
// Check step provisioner extensions
if err := a.authorizeRenew(ctx, oldCert); err != nil {
return nil, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)
prov, err := a.authorizeRenew(ctx, oldCert)
if err != nil {
return nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)
}
// Durations
@ -423,15 +443,17 @@ func (a *Authority) RenewContext(ctx context.Context, oldCert *x509.Certificate,
//
// TODO(hslatman,maraino): consider adding policies too and consider if
// RenewSSH should check policies.
if err := a.constraintsEngine.ValidateCertificate(newCert); err != nil {
if err = a.constraintsEngine.ValidateCertificate(newCert); err != nil {
var ee *errs.Error
if errors.As(err, &ee) {
return nil, errs.StatusCodeError(ee.StatusCode(), err, opts...)
switch {
case errors.As(err, &ee):
return nil, prov, errs.StatusCodeError(ee.StatusCode(), err, opts...)
default:
return nil, prov, errs.InternalServerErr(err,
errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String()),
errs.WithMessage("error renewing certificate"),
)
}
return nil, errs.InternalServerErr(err,
errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String()),
errs.WithMessage("error renewing certificate"),
)
}
// The token can optionally be in the context. If the CA is running in RA
@ -445,17 +467,16 @@ func (a *Authority) RenewContext(ctx context.Context, oldCert *x509.Certificate,
Token: token,
})
if err != nil {
return nil, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)
return nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)
}
fullchain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...)
if err = a.storeRenewedCertificate(oldCert, fullchain); err != nil {
if !errors.Is(err, db.ErrNotImplemented) {
return nil, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)
}
chain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...)
if err = a.storeRenewedCertificate(oldCert, chain); err != nil && !errors.Is(err, db.ErrNotImplemented) {
return nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)
}
return fullchain, nil
return chain, prov, nil
}
// storeCertificate allows to use an extension of the db.AuthDB interface that
@ -684,9 +705,17 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn
return a.db.RevokeSSH(rci)
}
// CertificateRevocationListInfo contains a CRL in DER format and associated metadata.
type CertificateRevocationListInfo struct {
Number int64
ExpiresAt time.Time
Duration time.Duration
Data []byte
}
// GetCertificateRevocationList will return the currently generated CRL from the DB, or a not implemented
// error if the underlying AuthDB does not support CRLs
func (a *Authority) GetCertificateRevocationList() ([]byte, error) {
func (a *Authority) GetCertificateRevocationList() (*CertificateRevocationListInfo, error) {
if !a.config.CRL.IsEnabled() {
return nil, errs.Wrap(http.StatusNotFound, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList")
}
@ -701,7 +730,12 @@ func (a *Authority) GetCertificateRevocationList() ([]byte, error) {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetCertificateRevocationList")
}
return crlInfo.DER, nil
return &CertificateRevocationListInfo{
Number: crlInfo.Number,
ExpiresAt: crlInfo.ExpiresAt,
Duration: crlInfo.Duration,
Data: crlInfo.DER,
}, nil
}
// GenerateCertificateRevocationList generates a DER representation of a signed CRL and stores it in the
@ -961,42 +995,52 @@ func templatingError(err error) error {
return errors.Wrap(cause, "error applying certificate template")
}
func callEnrichingWebhooksX509(ctx context.Context, webhookCtl webhookController, attData *provisioner.AttestationData, csr *x509.CertificateRequest) error {
func (a *Authority) callEnrichingWebhooksX509(ctx context.Context, prov provisioner.Interface, webhookCtl webhookController, attData *provisioner.AttestationData, csr *x509.CertificateRequest) (err error) {
if webhookCtl == nil {
return nil
return
}
var attested *webhook.AttestationData
if attData != nil {
attested = &webhook.AttestationData{
PermanentIdentifier: attData.PermanentIdentifier,
}
}
whEnrichReq, err := webhook.NewRequestBody(
var whEnrichReq *webhook.RequestBody
if whEnrichReq, err = webhook.NewRequestBody(
webhook.WithX509CertificateRequest(csr),
webhook.WithAttestationData(attested),
)
if err != nil {
return err
); err == nil {
err = webhookCtl.Enrich(ctx, whEnrichReq)
a.meter.X509WebhookEnriched(prov, err)
}
return webhookCtl.Enrich(ctx, whEnrichReq)
return
}
func callAuthorizingWebhooksX509(ctx context.Context, webhookCtl webhookController, cert *x509util.Certificate, leaf *x509.Certificate, attData *provisioner.AttestationData) error {
func (a *Authority) callAuthorizingWebhooksX509(ctx context.Context, prov provisioner.Interface, webhookCtl webhookController, cert *x509util.Certificate, leaf *x509.Certificate, attData *provisioner.AttestationData) (err error) {
if webhookCtl == nil {
return nil
return
}
var attested *webhook.AttestationData
if attData != nil {
attested = &webhook.AttestationData{
PermanentIdentifier: attData.PermanentIdentifier,
}
}
whAuthBody, err := webhook.NewRequestBody(
var whAuthBody *webhook.RequestBody
if whAuthBody, err = webhook.NewRequestBody(
webhook.WithX509Certificate(cert, leaf),
webhook.WithAttestationData(attested),
)
if err != nil {
return err
); err == nil {
err = webhookCtl.Authorize(ctx, whAuthBody)
a.meter.X509WebhookAuthorized(prov, err)
}
return webhookCtl.Authorize(ctx, whAuthBody)
return
}

@ -24,7 +24,7 @@ import (
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x509util"
"github.com/smallstep/assert"
sassert "github.com/smallstep/assert"
"github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/policy"
@ -33,6 +33,8 @@ import (
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/nosql/database"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
@ -80,25 +82,25 @@ func generateCertificate(t *testing.T, commonName string, sans []string, opts ..
t.Helper()
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
assert.FatalError(t, err)
require.NoError(t, err)
cr, err := x509util.CreateCertificateRequest(commonName, sans, priv)
assert.FatalError(t, err)
require.NoError(t, err)
template, err := x509util.NewCertificate(cr)
assert.FatalError(t, err)
require.NoError(t, err)
cert := template.GetCertificate()
for _, m := range opts {
switch m := m.(type) {
case provisioner.CertificateModifierFunc:
err = m.Modify(cert, provisioner.SignOptions{})
assert.FatalError(t, err)
require.NoError(t, err)
case signerFunc:
cert, err = m(cert, priv.Public())
assert.FatalError(t, err)
require.NoError(t, err)
default:
t.Fatalf("unknown type %T", m)
require.Fail(t, "", "unknown type %T", m)
}
}
@ -108,36 +110,36 @@ func generateCertificate(t *testing.T, commonName string, sans []string, opts ..
func generateRootCertificate(t *testing.T) (*x509.Certificate, crypto.Signer) {
t.Helper()
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
assert.FatalError(t, err)
require.NoError(t, err)
cr, err := x509util.CreateCertificateRequest("TestRootCA", nil, priv)
assert.FatalError(t, err)
require.NoError(t, err)
data := x509util.CreateTemplateData("TestRootCA", nil)
template, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data))
assert.FatalError(t, err)
require.NoError(t, err)
cert := template.GetCertificate()
cert, err = x509util.CreateCertificate(cert, cert, priv.Public(), priv)
assert.FatalError(t, err)
require.NoError(t, err)
return cert, priv
}
func generateIntermidiateCertificate(t *testing.T, issuer *x509.Certificate, signer crypto.Signer) (*x509.Certificate, crypto.Signer) {
t.Helper()
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
assert.FatalError(t, err)
require.NoError(t, err)
cr, err := x509util.CreateCertificateRequest("TestIntermediateCA", nil, priv)
assert.FatalError(t, err)
require.NoError(t, err)
data := x509util.CreateTemplateData("TestIntermediateCA", nil)
template, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data))
assert.FatalError(t, err)
require.NoError(t, err)
cert := template.GetCertificate()
cert, err = x509util.CreateCertificate(cert, issuer, priv.Public(), signer)
assert.FatalError(t, err)
require.NoError(t, err)
return cert, priv
}
@ -192,9 +194,9 @@ func getCSR(t *testing.T, priv interface{}, opts ...func(*x509.CertificateReques
opt(_csr)
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv)
assert.FatalError(t, err)
require.NoError(t, err)
csr, err := x509.ParseCertificateRequest(csrBytes)
assert.FatalError(t, err)
require.NoError(t, err)
return csr
}
@ -239,10 +241,10 @@ func (e *testEnforcer) Enforce(cert *x509.Certificate) error {
func TestAuthority_Sign(t *testing.T) {
pub, priv, err := keyutil.GenerateDefaultKeyPair()
assert.FatalError(t, err)
require.NoError(t, err)
a := testAuthority(t)
assert.FatalError(t, err)
require.NoError(t, err)
a.config.AuthorityConfig.Template = &ASN1DN{
Country: "Tazmania",
Organization: "Acme Co",
@ -262,12 +264,12 @@ func TestAuthority_Sign(t *testing.T) {
// Create a token to get test extra opts.
p := a.config.AuthorityConfig.Provisioners[1].(*provisioner.JWK)
key, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
assert.FatalError(t, err)
require.NoError(t, err)
token, err := generateToken("smallstep test", "step-cli", testAudiences.Sign[0], []string{"test.smallstep.com"}, time.Now(), key)
assert.FatalError(t, err)
require.NoError(t, err)
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod)
extraOpts, err := a.Authorize(ctx, token)
assert.FatalError(t, err)
require.NoError(t, err)
type signTest struct {
auth *Authority
@ -372,9 +374,9 @@ W5kR63lNVHBHgQmv5mA8YFsfrJHstaz5k727v2LMHEYIf5/3i16d5zhuxUoaPTYr
ZYtQ9Ot36qc=
-----END CERTIFICATE REQUEST-----`
block, _ := pem.Decode([]byte(shortRSAKeyPEM))
assert.FatalError(t, err)
require.NoError(t, err)
csr, err := x509.ParseCertificateRequest(block.Bytes)
assert.FatalError(t, err)
require.NoError(t, err)
return &signTest{
auth: a,
@ -413,10 +415,10 @@ ZYtQ9Ot36qc=
X509: &provisioner.X509Options{Template: `{{ fail "fail message" }}`},
}
testExtraOpts, err := testAuthority.Authorize(ctx, token)
assert.FatalError(t, err)
require.NoError(t, err)
testAuthority.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -442,10 +444,10 @@ ZYtQ9Ot36qc=
},
}
testExtraOpts, err := testAuthority.Authorize(ctx, token)
assert.FatalError(t, err)
require.NoError(t, err)
testAuthority.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -471,10 +473,10 @@ ZYtQ9Ot36qc=
},
}
testExtraOpts, err := testAuthority.Authorize(ctx, token)
assert.FatalError(t, err)
require.NoError(t, err)
testAuthority.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -492,7 +494,7 @@ ZYtQ9Ot36qc=
aa := testAuthority(t)
aa.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -517,7 +519,7 @@ ZYtQ9Ot36qc=
}))
aa.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -537,7 +539,7 @@ ZYtQ9Ot36qc=
aa.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
fmt.Println(crt.Subject)
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -549,7 +551,7 @@ ZYtQ9Ot36qc=
},
}
engine, err := policy.New(options)
assert.FatalError(t, err)
require.NoError(t, err)
aa.policyEngine = engine
return &signTest{
auth: aa,
@ -598,7 +600,7 @@ ZYtQ9Ot36qc=
_a := testAuthority(t)
_a.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -617,7 +619,7 @@ ZYtQ9Ot36qc=
bcExt.Id = asn1.ObjectIdentifier{2, 5, 29, 19}
bcExt.Critical = false
bcExt.Value, err = asn1.Marshal(basicConstraints{IsCA: true, MaxPathLen: 4})
assert.FatalError(t, err)
require.NoError(t, err)
csr := getCSR(t, priv, setExtraExtsCSR([]pkix.Extension{
bcExt,
@ -632,7 +634,7 @@ ZYtQ9Ot36qc=
_a := testAuthority(t)
_a.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -663,10 +665,10 @@ ZYtQ9Ot36qc=
}`},
}
testExtraOpts, err := testAuthority.Authorize(ctx, token)
assert.FatalError(t, err)
require.NoError(t, err)
testAuthority.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -697,10 +699,10 @@ ZYtQ9Ot36qc=
}`},
}
testExtraOpts, err := testAuthority.Authorize(ctx, token)
assert.FatalError(t, err)
require.NoError(t, err)
testAuthority.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -737,7 +739,7 @@ ZYtQ9Ot36qc=
_a.config.AuthorityConfig.Template = &ASN1DN{}
_a.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject, pkix.Name{})
sassert.Equals(t, crt.Subject, pkix.Name{})
return nil
},
}
@ -762,8 +764,8 @@ ZYtQ9Ot36qc=
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
aa.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
assert.Equals(t, crt.CRLDistributionPoints, []string{"http://ca.example.org/leaf.crl"})
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.CRLDistributionPoints, []string{"http://ca.example.org/leaf.crl"})
return nil
},
}
@ -783,7 +785,7 @@ ZYtQ9Ot36qc=
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
aa.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
sassert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
}
@ -796,7 +798,7 @@ ZYtQ9Ot36qc=
},
}
engine, err := policy.New(options)
assert.FatalError(t, err)
require.NoError(t, err)
aa.policyEngine = engine
return &signTest{
auth: aa,
@ -816,13 +818,13 @@ ZYtQ9Ot36qc=
MStoreCertificateChain: func(prov provisioner.Interface, certs ...*x509.Certificate) error {
p, ok := prov.(attProvisioner)
if assert.True(t, ok) {
assert.Equals(t, &provisioner.AttestationData{
sassert.Equals(t, &provisioner.AttestationData{
PermanentIdentifier: "1234567890",
}, p.AttestationData())
}
if assert.Len(t, 2, certs) {
assert.Equals(t, certs[0].Subject.CommonName, "smallstep test")
assert.Equals(t, certs[1].Subject.CommonName, "smallstep Intermediate CA")
if assert.Len(t, certs, 2) {
sassert.Equals(t, certs[0].Subject.CommonName, "smallstep test")
sassert.Equals(t, certs[1].Subject.CommonName, "smallstep Intermediate CA")
}
return nil
},
@ -851,26 +853,26 @@ ZYtQ9Ot36qc=
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
assert.Nil(t, certChain)
var sc render.StatusCodedError
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
sassert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
sassert.Equals(t, sc.StatusCode(), tc.code)
sassert.HasPrefix(t, err.Error(), tc.err.Error())
var ctxErr *errs.Error
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
assert.Equals(t, ctxErr.Details["csr"], tc.csr)
assert.Equals(t, ctxErr.Details["signOptions"], tc.signOpts)
sassert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
sassert.Equals(t, ctxErr.Details["csr"], tc.csr)
sassert.Equals(t, ctxErr.Details["signOptions"], tc.signOpts)
}
} else {
leaf := certChain[0]
intermediate := certChain[1]
if assert.Nil(t, tc.err) {
assert.Equals(t, leaf.NotBefore, tc.notBefore)
assert.Equals(t, leaf.NotAfter, tc.notAfter)
sassert.Equals(t, leaf.NotBefore, tc.notBefore)
sassert.Equals(t, leaf.NotAfter, tc.notAfter)
tmplt := a.config.AuthorityConfig.Template
if tc.csr.Subject.CommonName == "" {
assert.Equals(t, leaf.Subject, pkix.Name{})
sassert.Equals(t, leaf.Subject, pkix.Name{})
} else {
assert.Equals(t, leaf.Subject.String(),
sassert.Equals(t, leaf.Subject.String(),
pkix.Name{
Country: []string{tmplt.Country},
Organization: []string{tmplt.Organization},
@ -879,18 +881,18 @@ ZYtQ9Ot36qc=
Province: []string{tmplt.Province},
CommonName: "smallstep test",
}.String())
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"})
sassert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"})
}
assert.Equals(t, leaf.Issuer, intermediate.Subject)
assert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)
assert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)
assert.Equals(t, leaf.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
sassert.Equals(t, leaf.Issuer, intermediate.Subject)
sassert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)
sassert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)
sassert.Equals(t, leaf.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
issuer := getDefaultIssuer(a)
subjectKeyID, err := generateSubjectKeyID(pub)
assert.FatalError(t, err)
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
require.NoError(t, err)
sassert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
sassert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
// Verify Provisioner OID
found := 0
@ -900,18 +902,18 @@ ZYtQ9Ot36qc=
found++
val := stepProvisionerASN1{}
_, err := asn1.Unmarshal(ext.Value, &val)
assert.FatalError(t, err)
assert.Equals(t, val.Type, provisionerTypeJWK)
assert.Equals(t, val.Name, []byte(p.Name))
assert.Equals(t, val.CredentialID, []byte(p.Key.KeyID))
require.NoError(t, err)
sassert.Equals(t, val.Type, provisionerTypeJWK)
sassert.Equals(t, val.Name, []byte(p.Name))
sassert.Equals(t, val.CredentialID, []byte(p.Key.KeyID))
// Basic Constraints
case ext.Id.Equal(asn1.ObjectIdentifier([]int{2, 5, 29, 19})):
val := basicConstraints{}
_, err := asn1.Unmarshal(ext.Value, &val)
assert.FatalError(t, err)
require.NoError(t, err)
assert.False(t, val.IsCA, false)
assert.Equals(t, val.MaxPathLen, 0)
sassert.Equals(t, val.MaxPathLen, 0)
// SAN extension
case ext.Id.Equal(asn1.ObjectIdentifier([]int{2, 5, 29, 17})):
@ -922,11 +924,11 @@ ZYtQ9Ot36qc=
}
}
}
assert.Equals(t, found, 1)
sassert.Equals(t, found, 1)
realIntermediate, err := x509.ParseCertificate(issuer.Raw)
assert.FatalError(t, err)
assert.Equals(t, intermediate, realIntermediate)
assert.Len(t, tc.extensionsCount, leaf.Extensions)
require.NoError(t, err)
sassert.Equals(t, intermediate, realIntermediate)
assert.Len(t, leaf.Extensions, tc.extensionsCount)
}
}
})
@ -1056,7 +1058,7 @@ func TestAuthority_Renew(t *testing.T) {
for name, genTestCase := range tests {
t.Run(name, func(t *testing.T) {
tc, err := genTestCase()
assert.FatalError(t, err)
require.NoError(t, err)
var certChain []*x509.Certificate
if tc.auth != nil {
@ -1068,19 +1070,19 @@ func TestAuthority_Renew(t *testing.T) {
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
assert.Nil(t, certChain)
var sc render.StatusCodedError
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
sassert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
sassert.Equals(t, sc.StatusCode(), tc.code)
sassert.HasPrefix(t, err.Error(), tc.err.Error())
var ctxErr *errs.Error
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String())
sassert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
sassert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String())
}
} else {
leaf := certChain[0]
intermediate := certChain[1]
if assert.Nil(t, tc.err) {
assert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.cert.NotAfter.Sub(cert.NotBefore))
sassert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.cert.NotAfter.Sub(cert.NotBefore))
assert.True(t, leaf.NotBefore.After(now.Add(-2*time.Minute)))
assert.True(t, leaf.NotBefore.Before(now.Add(time.Minute)))
@ -1090,30 +1092,30 @@ func TestAuthority_Renew(t *testing.T) {
assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour)))
tmplt := a.config.AuthorityConfig.Template
assert.Equals(t, leaf.RawSubject, tc.cert.RawSubject)
assert.Equals(t, leaf.Subject.Country, []string{tmplt.Country})
assert.Equals(t, leaf.Subject.Organization, []string{tmplt.Organization})
assert.Equals(t, leaf.Subject.Locality, []string{tmplt.Locality})
assert.Equals(t, leaf.Subject.StreetAddress, []string{tmplt.StreetAddress})
assert.Equals(t, leaf.Subject.Province, []string{tmplt.Province})
assert.Equals(t, leaf.Subject.CommonName, tmplt.CommonName)
assert.Equals(t, leaf.Issuer, intermediate.Subject)
assert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)
assert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)
assert.Equals(t, leaf.ExtKeyUsage,
sassert.Equals(t, leaf.RawSubject, tc.cert.RawSubject)
sassert.Equals(t, leaf.Subject.Country, []string{tmplt.Country})
sassert.Equals(t, leaf.Subject.Organization, []string{tmplt.Organization})
sassert.Equals(t, leaf.Subject.Locality, []string{tmplt.Locality})
sassert.Equals(t, leaf.Subject.StreetAddress, []string{tmplt.StreetAddress})
sassert.Equals(t, leaf.Subject.Province, []string{tmplt.Province})
sassert.Equals(t, leaf.Subject.CommonName, tmplt.CommonName)
sassert.Equals(t, leaf.Issuer, intermediate.Subject)
sassert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)
sassert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)
sassert.Equals(t, leaf.ExtKeyUsage,
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com", "test"})
sassert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com", "test"})
subjectKeyID, err := generateSubjectKeyID(leaf.PublicKey)
assert.FatalError(t, err)
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
require.NoError(t, err)
sassert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
// We did not change the intermediate before renewing.
authIssuer := getDefaultIssuer(tc.auth)
if issuer.SerialNumber == authIssuer.SerialNumber {
assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
sassert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
// Compare extensions: they can be in a different order
for _, ext1 := range tc.cert.Extensions {
//skip SubjectKeyIdentifier
@ -1133,7 +1135,7 @@ func TestAuthority_Renew(t *testing.T) {
}
} else {
// We did change the intermediate before renewing.
assert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId)
sassert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId)
// Compare extensions: they can be in a different order
for _, ext1 := range tc.cert.Extensions {
//skip SubjectKeyIdentifier
@ -1161,8 +1163,8 @@ func TestAuthority_Renew(t *testing.T) {
}
realIntermediate, err := x509.ParseCertificate(authIssuer.Raw)
assert.FatalError(t, err)
assert.Equals(t, intermediate, realIntermediate)
require.NoError(t, err)
sassert.Equals(t, intermediate, realIntermediate)
}
}
})
@ -1171,7 +1173,7 @@ func TestAuthority_Renew(t *testing.T) {
func TestAuthority_Rekey(t *testing.T) {
pub, _, err := keyutil.GenerateDefaultKeyPair()
assert.FatalError(t, err)
require.NoError(t, err)
a := testAuthority(t)
a.config.AuthorityConfig.Template = &ASN1DN{
@ -1261,7 +1263,7 @@ func TestAuthority_Rekey(t *testing.T) {
for name, genTestCase := range tests {
t.Run(name, func(t *testing.T) {
tc, err := genTestCase()
assert.FatalError(t, err)
require.NoError(t, err)
var certChain []*x509.Certificate
if tc.auth != nil {
@ -1273,19 +1275,19 @@ func TestAuthority_Rekey(t *testing.T) {
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
assert.Nil(t, certChain)
var sc render.StatusCodedError
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
sassert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
sassert.Equals(t, sc.StatusCode(), tc.code)
sassert.HasPrefix(t, err.Error(), tc.err.Error())
var ctxErr *errs.Error
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String())
sassert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
sassert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String())
}
} else {
leaf := certChain[0]
intermediate := certChain[1]
if assert.Nil(t, tc.err) {
assert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.cert.NotAfter.Sub(cert.NotBefore))
sassert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.cert.NotAfter.Sub(cert.NotBefore))
assert.True(t, leaf.NotBefore.After(now.Add(-2*time.Minute)))
assert.True(t, leaf.NotBefore.Before(now.Add(time.Minute)))
@ -1295,7 +1297,7 @@ func TestAuthority_Rekey(t *testing.T) {
assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour)))
tmplt := a.config.AuthorityConfig.Template
assert.Equals(t, leaf.Subject.String(),
sassert.Equals(t, leaf.Subject.String(),
pkix.Name{
Country: []string{tmplt.Country},
Organization: []string{tmplt.Organization},
@ -1304,32 +1306,32 @@ func TestAuthority_Rekey(t *testing.T) {
Province: []string{tmplt.Province},
CommonName: tmplt.CommonName,
}.String())
assert.Equals(t, leaf.Issuer, intermediate.Subject)
sassert.Equals(t, leaf.Issuer, intermediate.Subject)
assert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)
assert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)
assert.Equals(t, leaf.ExtKeyUsage,
sassert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)
sassert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)
sassert.Equals(t, leaf.ExtKeyUsage,
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com", "test"})
sassert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com", "test"})
// Test Public Key and SubjectKeyId
expectedPK := tc.pk
if tc.pk == nil {
expectedPK = cert.PublicKey
}
assert.Equals(t, leaf.PublicKey, expectedPK)
sassert.Equals(t, leaf.PublicKey, expectedPK)
subjectKeyID, err := generateSubjectKeyID(expectedPK)
assert.FatalError(t, err)
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
require.NoError(t, err)
sassert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
if tc.pk == nil {
assert.Equals(t, leaf.SubjectKeyId, cert.SubjectKeyId)
sassert.Equals(t, leaf.SubjectKeyId, cert.SubjectKeyId)
}
// We did not change the intermediate before renewing.
authIssuer := getDefaultIssuer(tc.auth)
if issuer.SerialNumber == authIssuer.SerialNumber {
assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
sassert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
// Compare extensions: they can be in a different order
for _, ext1 := range tc.cert.Extensions {
//skip SubjectKeyIdentifier
@ -1349,7 +1351,7 @@ func TestAuthority_Rekey(t *testing.T) {
}
} else {
// We did change the intermediate before renewing.
assert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId)
sassert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId)
// Compare extensions: they can be in a different order
for _, ext1 := range tc.cert.Extensions {
//skip SubjectKeyIdentifier
@ -1377,8 +1379,8 @@ func TestAuthority_Rekey(t *testing.T) {
}
realIntermediate, err := x509.ParseCertificate(authIssuer.Raw)
assert.FatalError(t, err)
assert.Equals(t, intermediate, realIntermediate)
require.NoError(t, err)
sassert.Equals(t, intermediate, realIntermediate)
}
}
})
@ -1413,10 +1415,10 @@ func TestAuthority_GetTLSOptions(t *testing.T) {
for name, genTestCase := range tests {
t.Run(name, func(t *testing.T) {
tc, err := genTestCase()
assert.FatalError(t, err)
require.NoError(t, err)
opts := tc.auth.GetTLSOptions()
assert.Equals(t, opts, tc.opts)
sassert.Equals(t, opts, tc.opts)
})
}
}
@ -1429,11 +1431,11 @@ func TestAuthority_Revoke(t *testing.T) {
now := time.Now().UTC()
jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
assert.FatalError(t, err)
require.NoError(t, err)
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
assert.FatalError(t, err)
require.NoError(t, err)
a := testAuthority(t)
@ -1472,7 +1474,7 @@ func TestAuthority_Revoke(t *testing.T) {
ID: "44",
}
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
require.NoError(t, err)
return test{
auth: a,
@ -1486,9 +1488,9 @@ func TestAuthority_Revoke(t *testing.T) {
err: errors.New("authority.Revoke; no persistence layer configured"),
code: http.StatusNotImplemented,
checkErrDetails: func(err *errs.Error) {
assert.Equals(t, err.Details["token"], raw)
assert.Equals(t, err.Details["tokenID"], "44")
assert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
sassert.Equals(t, err.Details["token"], raw)
sassert.Equals(t, err.Details["tokenID"], "44")
sassert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
},
}
},
@ -1512,7 +1514,7 @@ func TestAuthority_Revoke(t *testing.T) {
ID: "44",
}
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
require.NoError(t, err)
return test{
auth: _a,
@ -1526,9 +1528,9 @@ func TestAuthority_Revoke(t *testing.T) {
err: errors.New("authority.Revoke: force"),
code: http.StatusInternalServerError,
checkErrDetails: func(err *errs.Error) {
assert.Equals(t, err.Details["token"], raw)
assert.Equals(t, err.Details["tokenID"], "44")
assert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
sassert.Equals(t, err.Details["token"], raw)
sassert.Equals(t, err.Details["tokenID"], "44")
sassert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
},
}
},
@ -1552,7 +1554,7 @@ func TestAuthority_Revoke(t *testing.T) {
ID: "44",
}
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
require.NoError(t, err)
return test{
auth: _a,
@ -1566,9 +1568,9 @@ func TestAuthority_Revoke(t *testing.T) {
err: errors.New("certificate with serial number 'sn' is already revoked"),
code: http.StatusBadRequest,
checkErrDetails: func(err *errs.Error) {
assert.Equals(t, err.Details["token"], raw)
assert.Equals(t, err.Details["tokenID"], "44")
assert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
sassert.Equals(t, err.Details["token"], raw)
sassert.Equals(t, err.Details["tokenID"], "44")
sassert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
},
}
},
@ -1591,7 +1593,7 @@ func TestAuthority_Revoke(t *testing.T) {
ID: "44",
}
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
require.NoError(t, err)
return test{
auth: _a,
ctx: tlsRevokeCtx,
@ -1607,7 +1609,7 @@ func TestAuthority_Revoke(t *testing.T) {
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
assert.FatalError(t, err)
require.NoError(t, err)
return test{
auth: _a,
@ -1625,7 +1627,7 @@ func TestAuthority_Revoke(t *testing.T) {
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
assert.FatalError(t, err)
require.NoError(t, err)
// Filter out provisioner extension.
for i, ext := range crt.Extensions {
if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}) {
@ -1650,7 +1652,7 @@ func TestAuthority_Revoke(t *testing.T) {
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
assert.FatalError(t, err)
require.NoError(t, err)
return test{
auth: _a,
@ -1683,7 +1685,7 @@ func TestAuthority_Revoke(t *testing.T) {
ID: "44",
}
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
require.NoError(t, err)
return test{
auth: a,
ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod),
@ -1702,17 +1704,17 @@ func TestAuthority_Revoke(t *testing.T) {
if err := tc.auth.Revoke(tc.ctx, tc.opts); err != nil {
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
var sc render.StatusCodedError
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
sassert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
sassert.Equals(t, sc.StatusCode(), tc.code)
sassert.HasPrefix(t, err.Error(), tc.err.Error())
var ctxErr *errs.Error
assert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
assert.Equals(t, ctxErr.Details["serialNumber"], tc.opts.Serial)
assert.Equals(t, ctxErr.Details["reasonCode"], tc.opts.ReasonCode)
assert.Equals(t, ctxErr.Details["reason"], tc.opts.Reason)
assert.Equals(t, ctxErr.Details["MTLS"], tc.opts.MTLS)
assert.Equals(t, ctxErr.Details["context"], provisioner.RevokeMethod.String())
sassert.Fatal(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
sassert.Equals(t, ctxErr.Details["serialNumber"], tc.opts.Serial)
sassert.Equals(t, ctxErr.Details["reasonCode"], tc.opts.ReasonCode)
sassert.Equals(t, ctxErr.Details["reason"], tc.opts.Reason)
sassert.Equals(t, ctxErr.Details["MTLS"], tc.opts.MTLS)
sassert.Equals(t, ctxErr.Details["context"], provisioner.RevokeMethod.String())
if tc.checkErrDetails != nil {
tc.checkErrDetails(ctxErr)
@ -1814,13 +1816,11 @@ func TestAuthority_CRL(t *testing.T) {
validIssuer := "step-cli"
validAudience := testAudiences.Revoke
now := time.Now().UTC()
//
jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
assert.FatalError(t, err)
//
require.NoError(t, err)
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
assert.FatalError(t, err)
require.NoError(t, err)
crlCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)
@ -1865,7 +1865,7 @@ func TestAuthority_CRL(t *testing.T) {
auth: a,
ctx: crlCtx,
expected: nil,
err: database.ErrNotFound,
err: errors.New("authority.GetCertificateRevocationList: not found"),
}
},
"ok/crl-full": func() test {
@ -1910,7 +1910,7 @@ func TestAuthority_CRL(t *testing.T) {
ID: sn,
}
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
require.NoError(t, err)
err = a.Revoke(crlCtx, &RevokeOptions{
Serial: sn,
ReasonCode: reasonCode,
@ -1918,7 +1918,7 @@ func TestAuthority_CRL(t *testing.T) {
OTT: raw,
})
assert.FatalError(t, err)
require.NoError(t, err)
ex = append(ex, sn)
}
@ -1933,22 +1933,22 @@ func TestAuthority_CRL(t *testing.T) {
for name, f := range tests {
tc := f()
t.Run(name, func(t *testing.T) {
if crlBytes, err := tc.auth.GetCertificateRevocationList(); err == nil {
crl, parseErr := x509.ParseRevocationList(crlBytes)
if parseErr != nil {
t.Errorf("x509.ParseCertificateRequest() error = %v, wantErr %v", parseErr, nil)
return
}
crlInfo, err := tc.auth.GetCertificateRevocationList()
if tc.err != nil {
assert.EqualError(t, err, tc.err.Error())
assert.Nil(t, crlInfo)
return
}
var cmpList []string
for _, c := range crl.RevokedCertificates {
cmpList = append(cmpList, c.SerialNumber.String())
}
crl, parseErr := x509.ParseRevocationList(crlInfo.Data)
require.NoError(t, parseErr)
assert.Equals(t, cmpList, tc.expected)
} else {
assert.NotNil(t, tc.err, err.Error())
var cmpList []string
for _, c := range crl.RevokedCertificateEntries {
cmpList = append(cmpList, c.SerialNumber.String())
}
assert.Equal(t, tc.expected, cmpList)
})
}
}

@ -176,7 +176,7 @@ func (c *ACMEClient) post(payload []byte, url string, headerOps ...withHeaderOpt
}
signed, err := signer.Sign(payload)
if err != nil {
return nil, errors.Errorf("error signing payload: %s", strings.TrimPrefix(err.Error(), "square/go-jose: "))
return nil, errors.Errorf("error signing payload: %s", jose.TrimPrefix(err))
}
raw, err := serialize(signed)
if err != nil {

@ -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"
@ -26,7 +26,9 @@ import (
"github.com/smallstep/certificates/authority/admin"
adminAPI "github.com/smallstep/certificates/authority/admin/api"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/internal/metrix"
"github.com/smallstep/certificates/logging"
"github.com/smallstep/certificates/monitoring"
"github.com/smallstep/certificates/scep"
@ -46,6 +48,8 @@ type options struct {
sshHostPassword []byte
sshUserPassword []byte
database db.AuthDB
x509CAService apiv1.CertificateAuthorityService
tlsConfig *tls.Config
}
func (o *options) apply(opts []Option) {
@ -65,6 +69,13 @@ func WithConfigFile(name string) Option {
}
}
// WithX509CAService provides the x509CAService to be used for signing x509 requests
func WithX509CAService(svc apiv1.CertificateAuthorityService) Option {
return func(o *options) {
o.x509CAService = svc
}
}
// WithPassword sets the given password as the configured password in the CA
// options.
func WithPassword(password []byte) Option {
@ -104,6 +115,14 @@ func WithDatabase(d db.AuthDB) Option {
}
}
// WithTLSConfig sets the TLS configuration to be used by the HTTP(s) server
// spun by step-ca.
func WithTLSConfig(t *tls.Config) Option {
return func(o *options) {
o.tlsConfig = t
}
}
// WithLinkedCAToken sets the token used to authenticate with the linkedca.
func WithLinkedCAToken(token string) Option {
return func(o *options) {
@ -125,6 +144,7 @@ type CA struct {
config *config.Config
srv *server.Server
insecureSrv *server.Server
metricsSrv *server.Server
opts *options
renewer *TLSRenewer
compactStop chan struct{}
@ -163,6 +183,16 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
opts = append(opts, authority.WithQuietInit())
}
if ca.opts.x509CAService != nil {
opts = append(opts, authority.WithX509CAService(ca.opts.x509CAService))
}
var meter *metrix.Meter
if ca.config.MetricsAddress != "" {
meter = metrix.New()
opts = append(opts, authority.WithMeter(meter))
}
webhookTransport := http.DefaultTransport.(*http.Transport).Clone()
opts = append(opts, authority.WithWebhookClient(&http.Client{Transport: webhookTransport}))
@ -172,9 +202,20 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
}
ca.auth = auth
tlsConfig, clientTLSConfig, err := ca.getTLSConfig(auth)
if err != nil {
return nil, err
var tlsConfig *tls.Config
var clientTLSConfig *tls.Config
if ca.opts.tlsConfig != nil {
// try using the tls Configuration supplied by the caller
log.Print("Using tls configuration supplied by the application")
tlsConfig = ca.opts.tlsConfig
clientTLSConfig = ca.opts.tlsConfig
} else {
// default to using the step-ca x509 Signer Interface
log.Print("Building new tls configuration using step-ca x509 Signer Interface")
tlsConfig, clientTLSConfig, err = ca.getTLSConfig(auth)
if err != nil {
return nil, err
}
}
webhookTransport.TLSClientConfig = clientTLSConfig
@ -250,19 +291,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)
})
@ -323,6 +359,13 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
}
}
if meter != nil {
ca.metricsSrv = server.New(ca.config.MetricsAddress, meter, nil)
ca.metricsSrv.BaseContext = func(net.Listener) context.Context {
return baseContext
}
}
return ca, nil
}
@ -409,6 +452,14 @@ func (ca *CA) Run() error {
}()
}
if ca.metricsSrv != nil {
wg.Add(1)
go func() {
defer wg.Done()
errs <- ca.metricsSrv.ListenAndServe()
}()
}
wg.Add(1)
go func() {
defer wg.Done()
@ -426,7 +477,10 @@ func (ca *CA) Run() error {
// Stop stops the CA calling to the server Shutdown method.
func (ca *CA) Stop() error {
close(ca.compactStop)
ca.renewer.Stop()
if ca.renewer != nil {
ca.renewer.Stop()
}
if err := ca.auth.Shutdown(); err != nil {
log.Printf("error stopping ca.Authority: %+v\n", err)
}
@ -485,6 +539,13 @@ func (ca *CA) Reload() error {
}
}
if ca.metricsSrv != nil {
if err = ca.metricsSrv.Reload(newCA.metricsSrv); err != nil {
logContinue("Reload failed because metrics server could not be replaced.")
return errors.Wrap(err, "error reloading metrics server")
}
}
if err = ca.srv.Reload(newCA.srv); err != nil {
logContinue("Reload failed because server could not be replaced.")
return errors.Wrap(err, "error reloading server")
@ -494,7 +555,10 @@ func (ca *CA) Reload() error {
// 2. Safely shutdown any internal resources (e.g. key manager)
// 3. Replace ca properties
// Do not replace ca.srv
ca.renewer.Stop()
if ca.renewer != nil {
ca.renewer.Stop()
}
ca.auth.CloseForReload()
ca.auth = newCA.auth
ca.config = newCA.config
@ -584,10 +648,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

@ -37,7 +37,6 @@ import (
"golang.org/x/net/http2"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"gopkg.in/square/go-jose.v2/jwt"
)
// DisableIdentity is a global variable to disable the identity.
@ -1374,7 +1373,7 @@ func (c *Client) RootFingerprintWithContext(ctx context.Context) (string, error)
// CreateSignRequest is a helper function that given an x509 OTT returns a
// simple but secure sign request as well as the private key used.
func CreateSignRequest(ott string) (*api.SignRequest, crypto.PrivateKey, error) {
token, err := jwt.ParseSigned(ott)
token, err := jose.ParseSigned(ott)
if err != nil {
return nil, nil, errors.Wrap(err, "error parsing ott")
}

@ -53,6 +53,8 @@ const (
StepCAS = "stepcas"
// VaultCAS is a CertificateAuthorityService using Hasicorp Vault PKI.
VaultCAS = "vaultcas"
// ExternalCAS is a CertificateAuthorityService using an external injected CA implementation
ExternalCAS = "externalcas"
)
// String returns a string from the type. It will always return the lower case

@ -13,6 +13,7 @@ func TestType_String(t *testing.T) {
{"default", "", "softcas"},
{"SoftCAS", SoftCAS, "softcas"},
{"CloudCAS", CloudCAS, "cloudcas"},
{"ExternalCAS", ExternalCAS, "externalcas"},
{"UnknownCAS", "UnknownCAS", "unknowncas"},
}
for _, tt := range tests {

@ -33,10 +33,10 @@ func Test_jwkIssuer_SignToken(t *testing.T) {
RA *raInfo `json:"ra"`
}
type claims struct {
Aud []string `json:"aud"`
Sub string `json:"sub"`
Sans []string `json:"sans"`
Step stepClaims `json:"step"`
Aud jose.Audience `json:"aud"`
Sub string `json:"sub"`
Sans []string `json:"sans"`
Step stepClaims `json:"step"`
}
tests := []struct {
name string
@ -72,7 +72,7 @@ func Test_jwkIssuer_SignToken(t *testing.T) {
}
var c claims
want := claims{
Aud: []string{tt.fields.caURL.String() + "/1.0/sign"},
Aud: jose.Audience{tt.fields.caURL.String() + "/1.0/sign"},
Sub: tt.args.subject,
Sans: tt.args.sans,
}
@ -80,6 +80,7 @@ func Test_jwkIssuer_SignToken(t *testing.T) {
want.Step.RA = tt.args.info
}
if err := jwt.Claims(testX5CKey.Public(), &c); err != nil {
t.Log(got)
t.Errorf("jwt.Claims() error = %v", err)
}
if !reflect.DeepEqual(c, want) {
@ -109,9 +110,9 @@ func Test_jwkIssuer_RevokeToken(t *testing.T) {
subject string
}
type claims struct {
Aud []string `json:"aud"`
Sub string `json:"sub"`
Sans []string `json:"sans"`
Aud jose.Audience `json:"aud"`
Sub string `json:"sub"`
Sans []string `json:"sans"`
}
tests := []struct {
name string

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

@ -58,10 +58,10 @@ func Test_x5cIssuer_SignToken(t *testing.T) {
RA *raInfo `json:"ra"`
}
type claims struct {
Aud []string `json:"aud"`
Sub string `json:"sub"`
Sans []string `json:"sans"`
Step stepClaims `json:"step"`
Aud jose.Audience `json:"aud"`
Sub string `json:"sub"`
Sans []string `json:"sans"`
Step stepClaims `json:"step"`
}
tests := []struct {
name string
@ -132,9 +132,9 @@ func Test_x5cIssuer_RevokeToken(t *testing.T) {
subject string
}
type claims struct {
Aud []string `json:"aud"`
Sub string `json:"sub"`
Sans []string `json:"sans"`
Aud jose.Audience `json:"aud"`
Sub string `json:"sub"`
Sans []string `json:"sans"`
}
tests := []struct {
name string

@ -251,7 +251,8 @@ To get a linked authority token:
ca.WithSSHUserPassword(sshUserPassword),
ca.WithIssuerPassword(issuerPassword),
ca.WithLinkedCAToken(token),
ca.WithQuiet(quiet))
ca.WithQuiet(quiet),
)
if err != nil {
fatal(err)
}

@ -78,11 +78,14 @@ func FromContext(ctx context.Context) (db AuthDB, ok bool) {
// MustFromContext returns the current database from the given context. It
// will panic if it's not in the context.
func MustFromContext(ctx context.Context) AuthDB {
if db, ok := FromContext(ctx); !ok {
var (
db AuthDB
ok bool
)
if db, ok = FromContext(ctx); !ok {
panic("authority database is not in the context")
} else {
return db
}
return db
}
// CertificateStorer is an extension of AuthDB that allows to store

@ -1,4 +1,4 @@
FROM golang AS builder
FROM golang:bookworm AS builder
WORKDIR /src
COPY . .
@ -9,9 +9,9 @@ RUN apt-get install -y --no-install-recommends \
RUN make V=1 GO_ENVS="CGO_ENABLED=1" bin/step-ca
RUN setcap CAP_NET_BIND_SERVICE=+eip bin/step-ca
FROM smallstep/step-kms-plugin:bullseye AS kms
FROM smallstep/step-kms-plugin:bookworm AS kms
FROM smallstep/step-cli:bullseye
FROM smallstep/step-cli:bookworm
COPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca
COPY --from=kms /usr/local/bin/step-kms-plugin /usr/local/bin/step-kms-plugin

@ -21,7 +21,7 @@ the token does contain the root fingerprint then it is simpler to use:
client, err := ca.Bootstrap(token)
```
After the initialization there are examples of all the client methods. These
After the initialization, there are examples of all the client methods. These
methods are a convenient way to use the CA API. The first method, `Health`,
returns the status of the CA server. If the server is up it will return
`{"status":"ok"}`.
@ -77,7 +77,7 @@ if err != nil { ... }
```
The following methods are for inpsecting Provisioners.
One method that returns a list of provisioners or a the encrypted key of one provisioner.
One method that returns a list of provisioners or an encrypted key of one provisioner.
```go
// Without options it will return the first 20 provisioners.
@ -98,7 +98,7 @@ key, err := client.ProvisionerKey("DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk")
```
The following example shows how to create a
tls.Config object that can be injected into servers and clients. By default these
tls.Config object that can be injected into servers and clients. By default, these
methods will spin off Go routines that auto-renew a certificate once (approximately)
two thirds of the duration of the certificate has passed.
@ -184,7 +184,7 @@ resp, err := client.Get("https://localhost:8443")
```
We will demonstrate the mTLS configuration in a different example. In this
examplefor we will configure the server to only verify client certificates
example we will configure the server to only verify client certificates
if they are provided.
To being with let's start the Step CA:
@ -226,7 +226,7 @@ If you'd like to turn off curl's verification of the certificate, use
HTTPS-proxy has similar options --proxy-cacert and --proxy-insecure.
```
Now lets use the root certificate generated for the Step PKI. It should work.
Now let's use the root certificate generated for the Step PKI. It should work.
```sh
certificates $ curl --cacert examples/pki/secrets/root_ca.crt https://localhost:8443
@ -236,7 +236,7 @@ Hello nobody at 2018-11-03 01:49:25.66912 +0000 UTC!!!
Notice that in the response we see `nobody`. This is because the server did not
detected a TLS client configuration.
But if we create a client with it's own certificate (generated by the Step CA),
But if we create a client with its own certificate (generated by the Step CA),
we should see the Common Name of the client certificate:
```sh
@ -304,7 +304,7 @@ We can use the bootstrap-server to demonstrate certificate rotation. We've
added a second provisioner, named `mike@smallstep.com`, to the CA configuration.
This provisioner is has a default certificate duration of 2 minutes.
Let's run the server, and inspect the certificate. We can should be able to
see the certificate rotate once approximately 2/3rds of it's lifespan has passed.
see the certificate rotate once approximately 2/3rds of its lifespan has passed.
```sh
certificates $ export STEPPATH=examples/pki
@ -320,7 +320,7 @@ The exact formula is `<duration>-<duration>/3-rand(<duration>/20)` (`duration=12
in our example).
We can use the following command to check the certificate expiration and to make
sure the certificate changes after 74-80 seconds.
sure the certificate changes after 74-80 seconds.
```sh
certificates $ step certificate inspect --insecure https://localhost:8443

@ -7,12 +7,12 @@ sleep 5
rm -f /var/local/step/root_ca.crt
rm -f /var/local/step/site.crt /var/local/step/site.key
# Donwload the root certificate
# Download the root certificate
step ca root /var/local/step/root_ca.crt
# Get token
STEP_TOKEN=$(step ca token $COMMON_NAME)
# Donwload the root certificate
# Download the root certificate
step ca certificate --token $STEP_TOKEN $COMMON_NAME /var/local/step/site.crt /var/local/step/site.key
exec "$@"
exec "$@"

137
go.mod

@ -3,63 +3,78 @@ module github.com/smallstep/certificates
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.5
cloud.google.com/go/security v1.15.5
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.5.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/chi/v5 v5.0.11
github.com/go-jose/go-jose/v3 v3.0.1
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.1
github.com/google/go-cmp v0.6.0
github.com/google/go-tpm v0.9.0
github.com/google/uuid v1.6.0
github.com/googleapis/gax-go/v2 v2.12.0
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/micromdm/scep/v2 v2.1.0
github.com/newrelic/go-agent/v3 v3.24.1
github.com/hashicorp/vault/api v1.12.0
github.com/hashicorp/vault/api/auth/approle v0.6.0
github.com/hashicorp/vault/api/auth/kubernetes v0.6.0
github.com/newrelic/go-agent/v3 v3.30.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.18.0
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.20240109183208-413678f90935
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.35.0
go.step.sm/linkedca v0.20.0
golang.org/x/crypto v0.13.0
go.step.sm/crypto v0.43.1
go.step.sm/linkedca v0.20.1
golang.org/x/crypto v0.19.0
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0
golang.org/x/net v0.15.0
google.golang.org/api v0.141.0
google.golang.org/grpc v1.58.1
google.golang.org/protobuf v1.31.0
gopkg.in/square/go-jose.v2 v2.6.0
golang.org/x/net v0.21.0
google.golang.org/api v0.165.0
google.golang.org/grpc v1.61.0
google.golang.org/protobuf v1.32.0
)
require (
cloud.google.com/go v0.110.6 // indirect
cloud.google.com/go/compute v1.23.0 // indirect
cloud.google.com/go v0.112.0 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.1 // indirect
cloud.google.com/go/kms v1.15.1 // indirect
filippo.io/edwards25519 v1.0.0 // indirect
cloud.google.com/go/iam v1.1.5 // indirect
cloud.google.com/go/kms v1.15.6 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 // 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.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // 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.2.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.318 // indirect
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.27.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/beorn7/perks v1.0.1 // 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 +83,26 @@ 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/felixge/httpsnoop v1.0.4 // 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-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // 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.2.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.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // 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
@ -104,12 +122,12 @@ require (
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
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/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
@ -117,8 +135,11 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/peterbourgon/diskv/v3 v3.0.1 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/schollz/jsonstore v1.1.0 // indirect
@ -129,17 +150,19 @@ 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.12.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.1.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230911183012-2d3300fd4832 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
go.opentelemetry.io/otel v1.23.0 // indirect
go.opentelemetry.io/otel/metric v1.23.0 // indirect
go.opentelemetry.io/otel/trace v1.23.0 // indirect
golang.org/x/oauth2 v0.17.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
// use github.com/smallstep/pkcs7 fork with patches applied
replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948

1357
go.sum

File diff suppressed because it is too large Load Diff

@ -0,0 +1,196 @@
// Package metrix implements stats-related functionality.
package metrix
import (
"net/http"
"strconv"
"time"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// New initializes and returns a new [Meter].
func New() (m *Meter) {
initializedAt := time.Now()
m = &Meter{
uptime: prometheus.NewGaugeFunc(
prometheus.GaugeOpts(opts(
"",
"uptime_seconds",
"Number of seconds since service start",
)),
func() float64 {
return float64(time.Since(initializedAt) / time.Second)
},
),
ssh: newProvisionerInstruments("ssh"),
x509: newProvisionerInstruments("x509"),
kms: &kms{
signed: prometheus.NewCounter(prometheus.CounterOpts(opts("kms", "signed", "Number of KMS-backed signatures"))),
errors: prometheus.NewCounter(prometheus.CounterOpts(opts("kms", "errors", "Number of KMS-related errors"))),
},
}
reg := prometheus.NewRegistry()
reg.MustRegister(
m.uptime,
m.ssh.rekeyed,
m.ssh.renewed,
m.ssh.signed,
m.x509.rekeyed,
m.x509.renewed,
m.x509.signed,
m.kms.signed,
m.kms.errors,
)
h := promhttp.HandlerFor(reg, promhttp.HandlerOpts{
Registry: reg,
Timeout: 5 * time.Second,
MaxRequestsInFlight: 10,
})
mux := http.NewServeMux()
mux.Handle("/metrics", h)
m.Handler = mux
return
}
// Meter wraps the functionality of a Prometheus-compatible HTTP handler.
type Meter struct {
http.Handler
uptime prometheus.GaugeFunc
ssh *provisionerInstruments
x509 *provisionerInstruments
kms *kms
}
// SSHRekeyed implements [authority.Meter] for [Meter].
func (m *Meter) SSHRekeyed(p provisioner.Interface, err error) {
incrProvisionerCounter(m.ssh.rekeyed, p, err)
}
// SSHRenewed implements [authority.Meter] for [Meter].
func (m *Meter) SSHRenewed(p provisioner.Interface, err error) {
incrProvisionerCounter(m.ssh.renewed, p, err)
}
// SSHSigned implements [authority.Meter] for [Meter].
func (m *Meter) SSHSigned(p provisioner.Interface, err error) {
incrProvisionerCounter(m.ssh.signed, p, err)
}
// SSHAuthorized implements [authority.Meter] for [Meter].
func (m *Meter) SSHWebhookAuthorized(p provisioner.Interface, err error) {
incrProvisionerCounter(m.ssh.webhookAuthorized, p, err)
}
// SSHEnriched implements [authority.Meter] for [Meter].
func (m *Meter) SSHWebhookEnriched(p provisioner.Interface, err error) {
incrProvisionerCounter(m.ssh.webhookEnriched, p, err)
}
// X509Rekeyed implements [authority.Meter] for [Meter].
func (m *Meter) X509Rekeyed(p provisioner.Interface, err error) {
incrProvisionerCounter(m.x509.rekeyed, p, err)
}
// X509Renewed implements [authority.Meter] for [Meter].
func (m *Meter) X509Renewed(p provisioner.Interface, err error) {
incrProvisionerCounter(m.x509.renewed, p, err)
}
// X509Signed implements [authority.Meter] for [Meter].
func (m *Meter) X509Signed(p provisioner.Interface, err error) {
incrProvisionerCounter(m.x509.signed, p, err)
}
// X509Authorized implements [authority.Meter] for [Meter].
func (m *Meter) X509WebhookAuthorized(p provisioner.Interface, err error) {
incrProvisionerCounter(m.x509.webhookAuthorized, p, err)
}
// X509Enriched implements [authority.Meter] for [Meter].
func (m *Meter) X509WebhookEnriched(p provisioner.Interface, err error) {
incrProvisionerCounter(m.x509.webhookEnriched, p, err)
}
func incrProvisionerCounter(cv *prometheus.CounterVec, p provisioner.Interface, err error) {
var name string
if p != nil {
name = p.GetName()
}
cv.WithLabelValues(name, strconv.FormatBool(err == nil)).Inc()
}
// KMSSigned implements [authority.Meter] for [Meter].
func (m *Meter) KMSSigned(err error) {
if err == nil {
m.kms.signed.Inc()
} else {
m.kms.errors.Inc()
}
}
// provisionerInstruments wraps the counters exported by provisioners.
type provisionerInstruments struct {
rekeyed *prometheus.CounterVec
renewed *prometheus.CounterVec
signed *prometheus.CounterVec
webhookAuthorized *prometheus.CounterVec
webhookEnriched *prometheus.CounterVec
}
func newProvisionerInstruments(subsystem string) *provisionerInstruments {
return &provisionerInstruments{
rekeyed: newCounterVec(subsystem, "rekeyed_total", "Number of certificates rekeyed",
"provisioner",
"success",
),
renewed: newCounterVec(subsystem, "renewed_total", "Number of certificates renewed",
"provisioner",
"success",
),
signed: newCounterVec(subsystem, "signed_total", "Number of certificates signed",
"provisioner",
"success",
),
webhookAuthorized: newCounterVec(subsystem, "webhook_authorized_total", "Number of authorizing webhooks called",
"provisioner",
"success",
),
webhookEnriched: newCounterVec(subsystem, "webhook_enriched_total", "Number of enriching webhooks called",
"provisioner",
"success",
),
}
}
type kms struct {
signed prometheus.Counter
errors prometheus.Counter
}
func newCounterVec(subsystem, name, help string, labels ...string) *prometheus.CounterVec {
opts := opts(subsystem, name, help)
return prometheus.NewCounterVec(prometheus.CounterOpts(opts), labels)
}
func opts(subsystem, name, help string) prometheus.Opts {
return prometheus.Opts{
Namespace: "step_ca",
Subsystem: subsystem,
Name: name,
Help: help,
}
}

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

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

@ -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,6 +33,22 @@ 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{
@ -37,6 +65,14 @@ func Test_decodeRequest(t *testing.T) {
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,
},
{
name: "fail/get-PKIOperation",
args: args{
@ -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)
})
}
}

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

Loading…
Cancel
Save