Compare commits

..

No commits in common. 'master' and 'v0.25.3-rc7' have entirely different histories.

@ -4,6 +4,6 @@ contact_links:
url: https://discord.gg/7xgjhVAg6g
about: You can ask for help here!
- name: Want to contribute to step certificates?
url: https://github.com/smallstep/certificates/blob/master/CONTRIBUTING.md
url: https://github.com/smallstep/certificates/blob/master/docs/CONTRIBUTING.md
about: Be sure to read contributing guidelines!

@ -1,17 +0,0 @@
name: Lint GitHub Actions workflows
on:
push:
workflow_call:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
jobs:
actionlint:
uses: smallstep/workflows/.github/workflows/actionlint.yml@main
secrets: inherit

@ -6,6 +6,17 @@ permissions:
pull-requests: write
jobs:
dependabot-auto-merge:
uses: smallstep/workflows/.github/workflows/dependabot-auto-merge.yml@main
secrets: inherit
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1.6.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

@ -45,12 +45,12 @@ jobs:
echo "DOCKER_TAGS_HSM=${{ env.DOCKER_TAGS_HSM }},${{ env.DOCKER_IMAGE }}:hsm" >> "${GITHUB_ENV}"
- name: Create Release
id: create_release
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
name: Release ${{ github.ref_name }}
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}
@ -59,7 +59,6 @@ jobs:
permissions:
id-token: write
contents: write
packages: write
uses: smallstep/workflows/.github/workflows/goreleaser.yml@main
secrets: inherit

@ -1,6 +1,6 @@
# Documentation: https://goreleaser.com/customization/
# This is an example .goreleaser.yml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
project_name: step-ca
version: 2
before:
hooks:
@ -98,7 +98,7 @@ signs:
- cmd: cosign
signature: "${artifact}.sig"
certificate: "${artifact}.pem"
args: ["sign-blob", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}", "--yes"]
args: ["sign-blob", "--oidc-issuer=https://token.actions.githubusercontent.com", "--output-certificate=${certificate}", "--output-signature=${signature}", "${artifact}"]
artifacts: all
snapshot:
@ -180,7 +180,7 @@ release:
Those were the changes on {{ .Tag }}!
Come join us on [Discord](https://discord.gg/X2RKGwEbV9) to ask questions, chat about PKI, or get a sneak peek at the freshest PKI memes.
Come join us on [Discord](https://discord.gg/X2RKGwEbV9) to ask questions, chat about PKI, or get a sneak peak at the freshest PKI memes.
# You can disable this pipe in order to not upload any artifacts.
# Defaults to false.
@ -268,7 +268,7 @@ winget:
# Release notes URL.
#
# Templates: allowed
release_notes_url: "https://github.com/smallstep/certificates/releases/tag/{{ .Tag }}"
release_notes_url: "https://github.com/smallstep/certificates/releases/tag/{{.Version}}"
# Create the PR - for testing
skip_upload: auto
@ -283,7 +283,7 @@ winget:
repository:
owner: smallstep
name: winget-pkgs
branch: "step-ca-{{.Version}}"
branch: step
# Optionally a token can be provided, if it differs from the token
# provided to GoReleaser

@ -25,118 +25,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
---
## [0.27.4] - 2024-09-13
### Fixed
- Release worfklow
## [0.27.3] - 2024-09-13
### Added
- AWS auth method for Vault RA mode (smallstep/certificates#1976)
- API endpoints for retrieving Intermediate certificates (smallstep/certificates#1962)
- Enable use of OIDC provisioner with private identity providers and a certificate from step-ca (smallstep/certificates#1940)
- Support for verifying `cnf` and `x5rt#S256` claim when provided in token (smallstep/certificates#1660)
- Add Wire integration to ACME provisioner (smallstep/certificates#1666)
### Changed
- Clarified SSH certificate policy errors (smallstep/certificates#1951)
### Fixed
- Nebula ECDSA P-256 support (smallstep/certificates#1662)
## [0.27.2] - 2024-07-18
### Added
- `--console` option to default step-ssh config (smallstep/certificates#1931)
## [0.27.1] - 2024-07-12
### Changed
- Enable use of strict FQDN with a flag (smallstep/certificates#1926)
- This reverses a change in 0.27.0 that required the use of strict FQDNs (smallstep/certificate#1910)
## [0.27.0] - 2024-07-11
### Added
- Support for validity windows in templates (smallstep/certificates#1903)
- Create identity certificate with host URI when using any provisioner (smallstep/certificates#1922)
### Changed
- Do strict DNS lookup on ACME (smallstep/certificates#1910)
### Fixed
- Handle bad attestation object in deviceAttest01 validation (smallstep/certificates#1913)
## [0.26.2] - 2024-06-13
### Added
- Add provisionerID to ACME accounts (smallstep/certificates#1830)
- Enable verifying ACME provisioner using provisionerID if available (smallstep/certificates#1844)
- Add methods to Authority to get intermediate certificates (smallstep/certificates#1848)
- Add GetX509Signer method (smallstep/certificates#1850)
### Changed
- Make ISErrNotFound more flexible (smallstep/certificates#1819)
- Log errors using slog.Logger (smallstep/certificates#1849)
- Update hardcoded AWS certificates (smallstep/certificates#1881)
## [0.26.1] - 2024-04-22
### Added
- Allow configuration of a custom SCEP key manager (smallstep/certificates#1797)
### Fixed
- id-scep-failInfoText OID (smallstep/certificates#1794)
- CA startup with Vault RA configuration (smallstep/certificates#1803)
## [0.26.0] - 2024-03-28
### Added
- [TPM KMS](https://github.com/smallstep/crypto/tree/master/kms/tpmkms) support for CA keys (smallstep/certificates#1772)
- Propagation of HTTP request identifier using X-Request-Id header (smallstep/certificates#1743, smallstep/certificates#1542)
- Expires header in CRL response (smallstep/certificates#1708)
- Support for providing TLS configuration programmatically (smallstep/certificates#1685)
- Support for providing external CAS implementation (smallstep/certificates#1684)
- AWS `ca-west-1` identity document root certificate (smallstep/certificates#1715)
- [COSE RS1](https://www.rfc-editor.org/rfc/rfc8812.html#section-2) as a supported algorithm with ACME `device-attest-01` challenge (smallstep/certificates#1663)
### Changed
- In an RA setup, let the CA decide the RA certificate lifetime (smallstep/certificates#1764)
- Use Debian Bookworm in Docker containers (smallstep/certificates#1615)
- Error message for CSR validation (smallstep/certificates#1665)
- Updated dependencies
### Fixed
- Stop CA when any of the required servers fails to start (smallstep/certificates#1751). Before the fix, the CA would continue running and only log the server failure when stopped.
- Configuration loading errors when not using context were not returned. Fixed in [cli-utils/109](https://github.com/smallstep/cli-utils/pull/109).
- HTTP_PROXY and HTTPS_PROXY support for ACME validation client (smallstep/certificates#1658).
### Security
- Upgrade to using cosign v2 for signing artifacts
## [0.25.1] - 2023-11-28
### Added

@ -147,7 +147,7 @@ lint:
# Install
#########################################
INSTALL_PREFIX?=/usr/local/
INSTALL_PREFIX?=/usr/
install: $(PREFIX)bin/$(BINNAME)
$Q install -D $(PREFIX)bin/$(BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(BINNAME)

@ -21,7 +21,6 @@ type Account struct {
OrdersURL string `json:"orders"`
ExternalAccountBinding interface{} `json:"externalAccountBinding,omitempty"`
LocationPrefix string `json:"-"`
ProvisionerID string `json:"-"`
ProvisionerName string `json:"-"`
}

@ -82,23 +82,23 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
payload, err := payloadFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
var nar NewAccountRequest
if err := json.Unmarshal(payload.value, &nar); err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
"failed to unmarshal new-account request payload"))
return
}
if err := nar.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
prov, err := acmeProvisionerFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -108,26 +108,26 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
var acmeErr *acme.Error
if !errors.As(err, &acmeErr) || acmeErr.Status != http.StatusBadRequest {
// Something went wrong ...
render.Error(w, r, err)
render.Error(w, err)
return
}
// Account does not exist //
if nar.OnlyReturnExisting {
render.Error(w, r, acme.NewError(acme.ErrorAccountDoesNotExistType,
render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType,
"account does not exist"))
return
}
jwk, err := jwkFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
eak, err := validateExternalAccountBinding(ctx, &nar)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -136,21 +136,20 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
Contact: nar.Contact,
Status: acme.StatusValid,
LocationPrefix: getAccountLocationPath(ctx, linker, ""),
ProvisionerID: prov.ID,
ProvisionerName: prov.Name,
ProvisionerName: prov.GetName(),
}
if err := db.CreateAccount(ctx, acc); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error creating account"))
render.Error(w, acme.WrapErrorISE(err, "error creating account"))
return
}
if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
if err := eak.BindTo(acc); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
if err := db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error updating external account binding key"))
render.Error(w, acme.WrapErrorISE(err, "error updating external account binding key"))
return
}
acc.ExternalAccountBinding = nar.ExternalAccountBinding
@ -163,7 +162,7 @@ func NewAccount(w http.ResponseWriter, r *http.Request) {
linker.LinkAccount(ctx, acc)
w.Header().Set("Location", getAccountLocationPath(ctx, linker, acc.ID))
render.JSONStatus(w, r, acc, httpStatus)
render.JSONStatus(w, acc, httpStatus)
}
// GetOrUpdateAccount is the api for updating an ACME account.
@ -174,12 +173,12 @@ func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) {
acc, err := accountFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
payload, err := payloadFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -188,12 +187,12 @@ func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) {
if !payload.isPostAsGet {
var uar UpdateAccountRequest
if err := json.Unmarshal(payload.value, &uar); err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
"failed to unmarshal new-account request payload"))
return
}
if err := uar.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
if len(uar.Status) > 0 || len(uar.Contact) > 0 {
@ -204,7 +203,7 @@ func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) {
}
if err := db.UpdateAccount(ctx, acc); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error updating account"))
render.Error(w, acme.WrapErrorISE(err, "error updating account"))
return
}
}
@ -213,7 +212,7 @@ func GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) {
linker.LinkAccount(ctx, acc)
w.Header().Set("Location", linker.GetLink(ctx, acme.AccountLinkType, acc.ID))
render.JSON(w, r, acc)
render.JSON(w, acc)
}
func logOrdersByAccount(w http.ResponseWriter, oids []string) {
@ -233,23 +232,23 @@ func GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) {
acc, err := accountFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
accID := chi.URLParam(r, "accID")
if acc.ID != accID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType, "account ID '%s' does not match url param '%s'", acc.ID, accID))
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account ID '%s' does not match url param '%s'", acc.ID, accID))
return
}
orders, err := db.GetOrdersByAccountID(ctx, acc.ID)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
linker.LinkOrdersByAccountID(ctx, orders)
render.JSON(w, r, orders)
render.JSON(w, orders)
logOrdersByAccount(w, orders)
}

@ -14,7 +14,6 @@ import (
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/pkg/errors"
"go.step.sm/crypto/jose"
@ -69,19 +68,6 @@ func newProv() acme.Provisioner {
return p
}
func newProvWithID() acme.Provisioner {
// Initialize provisioners
p := &provisioner.ACME{
ID: uuid.NewString(),
Type: "ACME",
Name: "test@acme-<test>provisioner.com",
}
if err := p.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil {
fmt.Printf("%v", err)
}
return p
}
func newProvWithOptions(options *provisioner.Options) acme.Provisioner {
// Initialize provisioners
p := &provisioner.ACME{

@ -223,13 +223,13 @@ func GetDirectory(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
acmeProv, err := acmeProvisionerFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
linker := acme.MustLinkerFromContext(ctx)
render.JSON(w, r, &Directory{
render.JSON(w, &Directory{
NewNonce: linker.GetLink(ctx, acme.NewNonceLinkType),
NewAccount: linker.GetLink(ctx, acme.NewAccountLinkType),
NewOrder: linker.GetLink(ctx, acme.NewOrderLinkType),
@ -273,8 +273,8 @@ func shouldAddMetaObject(p *provisioner.ACME) bool {
// NotImplemented returns a 501 and is generally a placeholder for functionality which
// MAY be added at some point in the future but is not in any way a guarantee of such.
func NotImplemented(w http.ResponseWriter, r *http.Request) {
render.Error(w, r, acme.NewError(acme.ErrorNotImplementedType, "this API is not implemented"))
func NotImplemented(w http.ResponseWriter, _ *http.Request) {
render.Error(w, acme.NewError(acme.ErrorNotImplementedType, "this API is not implemented"))
}
// GetAuthorization ACME api for retrieving an Authz.
@ -285,28 +285,28 @@ func GetAuthorization(w http.ResponseWriter, r *http.Request) {
acc, err := accountFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
az, err := db.GetAuthorization(ctx, chi.URLParam(r, "authzID"))
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving authorization"))
render.Error(w, acme.WrapErrorISE(err, "error retrieving authorization"))
return
}
if acc.ID != az.AccountID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"account '%s' does not own authorization '%s'", acc.ID, az.ID))
return
}
if err = az.UpdateStatus(ctx, db); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error updating authorization status"))
render.Error(w, acme.WrapErrorISE(err, "error updating authorization status"))
return
}
linker.LinkAuthorization(ctx, az)
w.Header().Set("Location", linker.GetLink(ctx, acme.AuthzLinkType, az.ID))
render.JSON(w, r, az)
render.JSON(w, az)
}
// GetChallenge ACME api for retrieving a Challenge.
@ -317,13 +317,13 @@ func GetChallenge(w http.ResponseWriter, r *http.Request) {
acc, err := accountFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
payload, err := payloadFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -336,22 +336,22 @@ func GetChallenge(w http.ResponseWriter, r *http.Request) {
azID := chi.URLParam(r, "authzID")
ch, err := db.GetChallenge(ctx, chi.URLParam(r, "chID"), azID)
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving challenge"))
render.Error(w, acme.WrapErrorISE(err, "error retrieving challenge"))
return
}
ch.AuthorizationID = azID
if acc.ID != ch.AccountID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"account '%s' does not own challenge '%s'", acc.ID, ch.ID))
return
}
jwk, err := jwkFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
if err = ch.Validate(ctx, db, jwk, payload.value); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error validating challenge"))
render.Error(w, acme.WrapErrorISE(err, "error validating challenge"))
return
}
@ -359,7 +359,7 @@ func GetChallenge(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Link", link(linker.GetLink(ctx, acme.AuthzLinkType, azID), "up"))
w.Header().Set("Location", linker.GetLink(ctx, acme.ChallengeLinkType, azID, ch.ID))
render.JSON(w, r, ch)
render.JSON(w, ch)
}
// GetCertificate ACME api for retrieving a Certificate.
@ -369,18 +369,18 @@ func GetCertificate(w http.ResponseWriter, r *http.Request) {
acc, err := accountFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
certID := chi.URLParam(r, "certID")
cert, err := db.GetCertificate(ctx, certID)
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving certificate"))
render.Error(w, acme.WrapErrorISE(err, "error retrieving certificate"))
return
}
if cert.AccountID != acc.ID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"account '%s' does not own certificate '%s'", acc.ID, certID))
return
}

@ -36,7 +36,7 @@ func addNonce(next nextHTTP) nextHTTP {
db := acme.MustDatabaseFromContext(r.Context())
nonce, err := db.CreateNonce(r.Context())
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
w.Header().Set("Replay-Nonce", string(nonce))
@ -64,7 +64,7 @@ func verifyContentType(next nextHTTP) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) {
p, err := provisionerFromContext(r.Context())
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -88,7 +88,7 @@ func verifyContentType(next nextHTTP) nextHTTP {
return
}
}
render.Error(w, r, acme.NewError(acme.ErrorMalformedType,
render.Error(w, acme.NewError(acme.ErrorMalformedType,
"expected content-type to be in %s, but got %s", expected, ct))
}
}
@ -98,12 +98,12 @@ func parseJWS(next nextHTTP) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "failed to read request body"))
render.Error(w, acme.WrapErrorISE(err, "failed to read request body"))
return
}
jws, err := jose.ParseJWS(string(body))
if err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, "failed to parse JWS from request body"))
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "failed to parse JWS from request body"))
return
}
ctx := context.WithValue(r.Context(), jwsContextKey, jws)
@ -133,26 +133,26 @@ func validateJWS(next nextHTTP) nextHTTP {
jws, err := jwsFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
if len(jws.Signatures) == 0 {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "request body does not contain a signature"))
render.Error(w, acme.NewError(acme.ErrorMalformedType, "request body does not contain a signature"))
return
}
if len(jws.Signatures) > 1 {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "request body contains more than one signature"))
render.Error(w, acme.NewError(acme.ErrorMalformedType, "request body contains more than one signature"))
return
}
sig := jws.Signatures[0]
uh := sig.Unprotected
if uh.KeyID != "" ||
if len(uh.KeyID) > 0 ||
uh.JSONWebKey != nil ||
uh.Algorithm != "" ||
uh.Nonce != "" ||
len(uh.Algorithm) > 0 ||
len(uh.Nonce) > 0 ||
len(uh.ExtraHeaders) > 0 {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "unprotected header must not be used"))
render.Error(w, acme.NewError(acme.ErrorMalformedType, "unprotected header must not be used"))
return
}
hdr := sig.Protected
@ -162,13 +162,13 @@ func validateJWS(next nextHTTP) nextHTTP {
switch k := hdr.JSONWebKey.Key.(type) {
case *rsa.PublicKey:
if k.Size() < keyutil.MinRSAKeyBytes {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType,
render.Error(w, acme.NewError(acme.ErrorMalformedType,
"rsa keys must be at least %d bits (%d bytes) in size",
8*keyutil.MinRSAKeyBytes, keyutil.MinRSAKeyBytes))
return
}
default:
render.Error(w, r, acme.NewError(acme.ErrorMalformedType,
render.Error(w, acme.NewError(acme.ErrorMalformedType,
"jws key type and algorithm do not match"))
return
}
@ -176,35 +176,35 @@ func validateJWS(next nextHTTP) nextHTTP {
case jose.ES256, jose.ES384, jose.ES512, jose.EdDSA:
// we good
default:
render.Error(w, r, acme.NewError(acme.ErrorBadSignatureAlgorithmType, "unsuitable algorithm: %s", hdr.Algorithm))
render.Error(w, acme.NewError(acme.ErrorBadSignatureAlgorithmType, "unsuitable algorithm: %s", hdr.Algorithm))
return
}
// Check the validity/freshness of the Nonce.
if err := db.DeleteNonce(ctx, acme.Nonce(hdr.Nonce)); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
// Check that the JWS url matches the requested url.
jwsURL, ok := hdr.ExtraHeaders["url"].(string)
if !ok {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "jws missing url protected header"))
render.Error(w, acme.NewError(acme.ErrorMalformedType, "jws missing url protected header"))
return
}
reqURL := &url.URL{Scheme: "https", Host: r.Host, Path: r.URL.Path}
if jwsURL != reqURL.String() {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType,
render.Error(w, acme.NewError(acme.ErrorMalformedType,
"url header in JWS (%s) does not match request url (%s)", jwsURL, reqURL))
return
}
if hdr.JSONWebKey != nil && hdr.KeyID != "" {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "jwk and kid are mutually exclusive"))
if hdr.JSONWebKey != nil && len(hdr.KeyID) > 0 {
render.Error(w, acme.NewError(acme.ErrorMalformedType, "jwk and kid are mutually exclusive"))
return
}
if hdr.JSONWebKey == nil && hdr.KeyID == "" {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "either jwk or kid must be defined in jws protected header"))
render.Error(w, acme.NewError(acme.ErrorMalformedType, "either jwk or kid must be defined in jws protected header"))
return
}
next(w, r)
@ -221,23 +221,23 @@ func extractJWK(next nextHTTP) nextHTTP {
jws, err := jwsFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
jwk := jws.Signatures[0].Protected.JSONWebKey
if jwk == nil {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "jwk expected in protected header"))
render.Error(w, acme.NewError(acme.ErrorMalformedType, "jwk expected in protected header"))
return
}
if !jwk.Valid() {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "invalid jwk in protected header"))
render.Error(w, acme.NewError(acme.ErrorMalformedType, "invalid jwk in protected header"))
return
}
// Overwrite KeyID with the JWK thumbprint.
jwk.KeyID, err = acme.KeyToID(jwk)
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error getting KeyID from JWK"))
render.Error(w, acme.WrapErrorISE(err, "error getting KeyID from JWK"))
return
}
@ -247,15 +247,15 @@ func extractJWK(next nextHTTP) nextHTTP {
// Get Account OR continue to generate a new one OR continue Revoke with certificate private key
acc, err := db.GetAccountByKeyID(ctx, jwk.KeyID)
switch {
case acme.IsErrNotFound(err):
case errors.Is(err, acme.ErrNotFound):
// For NewAccount and Revoke requests ...
break
case err != nil:
render.Error(w, r, err)
render.Error(w, err)
return
default:
if !acc.IsValid() {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType, "account is not active"))
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account is not active"))
return
}
ctx = context.WithValue(ctx, accContextKey, acc)
@ -274,11 +274,11 @@ func checkPrerequisites(next nextHTTP) nextHTTP {
if ok {
ok, err := checkFunc(ctx)
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error checking acme provisioner prerequisites"))
render.Error(w, acme.WrapErrorISE(err, "error checking acme provisioner prerequisites"))
return
}
if !ok {
render.Error(w, r, acme.NewError(acme.ErrorNotImplementedType, "acme provisioner configuration lacks prerequisites"))
render.Error(w, acme.NewError(acme.ErrorNotImplementedType, "acme provisioner configuration lacks prerequisites"))
return
}
}
@ -296,13 +296,13 @@ func lookupJWK(next nextHTTP) nextHTTP {
jws, err := jwsFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
kid := jws.Signatures[0].Protected.KeyID
if kid == "" {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "signature missing 'kid'"))
render.Error(w, acme.NewError(acme.ErrorMalformedType, "signature missing 'kid'"))
return
}
@ -310,14 +310,14 @@ func lookupJWK(next nextHTTP) nextHTTP {
acc, err := db.GetAccount(ctx, accID)
switch {
case acme.IsErrNotFound(err):
render.Error(w, r, acme.NewError(acme.ErrorAccountDoesNotExistType, "account with ID '%s' not found", accID))
render.Error(w, acme.NewError(acme.ErrorAccountDoesNotExistType, "account with ID '%s' not found", accID))
return
case err != nil:
render.Error(w, r, err)
render.Error(w, err)
return
default:
if !acc.IsValid() {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType, "account is not active"))
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account is not active"))
return
}
@ -325,7 +325,7 @@ func lookupJWK(next nextHTTP) nextHTTP {
if kid != storedLocation {
// ACME accounts should have a stored location equivalent to the
// kid in the ACME request.
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"kid does not match stored account location; expected %s, but got %s",
storedLocation, kid))
return
@ -334,16 +334,14 @@ func lookupJWK(next nextHTTP) nextHTTP {
// Verify that the provisioner with which the account was created
// matches the provisioner in the request URL.
reqProv := acme.MustProvisionerFromContext(ctx)
switch {
case acc.ProvisionerID == "" && acc.ProvisionerName != reqProv.GetName():
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s",
acc.ProvisionerName, reqProv.GetName()))
return
case acc.ProvisionerID != "" && acc.ProvisionerID != reqProv.GetID():
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
reqProvName := reqProv.GetName()
accProvName := acc.ProvisionerName
if reqProvName != accProvName {
// Provisioner in the URL must match the provisioner with
// which the account was created.
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s",
acc.ProvisionerID, reqProv.GetID()))
accProvName, reqProvName))
return
}
} else {
@ -355,7 +353,7 @@ func lookupJWK(next nextHTTP) nextHTTP {
linker := acme.MustLinkerFromContext(ctx)
kidPrefix := linker.GetLink(ctx, acme.AccountLinkType, "")
if !strings.HasPrefix(kid, kidPrefix) {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType,
render.Error(w, acme.NewError(acme.ErrorMalformedType,
"kid does not have required prefix; expected %s, but got %s",
kidPrefix, kid))
return
@ -376,7 +374,7 @@ func extractOrLookupJWK(next nextHTTP) nextHTTP {
ctx := r.Context()
jws, err := jwsFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -412,16 +410,16 @@ func verifyAndExtractJWSPayload(next nextHTTP) nextHTTP {
ctx := r.Context()
jws, err := jwsFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
jwk, err := jwkFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
if jwk.Algorithm != "" && jwk.Algorithm != jws.Signatures[0].Protected.Algorithm {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "verifier and signature algorithm do not match"))
render.Error(w, acme.NewError(acme.ErrorMalformedType, "verifier and signature algorithm do not match"))
return
}
@ -430,11 +428,11 @@ func verifyAndExtractJWSPayload(next nextHTTP) nextHTTP {
case errors.Is(err, jose.ErrCryptoFailure):
payload, err = retryVerificationWithPatchedSignatures(jws, jwk)
if err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, "error verifying jws with patched signature(s)"))
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "error verifying jws with patched signature(s)"))
return
}
case err != nil:
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, "error verifying jws"))
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "error verifying jws"))
return
}
@ -551,11 +549,11 @@ func isPostAsGet(next nextHTTP) nextHTTP {
return func(w http.ResponseWriter, r *http.Request) {
payload, err := payloadFromContext(r.Context())
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
if !payload.isPostAsGet {
render.Error(w, r, acme.NewError(acme.ErrorMalformedType, "expected POST-as-GET"))
render.Error(w, acme.NewError(acme.ErrorMalformedType, "expected POST-as-GET"))
return
}
next(w, r)

@ -15,7 +15,6 @@ import (
"strings"
"testing"
"github.com/google/uuid"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/acme"
tassert "github.com/stretchr/testify/assert"
@ -832,37 +831,8 @@ func TestHandler_lookupJWK(t *testing.T) {
},
statusCode: http.StatusUnauthorized,
err: acme.NewError(acme.ErrorUnauthorizedType,
"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s",
"other", prov.GetName()),
}
},
"fail/account-with-location-prefix/bad-provisioner-id": func(t *testing.T) test {
p := newProvWithID()
acc := &acme.Account{LocationPrefix: prefix + accID, Status: "valid", Key: jwk, ProvisionerID: uuid.NewString()}
ctx := acme.NewProvisionerContext(context.Background(), p)
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
return test{
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
db: &acme.MockDB{
MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {
assert.Equals(t, id, accID)
return acc, nil
},
},
ctx: ctx,
next: func(w http.ResponseWriter, r *http.Request) {
_acc, err := accountFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _acc, acc)
_jwk, err := jwkFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _jwk, jwk)
w.Write(testBody)
},
statusCode: http.StatusUnauthorized,
err: acme.NewError(acme.ErrorUnauthorizedType,
"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s",
acc.ProvisionerID, p.GetID()),
"account provisioner does not match requested provisioner; account provisioner = %s, reqested provisioner = %s",
prov.GetName(), "other"),
}
},
"ok/account-with-location-prefix": func(t *testing.T) test {
@ -915,32 +885,6 @@ func TestHandler_lookupJWK(t *testing.T) {
statusCode: 200,
}
},
"ok/account-with-provisioner-id": func(t *testing.T) test {
p := newProvWithID()
acc := &acme.Account{LocationPrefix: prefix + accID, Status: "valid", Key: jwk, ProvisionerID: p.GetID()}
ctx := acme.NewProvisionerContext(context.Background(), p)
ctx = context.WithValue(ctx, jwsContextKey, parsedJWS)
return test{
linker: acme.NewLinker("test.ca.smallstep.com", "acme"),
db: &acme.MockDB{
MockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {
assert.Equals(t, id, accID)
return acc, nil
},
},
ctx: ctx,
next: func(w http.ResponseWriter, r *http.Request) {
_acc, err := accountFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _acc, acc)
_jwk, err := jwkFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _jwk, jwk)
w.Write(testBody)
},
statusCode: 200,
}
},
}
for name, run := range tests {
tc := run(t)

@ -171,29 +171,29 @@ func NewOrder(w http.ResponseWriter, r *http.Request) {
acc, err := accountFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
prov, err := provisionerFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
payload, err := payloadFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
var nor NewOrderRequest
if err := json.Unmarshal(payload.value, &nor); err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
"failed to unmarshal new-order request payload"))
return
}
if err := nor.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -202,39 +202,39 @@ func NewOrder(w http.ResponseWriter, r *http.Request) {
acmeProv, err := acmeProvisionerFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
var eak *acme.ExternalAccountKey
if acmeProv.RequireEAB {
if eak, err = db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving external account binding key"))
render.Error(w, acme.WrapErrorISE(err, "error retrieving external account binding key"))
return
}
}
acmePolicy, err := newACMEPolicyEngine(eak)
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error creating ACME policy engine"))
render.Error(w, acme.WrapErrorISE(err, "error creating ACME policy engine"))
return
}
for _, identifier := range nor.Identifiers {
// evaluate the ACME account level policy
if err = isIdentifierAllowed(acmePolicy, identifier); err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
return
}
// evaluate the provisioner level policy
orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value}
if err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier); err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
return
}
// evaluate the authority level policy
if err = ca.AreSANsAllowed(ctx, []string{identifier.Value}); err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized"))
return
}
}
@ -260,7 +260,7 @@ func NewOrder(w http.ResponseWriter, r *http.Request) {
Status: acme.StatusPending,
}
if err := newAuthorization(ctx, az); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
o.AuthorizationIDs[i] = az.ID
@ -279,14 +279,14 @@ func NewOrder(w http.ResponseWriter, r *http.Request) {
}
if err := db.CreateOrder(ctx, o); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error creating order"))
render.Error(w, acme.WrapErrorISE(err, "error creating order"))
return
}
linker.LinkOrder(ctx, o)
w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID))
render.JSONStatus(w, r, o, http.StatusCreated)
render.JSONStatus(w, o, http.StatusCreated)
}
func isIdentifierAllowed(acmePolicy policy.X509Policy, identifier acme.Identifier) error {
@ -298,7 +298,6 @@ func isIdentifierAllowed(acmePolicy policy.X509Policy, identifier acme.Identifie
func newACMEPolicyEngine(eak *acme.ExternalAccountKey) (policy.X509Policy, error) {
if eak == nil {
//nolint:nilnil,nolintlint // expected values
return nil, nil
}
return policy.NewX509PolicyEngine(eak.Policy)
@ -392,39 +391,39 @@ func GetOrder(w http.ResponseWriter, r *http.Request) {
acc, err := accountFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
prov, err := provisionerFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID"))
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving order"))
render.Error(w, acme.WrapErrorISE(err, "error retrieving order"))
return
}
if acc.ID != o.AccountID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"account '%s' does not own order '%s'", acc.ID, o.ID))
return
}
if prov.GetID() != o.ProvisionerID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"provisioner '%s' does not own order '%s'", prov.GetID(), o.ID))
return
}
if err = o.UpdateStatus(ctx, db); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error updating order status"))
render.Error(w, acme.WrapErrorISE(err, "error updating order status"))
return
}
linker.LinkOrder(ctx, o)
w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID))
render.JSON(w, r, o)
render.JSON(w, o)
}
// FinalizeOrder attempts to finalize an order and create a certificate.
@ -435,56 +434,56 @@ func FinalizeOrder(w http.ResponseWriter, r *http.Request) {
acc, err := accountFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
prov, err := provisionerFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
payload, err := payloadFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
var fr FinalizeRequest
if err := json.Unmarshal(payload.value, &fr); err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err,
"failed to unmarshal finalize-order request payload"))
return
}
if err := fr.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
o, err := db.GetOrder(ctx, chi.URLParam(r, "ordID"))
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving order"))
render.Error(w, acme.WrapErrorISE(err, "error retrieving order"))
return
}
if acc.ID != o.AccountID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"account '%s' does not own order '%s'", acc.ID, o.ID))
return
}
if prov.GetID() != o.ProvisionerID {
render.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,
render.Error(w, acme.NewError(acme.ErrorUnauthorizedType,
"provisioner '%s' does not own order '%s'", prov.GetID(), o.ID))
return
}
ca := mustAuthority(ctx)
if err = o.Finalize(ctx, db, fr.csr, ca, prov); err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error finalizing order"))
render.Error(w, acme.WrapErrorISE(err, "error finalizing order"))
return
}
linker.LinkOrder(ctx, o)
w.Header().Set("Location", linker.GetLink(ctx, acme.OrderLinkType, o.ID))
render.JSON(w, r, o)
render.JSON(w, o)
}
// challengeTypes determines the types of challenges that should be used

@ -108,7 +108,7 @@ func TestNewOrderRequest_Validate(t *testing.T) {
{Type: "wireapp-device", Value: `{"name": "Smith, Alice M (QA)", "domain": "example.com", "client-id": "example.com", "handle": "wireapp://%40alice.smith.qa@example.com"}`},
},
},
err: acme.NewError(acme.ErrorMalformedType, `failed validating Wire identifiers: invalid Wire client ID "example.com": invalid Wire client ID scheme ""; expected "wireapp"`),
err: acme.NewError(acme.ErrorMalformedType, `failed validating Wire identifiers: invalid Wire client ID "example.com": invalid Wire client ID URI "example.com": error parsing example.com: scheme is missing`),
}
},
"fail/bad-identifier/wireapp-wrong-scheme": func(t *testing.T) test {
@ -743,7 +743,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
az: az,
err: &acme.Error{
Type: "urn:ietf:params:acme:error:malformed",
Err: errors.New(`failed parsing ClientID: invalid Wire client ID scheme ""; expected "wireapp"`),
Err: errors.New("failed parsing ClientID: invalid Wire client ID URI \"CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\": error parsing CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com: scheme is missing"),
Detail: "The request message was malformed",
Status: 400,
},

@ -33,65 +33,65 @@ func RevokeCert(w http.ResponseWriter, r *http.Request) {
jws, err := jwsFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
prov, err := provisionerFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
payload, err := payloadFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
var p revokePayload
err = json.Unmarshal(payload.value, &p)
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error unmarshaling payload"))
render.Error(w, acme.WrapErrorISE(err, "error unmarshaling payload"))
return
}
certBytes, err := base64.RawURLEncoding.DecodeString(p.Certificate)
if err != nil {
// in this case the most likely cause is a client that didn't properly encode the certificate
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, "error base64url decoding payload certificate property"))
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "error base64url decoding payload certificate property"))
return
}
certToBeRevoked, err := x509.ParseCertificate(certBytes)
if err != nil {
// in this case a client may have encoded something different than a certificate
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, "error parsing certificate"))
render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "error parsing certificate"))
return
}
serial := certToBeRevoked.SerialNumber.String()
dbCert, err := db.GetCertificateBySerial(ctx, serial)
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving certificate by serial"))
render.Error(w, acme.WrapErrorISE(err, "error retrieving certificate by serial"))
return
}
if !bytes.Equal(dbCert.Leaf.Raw, certToBeRevoked.Raw) {
// this should never happen
render.Error(w, r, acme.NewErrorISE("certificate raw bytes are not equal"))
render.Error(w, acme.NewErrorISE("certificate raw bytes are not equal"))
return
}
if shouldCheckAccountFrom(jws) {
account, err := accountFromContext(ctx)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
acmeErr := isAccountAuthorized(ctx, dbCert, certToBeRevoked, account)
if acmeErr != nil {
render.Error(w, r, acmeErr)
render.Error(w, acmeErr)
return
}
} else {
@ -100,7 +100,7 @@ func RevokeCert(w http.ResponseWriter, r *http.Request) {
_, err := jws.Verify(certToBeRevoked.PublicKey)
if err != nil {
// TODO(hs): possible to determine an error vs. unauthorized and thus provide an ISE vs. Unauthorized?
render.Error(w, r, wrapUnauthorizedError(certToBeRevoked, nil, "verification of jws using certificate public key failed", err))
render.Error(w, wrapUnauthorizedError(certToBeRevoked, nil, "verification of jws using certificate public key failed", err))
return
}
}
@ -108,19 +108,19 @@ func RevokeCert(w http.ResponseWriter, r *http.Request) {
ca := mustAuthority(ctx)
hasBeenRevokedBefore, err := ca.IsRevoked(serial)
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error retrieving revocation status of certificate"))
render.Error(w, acme.WrapErrorISE(err, "error retrieving revocation status of certificate"))
return
}
if hasBeenRevokedBefore {
render.Error(w, r, acme.NewError(acme.ErrorAlreadyRevokedType, "certificate was already revoked"))
render.Error(w, acme.NewError(acme.ErrorAlreadyRevokedType, "certificate was already revoked"))
return
}
reasonCode := p.ReasonCode
acmeErr := validateReasonCode(reasonCode)
if acmeErr != nil {
render.Error(w, r, acmeErr)
render.Error(w, acmeErr)
return
}
@ -128,14 +128,14 @@ func RevokeCert(w http.ResponseWriter, r *http.Request) {
ctx = provisioner.NewContextWithMethod(ctx, provisioner.RevokeMethod)
err = prov.AuthorizeRevoke(ctx, "")
if err != nil {
render.Error(w, r, acme.WrapErrorISE(err, "error authorizing revocation on provisioner"))
render.Error(w, acme.WrapErrorISE(err, "error authorizing revocation on provisioner"))
return
}
options := revokeOptions(serial, certToBeRevoked, reasonCode)
err = ca.Revoke(ctx, options)
if err != nil {
render.Error(w, r, wrapRevokeErr(err))
render.Error(w, wrapRevokeErr(err))
return
}
@ -180,7 +180,7 @@ func isAccountAuthorized(_ context.Context, dbCert *acme.Certificate, certToBeRe
func wrapRevokeErr(err error) *acme.Error {
t := err.Error()
if strings.Contains(t, "is already revoked") {
return acme.NewError(acme.ErrorAlreadyRevokedType, t) //nolint:govet // allow non-constant error messages
return acme.NewError(acme.ErrorAlreadyRevokedType, t)
}
return acme.WrapErrorISE(err, "error when revoking certificate")
}
@ -190,9 +190,9 @@ func wrapRevokeErr(err error) *acme.Error {
func wrapUnauthorizedError(cert *x509.Certificate, unauthorizedIdentifiers []acme.Identifier, msg string, err error) *acme.Error {
var acmeErr *acme.Error
if err == nil {
acmeErr = acme.NewError(acme.ErrorUnauthorizedType, msg) //nolint:govet // allow non-constant error messages
acmeErr = acme.NewError(acme.ErrorUnauthorizedType, msg)
} else {
acmeErr = acme.WrapError(acme.ErrorUnauthorizedType, err, msg) //nolint:govet // allow non-constant error messages
acmeErr = acme.WrapError(acme.ErrorUnauthorizedType, err, msg)
}
acmeErr.Status = http.StatusForbidden // RFC8555 7.6 shows example with 403

@ -1,7 +1,6 @@
package acme
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
@ -68,11 +67,6 @@ var (
//
// This variable can be used for testing purposes.
InsecurePortTLSALPN01 int
// StrictFQDN allows to enforce a fully qualified domain name in the DNS
// resolution. By default it allows domain resolution using a search list
// defined in the resolv.conf or similar configuration.
StrictFQDN bool
)
// Challenge represents an ACME response Challenge type.
@ -117,36 +111,25 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey,
case DEVICEATTEST01:
return deviceAttest01Validate(ctx, ch, db, jwk, payload)
case WIREOIDC01:
wireDB, ok := db.(WireDB)
if !ok {
return NewErrorISE("db %T is not a WireDB", db)
}
return wireOIDC01Validate(ctx, ch, wireDB, jwk, payload)
return wireOIDC01Validate(ctx, ch, db, jwk, payload)
case WIREDPOP01:
wireDB, ok := db.(WireDB)
if !ok {
return NewErrorISE("db %T is not a WireDB", db)
}
return wireDPOP01Validate(ctx, ch, wireDB, jwk, payload)
return wireDPOP01Validate(ctx, ch, db, jwk, payload)
default:
return NewErrorISE("unexpected challenge type %q", ch.Type)
}
}
func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error {
u := &url.URL{Scheme: "http", Host: ch.Value, Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)}
challengeURL := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)}
u := &url.URL{Scheme: "http", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf("/.well-known/acme-challenge/%s", ch.Token)}
// Append insecure port if set.
// Only used for testing purposes.
if InsecurePortHTTP01 != 0 {
insecurePort := strconv.Itoa(InsecurePortHTTP01)
u.Host += ":" + insecurePort
challengeURL.Host += ":" + insecurePort
u.Host += ":" + strconv.Itoa(InsecurePortHTTP01)
}
vc := MustClientFromContext(ctx)
resp, err := vc.Get(challengeURL.String())
resp, err := vc.Get(u.String())
if err != nil {
return storeError(ctx, db, ch, false, WrapError(ErrorConnectionType, err,
"error doing http GET for url %s", u))
@ -184,42 +167,15 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb
return nil
}
// rootedName adds a trailing "." to a given domain name.
func rootedName(name string) string {
if StrictFQDN {
if name == "" || name[len(name)-1] != '.' {
return name + "."
}
}
return name
}
// http01ChallengeHost checks if a Challenge value is an IPv6 address
// and adds square brackets if that's the case, so that it can be used
// as a hostname. Returns the original Challenge value as the host to
// use in other cases.
func http01ChallengeHost(value string) string {
if ip := net.ParseIP(value); ip != nil {
if ip.To4() == nil {
value = "[" + value + "]"
}
return value
}
return rootedName(value)
}
// tlsAlpn01ChallengeHost returns the rooted DNS used on TLS-ALPN-01
// validations.
func tlsAlpn01ChallengeHost(name string) string {
if ip := net.ParseIP(name); ip != nil {
return name
if ip := net.ParseIP(value); ip != nil && ip.To4() == nil {
value = "[" + value + "]"
}
return rootedName(name)
}
// dns01ChallengeHost returns the TXT record used in DNS-01 validations.
func dns01ChallengeHost(domain string) string {
return "_acme-challenge." + rootedName(domain)
return value
}
func tlsAlert(err error) uint8 {
@ -244,12 +200,13 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON
InsecureSkipVerify: true, //nolint:gosec // we expect a self-signed challenge certificate
}
var hostPort string
// Allow to change TLS port for testing purposes.
hostPort := tlsAlpn01ChallengeHost(ch.Value)
if port := InsecurePortTLSALPN01; port == 0 {
hostPort = net.JoinHostPort(hostPort, "443")
hostPort = net.JoinHostPort(ch.Value, "443")
} else {
hostPort = net.JoinHostPort(hostPort, strconv.Itoa(port))
hostPort = net.JoinHostPort(ch.Value, strconv.Itoa(port))
}
vc := MustClientFromContext(ctx)
@ -264,7 +221,7 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON
"cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge"))
}
return storeError(ctx, db, ch, false, WrapError(ErrorConnectionType, err,
"error doing TLS dial for %s", ch.Value))
"error doing TLS dial for %s", hostPort))
}
defer conn.Close()
@ -360,7 +317,7 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK
domain := strings.TrimPrefix(ch.Value, "*.")
vc := MustClientFromContext(ctx)
txtRecords, err := vc.LookupTxt(dns01ChallengeHost(domain))
txtRecords, err := vc.LookupTxt("_acme-challenge." + domain)
if err != nil {
return storeError(ctx, db, ch, false, WrapError(ErrorDNSType, err,
"error looking up TXT records for domain %s", domain))
@ -400,7 +357,7 @@ type wireOidcPayload struct {
IDToken string `json:"id_token"`
}
func wireOIDC01Validate(ctx context.Context, ch *Challenge, db WireDB, jwk *jose.JSONWebKey, payload []byte) error {
func wireOIDC01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error {
prov, ok := ProvisionerFromContext(ctx)
if !ok {
return NewErrorISE("missing provisioner")
@ -530,7 +487,7 @@ type wireDpopPayload struct {
AccessToken string `json:"access_token"`
}
func wireDPOP01Validate(ctx context.Context, ch *Challenge, db WireDB, accountJWK *jose.JSONWebKey, payload []byte) error {
func wireDPOP01Validate(ctx context.Context, ch *Challenge, db DB, accountJWK *jose.JSONWebKey, payload []byte) error {
prov, ok := ProvisionerFromContext(ctx)
if !ok {
return NewErrorISE("missing provisioner")
@ -684,19 +641,19 @@ func parseAndVerifyWireAccessToken(v wireVerifyParams) (*wireAccessToken, *wireD
}
if accessToken.Challenge == "" {
return nil, nil, errors.New("access token challenge 'chal' must not be empty")
return nil, nil, errors.New("access token challenge must not be empty")
}
if accessToken.Cnf.Kid == "" || accessToken.Cnf.Kid != v.dpopKeyID {
return nil, nil, fmt.Errorf("expected 'kid' %q; got %q", v.dpopKeyID, accessToken.Cnf.Kid)
return nil, nil, fmt.Errorf("expected kid %q; got %q", v.dpopKeyID, accessToken.Cnf.Kid)
}
if accessToken.ClientID != v.wireID.ClientID {
return nil, nil, fmt.Errorf("invalid Wire 'client_id' %q", accessToken.ClientID)
return nil, nil, fmt.Errorf("invalid Wire client ID %q", accessToken.ClientID)
}
if accessToken.Expiry.Time().After(v.t.Add(time.Hour)) {
return nil, nil, fmt.Errorf("token expiry 'exp' %s is too far into the future", accessToken.Expiry.Time().String())
return nil, nil, fmt.Errorf("'exp' %s is too far into the future", accessToken.Expiry.Time().String())
}
if accessToken.Scope != "wire_client_id" {
return nil, nil, fmt.Errorf("invalid Wire 'scope' %q", accessToken.Scope)
return nil, nil, fmt.Errorf("invalid Wire scope %q", accessToken.Scope)
}
dpopJWT, err := jose.ParseSigned(accessToken.Proof)
@ -728,7 +685,7 @@ func parseAndVerifyWireAccessToken(v wireVerifyParams) (*wireAccessToken, *wireD
return nil, nil, fmt.Errorf("failed DPoP validation: %w", err)
}
if wireDpop.HTU == "" || wireDpop.HTU != v.issuer { // DPoP doesn't contains "iss" claim, but has it in the "htu" claim
return nil, nil, fmt.Errorf("DPoP contains invalid issuer 'htu' %q", wireDpop.HTU)
return nil, nil, fmt.Errorf("DPoP contains invalid issuer (htu) %q", wireDpop.HTU)
}
if wireDpop.Expiry.Time().After(v.t.Add(time.Hour)) {
return nil, nil, fmt.Errorf("'exp' %s is too far into the future", wireDpop.Expiry.Time().String())
@ -737,10 +694,10 @@ func parseAndVerifyWireAccessToken(v wireVerifyParams) (*wireAccessToken, *wireD
return nil, nil, fmt.Errorf("DPoP contains invalid Wire client ID %q", wireDpop.ClientID)
}
if wireDpop.Nonce == "" || wireDpop.Nonce != accessToken.Nonce {
return nil, nil, fmt.Errorf("DPoP contains invalid 'nonce' %q", wireDpop.Nonce)
return nil, nil, fmt.Errorf("DPoP contains invalid nonce %q", wireDpop.Nonce)
}
if wireDpop.Challenge == "" || wireDpop.Challenge != accessToken.Challenge {
return nil, nil, fmt.Errorf("DPoP contains invalid challenge 'chal' %q", wireDpop.Challenge)
return nil, nil, fmt.Errorf("DPoP contains invalid challenge %q", wireDpop.Challenge)
}
// TODO(hs): can we use the wireDpopJwt and map that instead of doing Claims() twice?
@ -751,26 +708,26 @@ func parseAndVerifyWireAccessToken(v wireVerifyParams) (*wireAccessToken, *wireD
challenge, ok := dpopToken["chal"].(string)
if !ok {
return nil, nil, fmt.Errorf("invalid challenge 'chal' in Wire DPoP token")
return nil, nil, fmt.Errorf("invalid challenge in Wire DPoP token")
}
if challenge == "" || challenge != v.chToken {
return nil, nil, fmt.Errorf("invalid Wire DPoP challenge 'chal' %q", challenge)
return nil, nil, fmt.Errorf("invalid Wire DPoP challenge %q", challenge)
}
handle, ok := dpopToken["handle"].(string)
if !ok {
return nil, nil, fmt.Errorf("invalid 'handle' in Wire DPoP token")
return nil, nil, fmt.Errorf("invalid handle in Wire DPoP token")
}
if handle == "" || handle != v.wireID.Handle {
return nil, nil, fmt.Errorf("invalid Wire client 'handle' %q", handle)
return nil, nil, fmt.Errorf("invalid Wire client handle %q", handle)
}
name, ok := dpopToken["name"].(string)
if !ok {
return nil, nil, fmt.Errorf("invalid display 'name' in Wire DPoP token")
return nil, nil, fmt.Errorf("invalid display name in Wire DPoP token")
}
if name == "" || name != v.wireID.Name {
return nil, nil, fmt.Errorf("invalid Wire client display 'name' %q", name)
return nil, nil, fmt.Errorf("invalid Wire client display name %q", name)
}
return &accessToken, &dpopToken, nil
@ -806,26 +763,12 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
attObj, err := base64.RawURLEncoding.DecodeString(p.AttObj)
if err != nil {
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "failed base64 decoding attObj %q", p.AttObj))
}
if len(attObj) == 0 || bytes.Equal(attObj, []byte("{}")) {
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "attObj must not be empty"))
}
cborDecoderOptions := cbor.DecOptions{}
cborDecoder, err := cborDecoderOptions.DecMode()
if err != nil {
return WrapErrorISE(err, "failed creating CBOR decoder")
}
if err := cborDecoder.Wellformed(attObj); err != nil {
return storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, "attObj is not well formed CBOR: %v", err))
return WrapErrorISE(err, "error base64 decoding attObj")
}
att := attestationObject{}
if err := cborDecoder.Unmarshal(attObj, &att); err != nil {
return WrapErrorISE(err, "failed unmarshalling CBOR")
if err := cbor.Unmarshal(attObj, &att); err != nil {
return WrapErrorISE(err, "error unmarshalling CBOR")
}
format := att.Format
@ -1174,7 +1117,7 @@ var (
oidTCGKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3}
)
// validateAKCertificate validates the X.509 AK certificate to be
// validateAKCertifiate validates the X.509 AK certificate to be
// in accordance with the required properties. The requirements come from:
// https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements.
//
@ -1183,7 +1126,7 @@ var (
// - The Subject Alternative Name extension MUST be set as defined
// in [TPMv2-EK-Profile] section 3.2.9.
// - The Extended Key Usage extension MUST contain the OID 2.23.133.8.3
// ("joint-iso-itu-t(2) international-organizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)").
// ("joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)").
// - The Basic Constraints extension MUST have the CA component set to false.
// - An Authority Information Access (AIA) extension with entry id-ad-ocsp
// and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as
@ -1506,10 +1449,14 @@ func doStepAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge,
// should be the ARPA address https://datatracker.ietf.org/doc/html/rfc8738#section-6.
// It also references TLS Extensions [RFC6066].
func serverName(ch *Challenge) string {
if ip := net.ParseIP(ch.Value); ip != nil {
return reverseAddr(ip)
var serverName string
ip := net.ParseIP(ch.Value)
if ip != nil {
serverName = reverseAddr(ip)
} else {
serverName = ch.Value
}
return ch.Value
return serverName
}
// reverseaddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP

@ -670,7 +670,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
assert.Equal(t, "zap.internal", updch.Value)
assert.Equal(t, StatusPending, updch.Status)
err := NewError(ErrorConnectionType, "error doing TLS dial for %v: force", ch.Value)
err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443: force", ch.Value)
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
@ -962,16 +962,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
assert.Equal(t, "accID", accountID)
@ -986,100 +984,6 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
},
}
},
"fail/wire-oidc-01-no-wire-db": func(t *testing.T) test {
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
require.NoError(t, err)
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
Key: signerJWK,
}, new(jose.SignerOptions))
require.NoError(t, err)
srv := mustJWKServer(t, signerJWK.Public())
tokenBytes, err := json.Marshal(struct {
jose.Claims
Name string `json:"name,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
KeyAuth string `json:"keyauth"`
ACMEAudience string `json:"acme_aud"`
}{
Claims: jose.Claims{
Issuer: srv.URL,
Audience: []string{"test"},
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
},
Name: "Alice Smith",
PreferredUsername: "wireapp://%40alice_wire@wire.com",
KeyAuth: keyAuth,
ACMEAudience: "https://ca.example.com/acme/wire/challenge/azID/chID",
})
require.NoError(t, err)
signed, err := signer.Sign(tokenBytes)
require.NoError(t, err)
idToken, err := signed.CompactSerialize()
require.NoError(t, err)
payload, err := json.Marshal(struct {
IDToken string `json:"id_token"`
}{
IDToken: idToken,
})
require.NoError(t, err)
valueBytes, err := json.Marshal(struct {
Name string `json:"name,omitempty"`
Domain string `json:"domain,omitempty"`
ClientID string `json:"client-id,omitempty"`
Handle string `json:"handle,omitempty"`
}{
Name: "Alice Smith",
Domain: "wire.com",
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
Handle: "wireapp://%40alice_wire@wire.com",
})
require.NoError(t, err)
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
Wire: &wireprovisioner.Options{
OIDC: &wireprovisioner.OIDCOptions{
Provider: &wireprovisioner.Provider{
IssuerURL: srv.URL,
JWKSURL: srv.URL + "/keys",
Algorithms: []string{"ES256"},
},
Config: &wireprovisioner.Config{
ClientID: "test",
SignatureAlgorithms: []string{"ES256"},
Now: time.Now,
},
TransformTemplate: "",
},
DPOP: &wireprovisioner.DPOPOptions{
SigningKey: []byte(fakeKey),
},
},
}))
ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme"))
return test{
ch: &Challenge{
ID: "chID",
AuthorizationID: "azID",
AccountID: "accID",
Token: "token",
Type: "wire-oidc-01",
Status: StatusPending,
Value: string(valueBytes),
},
srv: srv,
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockDB{},
err: &Error{
Type: "urn:ietf:params:acme:error:serverInternal",
Detail: "The server experienced an internal error",
Status: 500,
Err: errors.New("db *acme.MockDB is not a WireDB"),
},
}
},
"ok/wire-dpop-01": func(t *testing.T) test {
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?
@ -1207,16 +1111,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
assert.Equal(t, "accID", accountID)
@ -1232,141 +1134,6 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
},
}
},
"fail/wire-dpop-01-no-wire-db": func(t *testing.T) test {
jwk, _ := mustAccountAndKeyAuthorization(t, "token")
dpopSigner, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
Key: jwk,
}, new(jose.SignerOptions))
require.NoError(t, err)
signerJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
require.NoError(t, err)
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),
Key: signerJWK,
}, new(jose.SignerOptions))
require.NoError(t, err)
signerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)
require.NoError(t, err)
signerPEMBytes := pem.EncodeToMemory(signerPEMBlock)
dpopBytes, err := json.Marshal(struct {
jose.Claims
Challenge string `json:"chal,omitempty"`
Handle string `json:"handle,omitempty"`
Nonce string `json:"nonce,omitempty"`
HTU string `json:"htu,omitempty"`
Name string `json:"name,omitempty"`
}{
Claims: jose.Claims{
Subject: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"},
},
Challenge: "token",
Handle: "wireapp://%40alice_wire@wire.com",
Nonce: "nonce",
HTU: "http://issuer.example.com",
Name: "Alice Smith",
})
require.NoError(t, err)
dpop, err := dpopSigner.Sign(dpopBytes)
require.NoError(t, err)
proof, err := dpop.CompactSerialize()
require.NoError(t, err)
tokenBytes, err := json.Marshal(struct {
jose.Claims
Challenge string `json:"chal,omitempty"`
Nonce string `json:"nonce,omitempty"`
Cnf struct {
Kid string `json:"kid,omitempty"`
} `json:"cnf"`
Proof string `json:"proof,omitempty"`
ClientID string `json:"client_id"`
APIVersion int `json:"api_version"`
Scope string `json:"scope"`
}{
Claims: jose.Claims{
Issuer: "http://issuer.example.com",
Audience: jose.Audience{"https://ca.example.com/acme/wire/challenge/azID/chID"},
Expiry: jose.NewNumericDate(time.Now().Add(1 * time.Minute)),
},
Challenge: "token",
Nonce: "nonce",
Cnf: struct {
Kid string `json:"kid,omitempty"`
}{
Kid: jwk.KeyID,
},
Proof: proof,
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
APIVersion: 5,
Scope: "wire_client_id",
})
require.NoError(t, err)
signed, err := signer.Sign(tokenBytes)
require.NoError(t, err)
accessToken, err := signed.CompactSerialize()
require.NoError(t, err)
payload, err := json.Marshal(struct {
AccessToken string `json:"access_token"`
}{
AccessToken: accessToken,
})
require.NoError(t, err)
valueBytes, err := json.Marshal(struct {
Name string `json:"name,omitempty"`
Domain string `json:"domain,omitempty"`
ClientID string `json:"client-id,omitempty"`
Handle string `json:"handle,omitempty"`
}{
Name: "Alice Smith",
Domain: "wire.com",
ClientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com",
Handle: "wireapp://%40alice_wire@wire.com",
})
require.NoError(t, err)
ctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{
Wire: &wireprovisioner.Options{
OIDC: &wireprovisioner.OIDCOptions{
Provider: &wireprovisioner.Provider{
IssuerURL: "http://issuerexample.com",
Algorithms: []string{"ES256"},
},
Config: &wireprovisioner.Config{
ClientID: "test",
SignatureAlgorithms: []string{"ES256"},
Now: time.Now,
},
TransformTemplate: "",
},
DPOP: &wireprovisioner.DPOPOptions{
Target: "http://issuer.example.com",
SigningKey: signerPEMBytes,
},
},
}))
ctx = NewLinkerContext(ctx, NewLinker("ca.example.com", "acme"))
return test{
ch: &Challenge{
ID: "chID",
AuthorizationID: "azID",
AccountID: "accID",
Token: "token",
Type: "wire-dpop-01",
Status: StatusPending,
Value: string(valueBytes),
},
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockDB{},
err: &Error{
Type: "urn:ietf:params:acme:error:serverInternal",
Detail: "The server experienced an internal error",
Status: 500,
Err: errors.New("db *acme.MockDB is not a WireDB"),
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
@ -2275,7 +2042,7 @@ func TestTLSALPN01Validate(t *testing.T) {
assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
assert.Equal(t, "zap.internal", updch.Value)
err := NewError(ErrorConnectionType, "error doing TLS dial for %v: force", ch.Value)
err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443: force", ch.Value)
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
@ -2306,7 +2073,7 @@ func TestTLSALPN01Validate(t *testing.T) {
assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
assert.Equal(t, "zap.internal", updch.Value)
err := NewError(ErrorConnectionType, "error doing TLS dial for %v: force", ch.Value)
err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443: force", ch.Value)
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
@ -2338,7 +2105,7 @@ func TestTLSALPN01Validate(t *testing.T) {
assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
assert.Equal(t, "zap.internal", updch.Value)
err := NewError(ErrorConnectionType, "error doing TLS dial for %v: context deadline exceeded", ch.Value)
err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443: context deadline exceeded", ch.Value)
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
@ -3319,34 +3086,14 @@ func Test_serverName(t *testing.T) {
func Test_http01ChallengeHost(t *testing.T) {
tests := []struct {
name string
strictFQDN bool
value string
want string
name string
value string
want string
}{
{
name: "dns",
strictFQDN: false,
value: "www.example.com",
want: "www.example.com",
},
{
name: "dns strict",
strictFQDN: true,
value: "www.example.com",
want: "www.example.com.",
},
{
name: "rooted dns",
strictFQDN: false,
value: "www.example.com.",
want: "www.example.com.",
},
{
name: "rooted dns strict",
strictFQDN: true,
value: "www.example.com.",
want: "www.example.com.",
name: "dns",
value: "www.example.com",
want: "www.example.com",
},
{
name: "ipv4",
@ -3361,11 +3108,6 @@ func Test_http01ChallengeHost(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmp := StrictFQDN
t.Cleanup(func() {
StrictFQDN = tmp
})
StrictFQDN = tt.strictFQDN
if got := http01ChallengeHost(tt.value); got != tt.want {
t.Errorf("http01ChallengeHost() = %v, want %v", got, tt.want)
}
@ -3840,50 +3582,10 @@ func Test_deviceAttest01Validate(t *testing.T) {
AttObj: "?!",
})
require.NoError(t, err)
emptyPayload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString([]byte("")),
})
require.NoError(t, err)
emptyObjectPayload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString([]byte("{}")),
})
require.NoError(t, err)
attObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "step",
AttStatement: map[string]interface{}{
"alg": -7,
"sig": "",
},
})
require.NoError(t, err)
errorNonWellformedCBORPayload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(attObj[:len(attObj)-1]), // cut the CBOR encoded data off
})
require.NoError(t, err)
unsupportedFormatAttObj, err := cbor.Marshal(struct {
Format string `json:"fmt"`
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
}{
Format: "unsupported-format",
AttStatement: map[string]interface{}{
"alg": -7,
"sig": "",
},
})
require.NoError(t, err)
errorUnsupportedFormat, err := json.Marshal(struct {
errorCBORPayload, err := json.Marshal(struct {
AttObj string `json:"attObj"`
}{
AttObj: base64.RawURLEncoding.EncodeToString(unsupportedFormatAttObj),
AttObj: "AAAA",
})
require.NoError(t, err)
type args struct {
@ -4020,7 +3722,7 @@ func Test_deviceAttest01Validate(t *testing.T) {
wantErr: nil,
}
},
"ok/base64-decode": func(t *testing.T) test {
"fail/base64-decode": func(t *testing.T) test {
return test{
args: args{
ch: &Challenge{
@ -4036,29 +3738,13 @@ func Test_deviceAttest01Validate(t *testing.T) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "12345678", updch.Value)
err := NewDetailedError(ErrorBadAttestationStatementType, "failed base64 decoding attObj %q", "?!")
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
assert.Equal(t, err.Detail, updch.Error.Detail)
assert.Equal(t, err.Status, updch.Error.Status)
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
return nil
},
},
payload: errorBase64Payload,
},
wantErr: NewErrorISE("error base64 decoding attObj: illegal base64 data at input byte 0"),
}
},
"ok/empty-attobj": func(t *testing.T) test {
"fail/cbor.Unmarshal": func(t *testing.T) test {
return test{
args: args{
ch: &Challenge{
@ -4074,142 +3760,10 @@ func Test_deviceAttest01Validate(t *testing.T) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "12345678", updch.Value)
err := NewDetailedError(ErrorBadAttestationStatementType, "attObj must not be empty")
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
assert.Equal(t, err.Detail, updch.Error.Detail)
assert.Equal(t, err.Status, updch.Error.Status)
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
return nil
},
},
payload: emptyPayload,
},
}
},
"ok/empty-json-attobj": func(t *testing.T) test {
return test{
args: args{
ch: &Challenge{
ID: "chID",
AuthorizationID: "azID",
Token: "token",
Type: "device-attest-01",
Status: StatusPending,
Value: "12345678",
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "12345678", updch.Value)
err := NewDetailedError(ErrorBadAttestationStatementType, "attObj must not be empty")
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
assert.Equal(t, err.Detail, updch.Error.Detail)
assert.Equal(t, err.Status, updch.Error.Status)
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
return nil
},
},
payload: emptyObjectPayload,
},
}
},
"ok/cborDecoder.Wellformed": func(t *testing.T) test {
return test{
args: args{
ch: &Challenge{
ID: "chID",
AuthorizationID: "azID",
Token: "token",
Type: "device-attest-01",
Status: StatusPending,
Value: "12345678",
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "12345678", updch.Value)
err := NewDetailedError(ErrorBadAttestationStatementType, "attObj is not well formed CBOR: unexpected EOF")
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
assert.Equal(t, err.Detail, updch.Error.Detail)
assert.Equal(t, err.Status, updch.Error.Status)
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
return nil
},
},
payload: errorNonWellformedCBORPayload,
},
}
},
"ok/unsupported-attestation-format": func(t *testing.T) test {
ctx := NewProvisionerContext(context.Background(), mustNonAttestationProvisioner(t))
return test{
args: args{
ctx: ctx,
ch: &Challenge{
ID: "chID",
AuthorizationID: "azID",
Token: "token",
Type: "device-attest-01",
Status: StatusPending,
Value: "12345678",
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
assert.Equal(t, "azID", id)
return &Authorization{ID: "azID"}, nil
},
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
assert.Equal(t, "12345678", updch.Value)
err := NewDetailedError(ErrorBadAttestationStatementType, "unsupported attestation object format %q", "unsupported-format")
assert.EqualError(t, updch.Error.Err, err.Err.Error())
assert.Equal(t, err.Type, updch.Error.Type)
assert.Equal(t, err.Detail, updch.Error.Detail)
assert.Equal(t, err.Status, updch.Error.Status)
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
return nil
},
},
payload: errorUnsupportedFormat,
payload: errorCBORPayload,
},
wantErr: NewErrorISE("error unmarshalling CBOR: cbor:"),
}
},
"ok/prov.IsAttestationFormatEnabled": func(t *testing.T) test {
@ -5066,59 +4620,3 @@ func createSubjectAltNameExtension(dnsNames, emailAddresses x509util.MultiString
Value: rawBytes,
}, nil
}
func Test_tlsAlpn01ChallengeHost(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
strictFQDN bool
args args
want string
}{
{"dns", false, args{"smallstep.com"}, "smallstep.com"},
{"dns strict", true, args{"smallstep.com"}, "smallstep.com."},
{"rooted dns", false, args{"smallstep.com."}, "smallstep.com."},
{"rooted dns strict", true, args{"smallstep.com."}, "smallstep.com."},
{"ipv4", true, args{"1.2.3.4"}, "1.2.3.4"},
{"ipv6", true, args{"2607:f8b0:4023:1009::71"}, "2607:f8b0:4023:1009::71"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmp := StrictFQDN
t.Cleanup(func() {
StrictFQDN = tmp
})
StrictFQDN = tt.strictFQDN
assert.Equal(t, tt.want, tlsAlpn01ChallengeHost(tt.args.name))
})
}
}
func Test_dns01ChallengeHost(t *testing.T) {
type args struct {
domain string
}
tests := []struct {
name string
strictFQDN bool
args args
want string
}{
{"dns", false, args{"smallstep.com"}, "_acme-challenge.smallstep.com"},
{"dns strict", true, args{"smallstep.com"}, "_acme-challenge.smallstep.com."},
{"rooted dns", false, args{"smallstep.com."}, "_acme-challenge.smallstep.com."},
{"rooted dns strict", true, args{"smallstep.com."}, "_acme-challenge.smallstep.com."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmp := StrictFQDN
t.Cleanup(func() {
StrictFQDN = tmp
})
StrictFQDN = tt.strictFQDN
assert.Equal(t, tt.want, dns01ChallengeHost(tt.args.domain))
})
}
}

@ -29,7 +29,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
type test struct {
ch *Challenge
jwk *jose.JSONWebKey
db WireDB
db DB
payload []byte
ctx context.Context
expectedErr *Error
@ -38,7 +38,6 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
"fail/no-provisioner": func(t *testing.T) test {
return test{
ctx: context.Background(),
db: &MockWireDB{},
expectedErr: &Error{
Type: "urn:ietf:params:acme:error:serverInternal",
Detail: "The server experienced an internal error",
@ -69,7 +68,6 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
}))
return test{
ctx: ctx,
db: &MockWireDB{},
expectedErr: &Error{
Type: "urn:ietf:params:acme:error:serverInternal",
Detail: "The server experienced an internal error",
@ -111,7 +109,6 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
Status: StatusPending,
Value: "1234",
},
db: &MockWireDB{},
expectedErr: &Error{
Type: "urn:ietf:params:acme:error:malformed",
Detail: "The request message was malformed",
@ -153,7 +150,6 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
Status: StatusPending,
Value: "1234",
},
db: &MockWireDB{},
expectedErr: &Error{
Type: "urn:ietf:params:acme:error:serverInternal",
Detail: "The server experienced an internal error",
@ -207,7 +203,6 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
Status: StatusPending,
Value: string(valueBytes),
},
db: &MockWireDB{},
expectedErr: &Error{
Type: "urn:ietf:params:acme:error:serverInternal",
Detail: "The server experienced an internal error",
@ -264,27 +259,25 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
Status: StatusPending,
Value: string(valueBytes),
},
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error {
assert.Equal(t, "chID", ch.ID)
assert.Equal(t, "azID", ch.AuthorizationID)
assert.Equal(t, "accID", ch.AccountID)
assert.Equal(t, "token", ch.Token)
assert.Equal(t, ChallengeType("wire-dpop-01"), ch.Type)
assert.Equal(t, StatusInvalid, ch.Status)
assert.Equal(t, string(valueBytes), ch.Value)
if assert.NotNil(t, ch.Error) {
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
if errors.As(ch.Error, &k) {
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
assert.Equal(t, 400, k.Status)
assert.Equal(t, `failed validating Wire access token: failed parsing token: go-jose/go-jose: compact JWS format must have three parts`, k.Err.Error())
}
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error {
assert.Equal(t, "chID", ch.ID)
assert.Equal(t, "azID", ch.AuthorizationID)
assert.Equal(t, "accID", ch.AccountID)
assert.Equal(t, "token", ch.Token)
assert.Equal(t, ChallengeType("wire-dpop-01"), ch.Type)
assert.Equal(t, StatusInvalid, ch.Status)
assert.Equal(t, string(valueBytes), ch.Value)
if assert.NotNil(t, ch.Error) {
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
if errors.As(ch.Error, &k) {
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
assert.Equal(t, 400, k.Status)
assert.Equal(t, `failed validating Wire access token: failed parsing token: go-jose/go-jose: compact JWS format must have three parts`, k.Err.Error())
}
return nil
},
}
return nil
},
},
}
@ -417,16 +410,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return errors.New("fail")
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return errors.New("fail")
},
},
expectedErr: &Error{
@ -565,16 +556,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
assert.Equal(t, "accID", accountID)
@ -717,16 +706,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
assert.Equal(t, "accID", accountID)
@ -869,16 +856,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
assert.Equal(t, "accID", accountID)
@ -1027,16 +1012,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-dpop-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
assert.Equal(t, "accID", accountID)
@ -1082,7 +1065,7 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
type test struct {
ch *Challenge
jwk *jose.JSONWebKey
db WireDB
db DB
payload []byte
srv *httptest.Server
ctx context.Context
@ -1092,7 +1075,6 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
"fail/no-provisioner": func(t *testing.T) test {
return test{
ctx: context.Background(),
db: &MockWireDB{},
expectedErr: &Error{
Type: "urn:ietf:params:acme:error:serverInternal",
Detail: "The server experienced an internal error",
@ -1123,7 +1105,6 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
}))
return test{
ctx: ctx,
db: &MockWireDB{},
expectedErr: &Error{
Type: "urn:ietf:params:acme:error:serverInternal",
Detail: "The server experienced an internal error",
@ -1165,12 +1146,10 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
Status: StatusPending,
Value: "1234",
},
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error {
assert.Equal(t, "chID", ch.ID)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, ch *Challenge) error {
assert.Equal(t, "chID", ch.ID)
return nil
},
},
expectedErr: &Error{
@ -1214,7 +1193,6 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
Status: StatusPending,
Value: "1234",
},
db: &MockWireDB{},
expectedErr: &Error{
Type: "urn:ietf:params:acme:error:serverInternal",
Detail: "The server experienced an internal error",
@ -1310,25 +1288,23 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
if assert.NotNil(t, updch.Error) {
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
if errors.As(updch.Error, &k) {
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
assert.Equal(t, 400, k.Status)
assert.Equal(t, `error verifying ID token signature: failed to verify signature: failed to verify id token signature`, k.Err.Error())
}
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
if assert.NotNil(t, updch.Error) {
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
if errors.As(updch.Error, &k) {
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
assert.Equal(t, 400, k.Status)
assert.Equal(t, `error verifying ID token signature: failed to verify signature: failed to verify id token signature`, k.Err.Error())
}
return nil
},
}
return nil
},
},
}
@ -1418,25 +1394,23 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
if assert.NotNil(t, updch.Error) {
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
if errors.As(updch.Error, &k) {
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
assert.Equal(t, 400, k.Status)
assert.Contains(t, k.Err.Error(), "keyAuthorization does not match")
}
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
if assert.NotNil(t, updch.Error) {
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
if errors.As(updch.Error, &k) {
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
assert.Equal(t, 400, k.Status)
assert.Contains(t, k.Err.Error(), "keyAuthorization does not match")
}
return nil
},
}
return nil
},
},
}
@ -1526,25 +1500,23 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
if assert.NotNil(t, updch.Error) {
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
if errors.As(updch.Error, &k) {
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
assert.Equal(t, 400, k.Status)
assert.Equal(t, `claims in OIDC ID token don't match: invalid 'preferred_username' "wireapp://%40bob@wire.com" after transformation`, k.Err.Error())
}
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusInvalid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
if assert.NotNil(t, updch.Error) {
var k *Error // NOTE: the error is not returned up, but stored with the challenge instead
if errors.As(updch.Error, &k) {
assert.Equal(t, "urn:ietf:params:acme:error:rejectedIdentifier", k.Type)
assert.Equal(t, "The server will not issue certificates for the identifier", k.Detail)
assert.Equal(t, 400, k.Status)
assert.Equal(t, `claims in OIDC ID token don't match: invalid 'preferred_username' "wireapp://%40bob@wire.com" after transformation`, k.Err.Error())
}
return nil
},
}
return nil
},
},
}
@ -1634,16 +1606,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return errors.New("fail")
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return errors.New("fail")
},
},
expectedErr: &Error{
@ -1739,16 +1709,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
assert.Equal(t, "accID", accountID)
@ -1848,16 +1816,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
assert.Equal(t, "accID", accountID)
@ -1957,16 +1923,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
assert.Equal(t, "accID", accountID)
@ -2072,16 +2036,14 @@ MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
payload: payload,
ctx: ctx,
jwk: jwk,
db: &MockWireDB{
MockDB: MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
db: &MockDB{
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
assert.Equal(t, "chID", updch.ID)
assert.Equal(t, "token", updch.Token)
assert.Equal(t, StatusValid, updch.Status)
assert.Equal(t, ChallengeType("wire-oidc-01"), updch.Type)
assert.Equal(t, string(valueBytes), updch.Value)
return nil
},
MockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {
assert.Equal(t, "accID", accountID)

@ -130,7 +130,7 @@ func (m *MockProvisioner) GetName() string {
return m.Mret1.(string)
}
// AuthorizeOrderIdentifier mock
// AuthorizeOrderIdentifiers mock
func (m *MockProvisioner) AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error {
if m.MauthorizeOrderIdentifier != nil {
return m.MauthorizeOrderIdentifier(ctx, identifier)

@ -2,7 +2,6 @@ package acme
import (
"context"
"database/sql"
"github.com/pkg/errors"
)
@ -16,7 +15,7 @@ var ErrNotFound = errors.New("not found")
// IsErrNotFound returns true if the error is a "not found" error. Returns false
// otherwise.
func IsErrNotFound(err error) bool {
return errors.Is(err, ErrNotFound) || errors.Is(err, sql.ErrNoRows)
return errors.Is(err, ErrNotFound)
}
// DB is the DB interface expected by the step-ca ACME API.
@ -54,14 +53,8 @@ type DB interface {
GetOrder(ctx context.Context, id string) (*Order, error)
GetOrdersByAccountID(ctx context.Context, accountID string) ([]string, error)
UpdateOrder(ctx context.Context, o *Order) error
}
// WireDB is the interface used for operations on ACME Orders for Wire identifiers. This
// is not a general purpose interface, and it should only be used when Wire identifiers
// are enabled in the CA configuration. Currently it provides a runtime assertion only;
// not at compile time.
type WireDB interface {
DB
// TODO(hs): put in a different interface
GetAllOrdersByAccountID(ctx context.Context, accountID string) ([]string, error)
CreateDpopToken(ctx context.Context, orderID string, dpop map[string]interface{}) error
GetDpopToken(ctx context.Context, orderID string) (map[string]interface{}, error)
@ -132,20 +125,14 @@ type MockDB struct {
MockGetOrdersByAccountID func(ctx context.Context, accountID string) ([]string, error)
MockUpdateOrder func(ctx context.Context, o *Order) error
MockRet1 interface{}
MockError error
}
// MockWireDB is an implementation of the WireDB interface that should only be used as
// a mock in tests. It embeds the MockDB, as it is an extension of the existing database
// methods.
type MockWireDB struct {
MockDB
MockGetAllOrdersByAccountID func(ctx context.Context, accountID string) ([]string, error)
MockGetDpopToken func(ctx context.Context, orderID string) (map[string]interface{}, error)
MockCreateDpopToken func(ctx context.Context, orderID string, dpop map[string]interface{}) error
MockGetOidcToken func(ctx context.Context, orderID string) (map[string]interface{}, error)
MockCreateOidcToken func(ctx context.Context, orderID string, idToken map[string]interface{}) error
MockRet1 interface{}
MockError error
}
// CreateAccount mock.
@ -419,7 +406,7 @@ func (m *MockDB) GetOrdersByAccountID(ctx context.Context, accID string) ([]stri
}
// GetAllOrdersByAccountID returns a list of any order IDs owned by the account.
func (m *MockWireDB) GetAllOrdersByAccountID(ctx context.Context, accountID string) ([]string, error) {
func (m *MockDB) GetAllOrdersByAccountID(ctx context.Context, accountID string) ([]string, error) {
if m.MockGetAllOrdersByAccountID != nil {
return m.MockGetAllOrdersByAccountID(ctx, accountID)
} else if m.MockError != nil {
@ -429,7 +416,7 @@ func (m *MockWireDB) GetAllOrdersByAccountID(ctx context.Context, accountID stri
}
// GetDpop retrieves a DPoP from the database.
func (m *MockWireDB) GetDpopToken(ctx context.Context, orderID string) (map[string]any, error) {
func (m *MockDB) GetDpopToken(ctx context.Context, orderID string) (map[string]any, error) {
if m.MockGetDpopToken != nil {
return m.MockGetDpopToken(ctx, orderID)
} else if m.MockError != nil {
@ -439,7 +426,7 @@ func (m *MockWireDB) GetDpopToken(ctx context.Context, orderID string) (map[stri
}
// CreateDpop creates DPoP resources and saves them to the DB.
func (m *MockWireDB) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]any) error {
func (m *MockDB) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]any) error {
if m.MockCreateDpopToken != nil {
return m.MockCreateDpopToken(ctx, orderID, dpop)
}
@ -447,7 +434,7 @@ func (m *MockWireDB) CreateDpopToken(ctx context.Context, orderID string, dpop m
}
// GetOidcToken retrieves an oidc token from the database.
func (m *MockWireDB) GetOidcToken(ctx context.Context, orderID string) (map[string]any, error) {
func (m *MockDB) GetOidcToken(ctx context.Context, orderID string) (map[string]any, error) {
if m.MockGetOidcToken != nil {
return m.MockGetOidcToken(ctx, orderID)
} else if m.MockError != nil {
@ -457,7 +444,7 @@ func (m *MockWireDB) GetOidcToken(ctx context.Context, orderID string) (map[stri
}
// CreateOidcToken creates oidc token resources and saves them to the DB.
func (m *MockWireDB) CreateOidcToken(ctx context.Context, orderID string, idToken map[string]any) error {
func (m *MockDB) CreateOidcToken(ctx context.Context, orderID string, idToken map[string]any) error {
if m.MockCreateOidcToken != nil {
return m.MockCreateOidcToken(ctx, orderID, idToken)
}

@ -18,7 +18,6 @@ type dbAccount struct {
Contact []string `json:"contact,omitempty"`
Status acme.Status `json:"status"`
LocationPrefix string `json:"locationPrefix"`
ProvisionerID string `json:"provisionerID,omitempty"`
ProvisionerName string `json:"provisionerName"`
CreatedAt time.Time `json:"createdAt"`
DeactivatedAt time.Time `json:"deactivatedAt"`
@ -70,7 +69,6 @@ func (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error)
Key: dbacc.Key,
ID: dbacc.ID,
LocationPrefix: dbacc.LocationPrefix,
ProvisionerID: dbacc.ProvisionerID,
ProvisionerName: dbacc.ProvisionerName,
}, nil
}
@ -99,7 +97,6 @@ func (db *DB) CreateAccount(ctx context.Context, acc *acme.Account) error {
Status: acc.Status,
CreatedAt: clock.Now(),
LocationPrefix: acc.LocationPrefix,
ProvisionerID: acc.ProvisionerID,
ProvisionerName: acc.ProvisionerName,
}

@ -68,14 +68,12 @@ func TestDB_getDBAccount(t *testing.T) {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
dbacc := &dbAccount{
ID: accID,
Status: acme.StatusDeactivated,
CreatedAt: now,
DeactivatedAt: now,
Contact: []string{"foo", "bar"},
Key: jwk,
ProvisionerID: "73d2c0f1-9753-448b-9b48-bf00fe434681",
ProvisionerName: "acme",
ID: accID,
Status: acme.StatusDeactivated,
CreatedAt: now,
DeactivatedAt: now,
Contact: []string{"foo", "bar"},
Key: jwk,
}
b, err := json.Marshal(dbacc)
assert.FatalError(t, err)

@ -1,32 +0,0 @@
package acme
import (
"database/sql"
"errors"
"fmt"
"testing"
)
func TestIsErrNotFound(t *testing.T) {
type args struct {
err error
}
tests := []struct {
name string
args args
want bool
}{
{"true ErrNotFound", args{ErrNotFound}, true},
{"true sql.ErrNoRows", args{sql.ErrNoRows}, true},
{"true wrapped ErrNotFound", args{fmt.Errorf("something failed: %w", ErrNotFound)}, true},
{"true wrapped sql.ErrNoRows", args{fmt.Errorf("something failed: %w", sql.ErrNoRows)}, true},
{"false other", args{errors.New("not found")}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsErrNotFound(tt.args.err); got != tt.want {
t.Errorf("IsErrNotFound() = %v, want %v", got, tt.want)
}
})
}
}

@ -294,14 +294,14 @@ type Subproblem struct {
}
// NewError creates a new Error.
func NewError(pt ProblemType, msg string, args ...any) *Error {
func NewError(pt ProblemType, msg string, args ...interface{}) *Error {
return newError(pt, errors.Errorf(msg, args...))
}
// NewDetailedError creates a new Error that includes the error
// message in the details, providing more information to the
// ACME client.
func NewDetailedError(pt ProblemType, msg string, args ...any) *Error {
func NewDetailedError(pt ProblemType, msg string, args ...interface{}) *Error {
return NewError(pt, msg, args...).withDetail()
}
@ -324,7 +324,7 @@ func (e *Error) AddSubproblems(subproblems ...Subproblem) *Error {
// NewSubproblem creates a new Subproblem. The msg and args
// are used to create a new error, which is set as the Detail, allowing
// for more detailed error messages to be returned to the ACME client.
func NewSubproblem(pt ProblemType, msg string, args ...any) Subproblem {
func NewSubproblem(pt ProblemType, msg string, args ...interface{}) Subproblem {
e := newError(pt, fmt.Errorf(msg, args...))
s := Subproblem{
Type: e.Type,
@ -335,7 +335,7 @@ func NewSubproblem(pt ProblemType, msg string, args ...any) Subproblem {
// NewSubproblemWithIdentifier creates a new Subproblem with a specific ACME
// Identifier. It calls NewSubproblem and sets the Identifier.
func NewSubproblemWithIdentifier(pt ProblemType, identifier Identifier, msg string, args ...any) Subproblem {
func NewSubproblemWithIdentifier(pt ProblemType, identifier Identifier, msg string, args ...interface{}) Subproblem {
s := NewSubproblem(pt, msg, args...)
s.Identifier = &identifier
return s
@ -362,12 +362,12 @@ func newError(pt ProblemType, err error) *Error {
}
// NewErrorISE creates a new ErrorServerInternalType Error.
func NewErrorISE(msg string, args ...any) *Error {
func NewErrorISE(msg string, args ...interface{}) *Error {
return NewError(ErrorServerInternalType, msg, args...)
}
// WrapError attempts to wrap the internal error.
func WrapError(typ ProblemType, err error, msg string, args ...any) *Error {
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
var e *Error
switch {
case err == nil:
@ -384,12 +384,12 @@ func WrapError(typ ProblemType, err error, msg string, args ...any) *Error {
}
}
func WrapDetailedError(typ ProblemType, err error, msg string, args ...any) *Error {
func WrapDetailedError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
return WrapError(typ, err, msg, args...).withDetail()
}
// WrapErrorISE shortcut to wrap an internal server error type.
func WrapErrorISE(err error, msg string, args ...any) *Error {
func WrapErrorISE(err error, msg string, args ...interface{}) *Error {
return WrapError(ErrorServerInternalType, err, msg, args...)
}
@ -415,7 +415,7 @@ func (e *Error) Cause() error {
}
// ToLog implements the EnableLogger interface.
func (e *Error) ToLog() (any, error) {
func (e *Error) ToLog() (interface{}, error) {
b, err := json.Marshal(e)
if err != nil {
return nil, WrapErrorISE(err, "error marshaling acme.Error for logging")
@ -424,7 +424,7 @@ func (e *Error) ToLog() (any, error) {
}
// Render implements render.RenderableError for Error.
func (e *Error) Render(w http.ResponseWriter, r *http.Request) {
func (e *Error) Render(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/problem+json")
render.JSONStatus(w, r, e, e.StatusCode())
render.JSONStatus(w, e, e.StatusCode())
}

@ -186,19 +186,19 @@ func (l *linker) Middleware(next http.Handler) http.Handler {
nameEscaped := chi.URLParam(r, "provisionerID")
name, err := url.PathUnescape(nameEscaped)
if err != nil {
render.Error(w, r, WrapErrorISE(err, "error url unescaping provisioner name '%s'", nameEscaped))
render.Error(w, WrapErrorISE(err, "error url unescaping provisioner name '%s'", nameEscaped))
return
}
p, err := authority.MustFromContext(ctx).LoadProvisionerByName(name)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
acmeProv, ok := p.(*provisioner.ACME)
if !ok {
render.Error(w, r, NewError(ErrorAccountDoesNotExistType, "provisioner must be of type ACME"))
render.Error(w, NewError(ErrorAccountDoesNotExistType, "provisioner must be of type ACME"))
return
}

@ -138,7 +138,7 @@ func (o *Order) UpdateStatus(ctx context.Context, db DB) error {
return nil
}
// getAuthorizationFingerprint returns a fingerprint from the list of authorizations. This
// getKeyFingerprint returns a fingerprint from the list of authorizations. This
// fingerprint is used on the device-attest-01 flow to verify the attestation
// certificate public key with the CSR public key.
//
@ -208,10 +208,6 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
// Template data
data := x509util.NewTemplateData()
if o.containsWireIdentifiers() {
wireDB, ok := db.(WireDB)
if !ok {
return fmt.Errorf("db %T is not a WireDB", db)
}
subject, err := createWireSubject(o, csr)
if err != nil {
return fmt.Errorf("failed creating Wire subject: %w", err)
@ -219,13 +215,13 @@ func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateReques
data.SetSubject(subject)
// Inject Wire's custom challenges into the template once they have been validated
dpop, err := wireDB.GetDpopToken(ctx, o.ID)
dpop, err := db.GetDpopToken(ctx, o.ID)
if err != nil {
return fmt.Errorf("failed getting Wire DPoP token: %w", err)
}
data.Set("Dpop", dpop)
oidc, err := wireDB.GetOidcToken(ctx, o.ID)
oidc, err := db.GetOidcToken(ctx, o.ID)
if err != nil {
return fmt.Errorf("failed getting Wire OIDC token: %w", err)
}
@ -571,7 +567,7 @@ func uniqueSortedLowerNames(names []string) (unique []string) {
}
unique = make([]string, 0, len(nameMap))
for name := range nameMap {
if name != "" {
if len(name) > 0 {
unique = append(unique, name)
}
}

@ -4,8 +4,9 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"go.step.sm/crypto/kms/uri"
)
type UserID struct {
@ -70,7 +71,7 @@ type ClientID struct {
//
// where '!' is used as a separator between the user id & device id.
func ParseClientID(clientID string) (ClientID, error) {
clientIDURI, err := url.Parse(clientID)
clientIDURI, err := uri.Parse(clientID)
if err != nil {
return ClientID{}, fmt.Errorf("invalid Wire client ID URI %q: %w", clientID, err)
}

@ -81,7 +81,7 @@ func TestParseClientID(t *testing.T) {
expectedErr error
}{
{name: "ok", clientID: "wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com", want: ClientID{Scheme: "wireapp", Username: "CzbfFjDOQrenCbDxVmgnFw", DeviceID: "594930e9d50bb175", Domain: "wire.com"}},
{name: "fail/uri", clientID: "bla", expectedErr: errors.New(`invalid Wire client ID scheme ""; expected "wireapp"`)},
{name: "fail/uri", clientID: "bla", expectedErr: errors.New(`invalid Wire client ID URI "bla": error parsing bla: scheme is missing`)},
{name: "fail/scheme", clientID: "not-wireapp://bla.com", expectedErr: errors.New(`invalid Wire client ID scheme "not-wireapp"; expected "wireapp"`)},
{name: "fail/username", clientID: "wireapp://user@wire.com", expectedErr: errors.New(`invalid Wire client ID username "user"`)},
}

@ -4,7 +4,7 @@ import (
"bytes"
"context"
"crypto"
"crypto/dsa" // support legacy algorithms
"crypto/dsa" //nolint:staticcheck // support legacy algorithms
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
@ -52,7 +52,6 @@ type Authority interface {
Revoke(context.Context, *authority.RevokeOptions) error
GetEncryptedKey(kid string) (string, error)
GetRoots() ([]*x509.Certificate, error)
GetIntermediateCertificates() []*x509.Certificate
GetFederation() ([]*x509.Certificate, error)
Version() authority.Version
GetCertificateRevocationList() (*authority.CertificateRevocationListInfo, error)
@ -296,11 +295,6 @@ type RootsResponse struct {
Certificates []Certificate `json:"crts"`
}
// IntermediatesResponse is the response object of the intermediates request.
type IntermediatesResponse struct {
Certificates []Certificate `json:"crts"`
}
// FederationResponse is the response object of the federation request.
type FederationResponse struct {
Certificates []Certificate `json:"crts"`
@ -336,10 +330,7 @@ func Route(r Router) {
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
r.MethodFunc("GET", "/roots", Roots)
r.MethodFunc("GET", "/roots.pem", RootsPEM)
r.MethodFunc("GET", "/intermediates", Intermediates)
r.MethodFunc("GET", "/intermediates.pem", IntermediatesPEM)
r.MethodFunc("GET", "/federation", Federation)
// SSH CA
r.MethodFunc("POST", "/ssh/sign", SSHSign)
r.MethodFunc("POST", "/ssh/renew", SSHRenew)
@ -362,15 +353,15 @@ func Route(r Router) {
// Version is an HTTP handler that returns the version of the server.
func Version(w http.ResponseWriter, r *http.Request) {
v := mustAuthority(r.Context()).Version()
render.JSON(w, r, VersionResponse{
render.JSON(w, VersionResponse{
Version: v.Version,
RequireClientAuthentication: v.RequireClientAuthentication,
})
}
// Health is an HTTP handler that returns the status of the server.
func Health(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, HealthResponse{Status: "ok"})
func Health(w http.ResponseWriter, _ *http.Request) {
render.JSON(w, HealthResponse{Status: "ok"})
}
// Root is an HTTP handler that using the SHA256 from the URL, returns the root
@ -381,11 +372,11 @@ func Root(w http.ResponseWriter, r *http.Request) {
// Load root certificate with the
cert, err := mustAuthority(r.Context()).Root(sum)
if err != nil {
render.Error(w, r, errs.Wrapf(http.StatusNotFound, err, "%s was not found", r.RequestURI))
render.Error(w, errs.Wrapf(http.StatusNotFound, err, "%s was not found", r.RequestURI))
return
}
render.JSON(w, r, &RootResponse{RootPEM: Certificate{cert}})
render.JSON(w, &RootResponse{RootPEM: Certificate{cert}})
}
func certChainToPEM(certChain []*x509.Certificate) []Certificate {
@ -400,17 +391,17 @@ func certChainToPEM(certChain []*x509.Certificate) []Certificate {
func Provisioners(w http.ResponseWriter, r *http.Request) {
cursor, limit, err := ParseCursor(r)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
p, next, err := mustAuthority(r.Context()).GetProvisioners(cursor, limit)
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
render.JSON(w, r, &ProvisionersResponse{
render.JSON(w, &ProvisionersResponse{
Provisioners: p,
NextCursor: next,
})
@ -421,18 +412,18 @@ func ProvisionerKey(w http.ResponseWriter, r *http.Request) {
kid := chi.URLParam(r, "kid")
key, err := mustAuthority(r.Context()).GetEncryptedKey(kid)
if err != nil {
render.Error(w, r, errs.NotFoundErr(err))
render.Error(w, errs.NotFoundErr(err))
return
}
render.JSON(w, r, &ProvisionerKeyResponse{key})
render.JSON(w, &ProvisionerKeyResponse{key})
}
// Roots returns all the root certificates for the CA.
func Roots(w http.ResponseWriter, r *http.Request) {
roots, err := mustAuthority(r.Context()).GetRoots()
if err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error getting roots"))
render.Error(w, errs.ForbiddenErr(err, "error getting roots"))
return
}
@ -441,7 +432,7 @@ func Roots(w http.ResponseWriter, r *http.Request) {
certs[i] = Certificate{roots[i]}
}
render.JSONStatus(w, r, &RootsResponse{
render.JSONStatus(w, &RootsResponse{
Certificates: certs,
}, http.StatusCreated)
}
@ -450,7 +441,7 @@ func Roots(w http.ResponseWriter, r *http.Request) {
func RootsPEM(w http.ResponseWriter, r *http.Request) {
roots, err := mustAuthority(r.Context()).GetRoots()
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
@ -463,48 +454,7 @@ func RootsPEM(w http.ResponseWriter, r *http.Request) {
})
if _, err := w.Write(block); err != nil {
log.Error(w, r, err)
return
}
}
}
// Intermediates returns all the intermediate certificates of the CA.
func Intermediates(w http.ResponseWriter, r *http.Request) {
intermediates := mustAuthority(r.Context()).GetIntermediateCertificates()
if len(intermediates) == 0 {
render.Error(w, r, errs.NotImplemented("error getting intermediates: method not implemented"))
return
}
certs := make([]Certificate, len(intermediates))
for i := range intermediates {
certs[i] = Certificate{intermediates[i]}
}
render.JSONStatus(w, r, &IntermediatesResponse{
Certificates: certs,
}, http.StatusCreated)
}
// IntermediatesPEM returns all the intermediate certificates for the CA in PEM format.
func IntermediatesPEM(w http.ResponseWriter, r *http.Request) {
intermediates := mustAuthority(r.Context()).GetIntermediateCertificates()
if len(intermediates) == 0 {
render.Error(w, r, errs.NotImplemented("error getting intermediates: method not implemented"))
return
}
w.Header().Set("Content-Type", "application/x-pem-file")
for _, crt := range intermediates {
block := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: crt.Raw,
})
if _, err := w.Write(block); err != nil {
log.Error(w, r, err)
log.Error(w, err)
return
}
}
@ -514,7 +464,7 @@ func IntermediatesPEM(w http.ResponseWriter, r *http.Request) {
func Federation(w http.ResponseWriter, r *http.Request) {
federated, err := mustAuthority(r.Context()).GetFederation()
if err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error getting federated roots"))
render.Error(w, errs.ForbiddenErr(err, "error getting federated roots"))
return
}
@ -523,7 +473,7 @@ func Federation(w http.ResponseWriter, r *http.Request) {
certs[i] = Certificate{federated[i]}
}
render.JSONStatus(w, r, &FederationResponse{
render.JSONStatus(w, &FederationResponse{
Certificates: certs,
}, http.StatusCreated)
}
@ -615,7 +565,7 @@ func LogSSHCertificate(w http.ResponseWriter, cert *ssh.Certificate) {
func ParseCursor(r *http.Request) (cursor string, limit int, err error) {
q := r.URL.Query()
cursor = q.Get("cursor")
if v := q.Get("limit"); v != "" {
if v := q.Get("limit"); len(v) > 0 {
limit, err = strconv.Atoi(v)
if err != nil {
return "", 0, errs.BadRequestErr(err, "limit '%s' is not an integer", v)

@ -31,7 +31,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/x509util"
"golang.org/x/crypto/ssh"
@ -148,13 +147,6 @@ nIHOI54lAqDeF7A0y73fPRVCiJEWmuxz0g==
privKey = "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiNEhBYjE0WDQ5OFM4LWxSb29JTnpqZyJ9.RbkJXGzI3kOsaP20KmZs0ELFLgpRddAE49AJHlEblw-uH_gg6SV3QA.M3MArEpHgI171lhm.gBlFySpzK9F7riBJbtLSNkb4nAw_gWokqs1jS-ZK1qxuqTK-9mtX5yILjRnftx9P9uFp5xt7rvv4Mgom1Ed4V9WtIyfNP_Cz3Pme1Eanp5nY68WCe_yG6iSB1RJdMDBUb2qBDZiBdhJim1DRXsOfgedOrNi7GGbppMlD77DEpId118owR5izA-c6Q_hg08hIE3tnMAnebDNQoF9jfEY99_AReVRH8G4hgwZEPCfXMTb3J-lowKGG4vXIbK5knFLh47SgOqG4M2M51SMS-XJ7oBz1Vjoamc90QIqKV51rvZ5m0N_sPFtxzcfV4E9yYH3XVd4O-CG4ydVKfKVyMtQ.mcKFZqBHp_n7Ytj2jz9rvw"
)
func mustJSON(t *testing.T, v any) []byte {
t.Helper()
var buf bytes.Buffer
require.NoError(t, json.NewEncoder(&buf).Encode(v))
return buf.Bytes()
}
func parseCertificate(data string) *x509.Certificate {
block, _ := pem.Decode([]byte(data))
if block == nil {
@ -207,7 +199,6 @@ type mockAuthority struct {
revoke func(context.Context, *authority.RevokeOptions) error
getEncryptedKey func(kid string) (string, error)
getRoots func() ([]*x509.Certificate, error)
getIntermediateCertificates func() []*x509.Certificate
getFederation func() ([]*x509.Certificate, error)
getCRL func() (*authority.CertificateRevocationListInfo, error)
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
@ -330,13 +321,6 @@ func (m *mockAuthority) GetRoots() ([]*x509.Certificate, error) {
return m.ret1.([]*x509.Certificate), m.err
}
func (m *mockAuthority) GetIntermediateCertificates() []*x509.Certificate {
if m.getIntermediateCertificates != nil {
return m.getIntermediateCertificates()
}
return m.ret1.([]*x509.Certificate)
}
func (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) {
if m.getFederation != nil {
return m.getFederation()
@ -1674,83 +1658,3 @@ func TestLogSSHCertificate(t *testing.T) {
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"])
}
func TestIntermediates(t *testing.T) {
ca, err := minica.New()
require.NoError(t, err)
getRequest := func(t *testing.T, crt []*x509.Certificate) *http.Request {
mockMustAuthority(t, &mockAuthority{
ret1: crt,
})
return httptest.NewRequest("GET", "/intermediates", http.NoBody)
}
type args struct {
crts []*x509.Certificate
}
tests := []struct {
name string
args args
wantStatusCode int
wantBody []byte
}{
{"ok", args{[]*x509.Certificate{ca.Intermediate}}, http.StatusCreated, mustJSON(t, IntermediatesResponse{
Certificates: []Certificate{{ca.Intermediate}},
})},
{"ok multiple", args{[]*x509.Certificate{ca.Root, ca.Intermediate}}, http.StatusCreated, mustJSON(t, IntermediatesResponse{
Certificates: []Certificate{{ca.Root}, {ca.Intermediate}},
})},
{"fail", args{}, http.StatusNotImplemented, mustJSON(t, errs.NotImplemented("not implemented"))},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
r := getRequest(t, tt.args.crts)
Intermediates(w, r)
assert.Equal(t, tt.wantStatusCode, w.Result().StatusCode)
assert.Equal(t, tt.wantBody, w.Body.Bytes())
})
}
}
func TestIntermediatesPEM(t *testing.T) {
ca, err := minica.New()
require.NoError(t, err)
getRequest := func(t *testing.T, crt []*x509.Certificate) *http.Request {
mockMustAuthority(t, &mockAuthority{
ret1: crt,
})
return httptest.NewRequest("GET", "/intermediates.pem", http.NoBody)
}
type args struct {
crts []*x509.Certificate
}
tests := []struct {
name string
args args
wantStatusCode int
wantBody []byte
}{
{"ok", args{[]*x509.Certificate{ca.Intermediate}}, http.StatusOK, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Bytes: ca.Intermediate.Raw,
})},
{"ok multiple", args{[]*x509.Certificate{ca.Root, ca.Intermediate}}, http.StatusOK, append(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Bytes: ca.Root.Raw,
}), pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Bytes: ca.Intermediate.Raw,
})...)},
{"fail", args{}, http.StatusNotImplemented, mustJSON(t, errs.NotImplemented("not implemented"))},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
r := getRequest(t, tt.args.crts)
IntermediatesPEM(w, r)
assert.Equal(t, tt.wantStatusCode, w.Result().StatusCode)
assert.Equal(t, tt.wantBody, w.Body.Bytes())
})
}
}

@ -13,12 +13,12 @@ import (
func CRL(w http.ResponseWriter, r *http.Request) {
crlInfo, err := mustAuthority(r.Context()).GetCertificateRevocationList()
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
if crlInfo == nil {
render.Error(w, r, errs.New(http.StatusNotFound, "no CRL available"))
render.Error(w, errs.New(http.StatusNotFound, "no CRL available"))
return
}

@ -2,7 +2,6 @@
package log
import (
"context"
"fmt"
"net/http"
"os"
@ -10,29 +9,6 @@ import (
"github.com/pkg/errors"
)
type errorLoggerKey struct{}
// ErrorLogger is the function type used to log errors.
type ErrorLogger func(http.ResponseWriter, *http.Request, error)
func (fn ErrorLogger) call(w http.ResponseWriter, r *http.Request, err error) {
if fn == nil {
return
}
fn(w, r, err)
}
// WithErrorLogger returns a new context with the given error logger.
func WithErrorLogger(ctx context.Context, fn ErrorLogger) context.Context {
return context.WithValue(ctx, errorLoggerKey{}, fn)
}
// ErrorLoggerFromContext returns an error logger from the context.
func ErrorLoggerFromContext(ctx context.Context) (fn ErrorLogger) {
fn, _ = ctx.Value(errorLoggerKey{}).(ErrorLogger)
return
}
// StackTracedError is the set of errors implementing the StackTrace function.
//
// Errors implementing this interface have their stack traces logged when passed
@ -51,10 +27,8 @@ type fieldCarrier interface {
// Error adds to the response writer the given error if it implements
// logging.ResponseLogger. If it does not implement it, then writes the error
// using the log package.
func Error(w http.ResponseWriter, r *http.Request, err error) {
ErrorLoggerFromContext(r.Context()).call(w, r, err)
fc, ok := w.(fieldCarrier)
func Error(rw http.ResponseWriter, err error) {
fc, ok := rw.(fieldCarrier)
if !ok {
return
}
@ -77,7 +51,7 @@ func Error(w http.ResponseWriter, r *http.Request, err error) {
// EnabledResponse log the response object if it implements the EnableLogger
// interface.
func EnabledResponse(rw http.ResponseWriter, r *http.Request, v any) {
func EnabledResponse(rw http.ResponseWriter, v any) {
type enableLogger interface {
ToLog() (any, error)
}
@ -85,7 +59,7 @@ func EnabledResponse(rw http.ResponseWriter, r *http.Request, v any) {
if el, ok := v.(enableLogger); ok {
out, err := el.ToLog()
if err != nil {
Error(rw, r, err)
Error(rw, err)
return
}

@ -1,9 +1,6 @@
package log
import (
"bytes"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
@ -30,34 +27,21 @@ func (stackTracedError) StackTrace() pkgerrors.StackTrace {
}
func TestError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{}))
req := httptest.NewRequest("GET", "/test", http.NoBody)
reqWithLogger := req.WithContext(WithErrorLogger(req.Context(), func(w http.ResponseWriter, r *http.Request, err error) {
if err != nil {
logger.ErrorContext(r.Context(), "request failed", slog.Any("error", err))
}
}))
tests := []struct {
name string
error
rw http.ResponseWriter
r *http.Request
isFieldCarrier bool
isSlogLogger bool
stepDebug bool
expectStackTrace bool
}{
{"noLogger", nil, nil, req, false, false, false, false},
{"noError", nil, logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, false, false},
{"noErrorDebug", nil, logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, true, false},
{"anError", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, false, false},
{"anErrorDebug", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, true, false},
{"stackTracedError", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, true, true},
{"stackTracedErrorDebug", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, true, true},
{"slogWithNoError", nil, logging.NewResponseLogger(httptest.NewRecorder()), reqWithLogger, true, true, false, false},
{"slogWithError", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), reqWithLogger, true, true, false, false},
{"noLogger", nil, nil, false, false, false},
{"noError", nil, logging.NewResponseLogger(httptest.NewRecorder()), true, false, false},
{"noErrorDebug", nil, logging.NewResponseLogger(httptest.NewRecorder()), true, true, false},
{"anError", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), true, false, false},
{"anErrorDebug", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), true, true, false},
{"stackTracedError", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), true, true, true},
{"stackTracedErrorDebug", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), true, true, true},
}
for _, tt := range tests {
@ -68,41 +52,27 @@ func TestError(t *testing.T) {
t.Setenv("STEPDEBUG", "0")
}
Error(tt.rw, tt.r, tt.error)
Error(tt.rw, tt.error)
// return early if test case doesn't use logger
if !tt.isFieldCarrier && !tt.isSlogLogger {
if !tt.isFieldCarrier {
return
}
if tt.isFieldCarrier {
fields := tt.rw.(logging.ResponseLogger).Fields()
// expect the error field to be (not) set and to be the same error that was fed to Error
if tt.error == nil {
assert.Nil(t, fields["error"])
} else {
assert.Same(t, tt.error, fields["error"])
}
fields := tt.rw.(logging.ResponseLogger).Fields()
// check if stack-trace is set when expected
if _, hasStackTrace := fields["stack-trace"]; tt.expectStackTrace && !hasStackTrace {
t.Error(`ResponseLogger["stack-trace"] not set`)
} else if !tt.expectStackTrace && hasStackTrace {
t.Error(`ResponseLogger["stack-trace"] was set`)
}
// expect the error field to be (not) set and to be the same error that was fed to Error
if tt.error == nil {
assert.Nil(t, fields["error"])
} else {
assert.Same(t, tt.error, fields["error"])
}
if tt.isSlogLogger {
b := buf.Bytes()
if tt.error == nil {
assert.Empty(t, b)
} else if assert.NotEmpty(t, b) {
var m map[string]any
assert.NoError(t, json.Unmarshal(b, &m))
assert.Equal(t, tt.error.Error(), m["error"])
}
buf.Reset()
// check if stack-trace is set when expected
if _, hasStackTrace := fields["stack-trace"]; tt.expectStackTrace && !hasStackTrace {
t.Error(`ResponseLogger["stack-trace"] not set`)
} else if !tt.expectStackTrace && hasStackTrace {
t.Error(`ResponseLogger["stack-trace"] was set`)
}
})
}

@ -97,7 +97,7 @@ func (s *SCEP) AuthorizeSSHSign(context.Context, string) ([]provisioner.SignOpti
return nil, errDummyImplementation
}
// AuthorizeSSHRevoke returns an unimplemented error. Provisioners should overwrite
// 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

@ -51,7 +51,7 @@ func (e badProtoJSONError) Error() string {
}
// Render implements render.RenderableError for badProtoJSONError
func (e badProtoJSONError) Render(w http.ResponseWriter, r *http.Request) {
func (e badProtoJSONError) Render(w http.ResponseWriter) {
v := struct {
Type string `json:"type"`
Detail string `json:"detail"`
@ -62,5 +62,5 @@ func (e badProtoJSONError) Render(w http.ResponseWriter, r *http.Request) {
// trim the proto prefix for the message
Message: strings.TrimSpace(strings.TrimPrefix(e.Error(), "proto:")),
}
render.JSONStatus(w, r, v, http.StatusBadRequest)
render.JSONStatus(w, v, http.StatusBadRequest)
}

@ -142,8 +142,7 @@ func Test_badProtoJSONError_Render(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/test", http.NoBody)
tt.e.Render(w, r)
tt.e.Render(w)
res := w.Result()
defer res.Body.Close()

@ -29,25 +29,25 @@ func (s *RekeyRequest) Validate() error {
// Rekey is similar to renew except that the certificate will be renewed with new key from csr.
func Rekey(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
render.Error(w, r, errs.BadRequest("missing client certificate"))
render.Error(w, errs.BadRequest("missing client certificate"))
return
}
var body RekeyRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error reading request body"))
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
return
}
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
a := mustAuthority(r.Context())
certChain, err := a.Rekey(r.TLS.PeerCertificates[0], body.CsrPEM.CertificateRequest.PublicKey)
if err != nil {
render.Error(w, r, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Rekey"))
render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Rekey"))
return
}
certChainPEM := certChainToPEM(certChain)
@ -57,7 +57,7 @@ func Rekey(w http.ResponseWriter, r *http.Request) {
}
LogCertificate(w, certChain[0])
render.JSONStatus(w, r, &SignResponse{
render.JSONStatus(w, &SignResponse{
ServerPEM: certChainPEM[0],
CaPEM: caPEM,
CertChainPEM: certChainPEM,

@ -13,8 +13,8 @@ import (
)
// JSON is shorthand for JSONStatus(w, v, http.StatusOK).
func JSON(w http.ResponseWriter, r *http.Request, v interface{}) {
JSONStatus(w, r, v, http.StatusOK)
func JSON(w http.ResponseWriter, v interface{}) {
JSONStatus(w, v, http.StatusOK)
}
// JSONStatus marshals v into w. It additionally sets the status code of
@ -22,7 +22,7 @@ func JSON(w http.ResponseWriter, r *http.Request, v interface{}) {
//
// JSONStatus sets the Content-Type of w to application/json unless one is
// specified.
func JSONStatus(w http.ResponseWriter, r *http.Request, v interface{}, status int) {
func JSONStatus(w http.ResponseWriter, v interface{}, status int) {
setContentTypeUnlessPresent(w, "application/json")
w.WriteHeader(status)
@ -43,7 +43,7 @@ func JSONStatus(w http.ResponseWriter, r *http.Request, v interface{}, status in
}
}
log.EnabledResponse(w, r, v)
log.EnabledResponse(w, v)
}
// ProtoJSON is shorthand for ProtoJSONStatus(w, m, http.StatusOK).
@ -80,22 +80,22 @@ func setContentTypeUnlessPresent(w http.ResponseWriter, contentType string) {
type RenderableError interface {
error
Render(http.ResponseWriter, *http.Request)
Render(http.ResponseWriter)
}
// Error marshals the JSON representation of err to w. In case err implements
// RenderableError its own Render method will be called instead.
func Error(rw http.ResponseWriter, r *http.Request, err error) {
log.Error(rw, r, err)
func Error(w http.ResponseWriter, err error) {
log.Error(w, err)
var re RenderableError
if errors.As(err, &re) {
re.Render(rw, r)
var r RenderableError
if errors.As(err, &r) {
r.Render(w)
return
}
JSONStatus(rw, r, err, statusCodeFromError(err))
JSONStatus(w, err, statusCodeFromError(err))
}
// StatusCodedError is the set of errors that implement the basic StatusCode

@ -18,8 +18,8 @@ import (
func TestJSON(t *testing.T) {
rec := httptest.NewRecorder()
rw := logging.NewResponseLogger(rec)
r := httptest.NewRequest("POST", "/test", http.NoBody)
JSON(rw, r, map[string]interface{}{"foo": "bar"})
JSON(rw, map[string]interface{}{"foo": "bar"})
assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
@ -64,8 +64,7 @@ func jsonPanicTest[T json.UnsupportedTypeError | json.UnsupportedValueError | js
assert.ErrorAs(t, err, &e)
}()
r := httptest.NewRequest("POST", "/test", http.NoBody)
JSON(httptest.NewRecorder(), r, v)
JSON(httptest.NewRecorder(), v)
}
type renderableError struct {
@ -77,9 +76,10 @@ func (err renderableError) Error() string {
return err.Message
}
func (err renderableError) Render(w http.ResponseWriter, r *http.Request) {
func (err renderableError) Render(w http.ResponseWriter) {
w.Header().Set("Content-Type", "something/custom")
JSONStatus(w, r, err, err.Code)
JSONStatus(w, err, err.Code)
}
type statusedError struct {
@ -116,8 +116,8 @@ func TestError(t *testing.T) {
t.Run(strconv.Itoa(caseIndex), func(t *testing.T) {
rec := httptest.NewRecorder()
r := httptest.NewRequest("POST", "/test", http.NoBody)
Error(rec, r, kase.err)
Error(rec, kase.err)
assert.Equal(t, kase.code, rec.Result().StatusCode)
assert.Equal(t, kase.body, rec.Body.String())

@ -23,20 +23,19 @@ func Renew(w http.ResponseWriter, r *http.Request) {
// Get the leaf certificate from the peer or the token.
cert, token, err := getPeerCertificate(r)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
// The token can be used by RAs to renew a certificate.
if token != "" {
ctx = authority.NewTokenContext(ctx, token)
logOtt(w, token)
}
a := mustAuthority(ctx)
certChain, err := a.RenewContext(ctx, cert, nil)
if err != nil {
render.Error(w, r, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
render.Error(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
return
}
certChainPEM := certChainToPEM(certChain)
@ -46,7 +45,7 @@ func Renew(w http.ResponseWriter, r *http.Request) {
}
LogCertificate(w, certChain[0])
render.JSONStatus(w, r, &SignResponse{
render.JSONStatus(w, &SignResponse{
ServerPEM: certChainPEM[0],
CaPEM: caPEM,
CertChainPEM: certChainPEM,

@ -57,12 +57,12 @@ func (r *RevokeRequest) Validate() (err error) {
func Revoke(w http.ResponseWriter, r *http.Request) {
var body RevokeRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error reading request body"))
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
return
}
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -78,10 +78,10 @@ func Revoke(w http.ResponseWriter, r *http.Request) {
// A token indicates that we are using the api via a provisioner token,
// otherwise it is assumed that the certificate is revoking itself over mTLS.
if body.OTT != "" {
if len(body.OTT) > 0 {
logOtt(w, body.OTT)
if _, err := a.Authorize(ctx, body.OTT); err != nil {
render.Error(w, r, errs.UnauthorizedErr(err))
render.Error(w, errs.UnauthorizedErr(err))
return
}
opts.OTT = body.OTT
@ -90,12 +90,12 @@ func Revoke(w http.ResponseWriter, r *http.Request) {
// the client certificate Serial Number must match the serial number
// being revoked.
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
render.Error(w, r, errs.BadRequest("missing ott or client certificate"))
render.Error(w, errs.BadRequest("missing ott or client certificate"))
return
}
opts.Crt = r.TLS.PeerCertificates[0]
if opts.Crt.SerialNumber.String() != opts.Serial {
render.Error(w, r, errs.BadRequest("serial number in client certificate different than body"))
render.Error(w, errs.BadRequest("serial number in client certificate different than body"))
return
}
// TODO: should probably be checking if the certificate was revoked here.
@ -106,12 +106,12 @@ func Revoke(w http.ResponseWriter, r *http.Request) {
}
if err := a.Revoke(ctx, opts); err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error revoking certificate"))
render.Error(w, errs.ForbiddenErr(err, "error revoking certificate"))
return
}
logRevoke(w, opts)
render.JSON(w, r, &RevokeResponse{Status: "ok"})
render.JSON(w, &RevokeResponse{Status: "ok"})
}
func logRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {

@ -52,13 +52,13 @@ type SignResponse struct {
func Sign(w http.ResponseWriter, r *http.Request) {
var body SignRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error reading request body"))
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
return
}
logOtt(w, body.OTT)
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -74,13 +74,13 @@ func Sign(w http.ResponseWriter, r *http.Request) {
ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)
signOpts, err := a.Authorize(ctx, body.OTT)
if err != nil {
render.Error(w, r, errs.UnauthorizedErr(err))
render.Error(w, errs.UnauthorizedErr(err))
return
}
certChain, err := a.SignWithContext(ctx, body.CsrPEM.CertificateRequest, opts, signOpts...)
if err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error signing certificate"))
render.Error(w, errs.ForbiddenErr(err, "error signing certificate"))
return
}
certChainPEM := certChainToPEM(certChain)
@ -90,7 +90,7 @@ func Sign(w http.ResponseWriter, r *http.Request) {
}
LogCertificate(w, certChain[0])
render.JSONStatus(w, r, &SignResponse{
render.JSONStatus(w, &SignResponse{
ServerPEM: certChainPEM[0],
CaPEM: caPEM,
CertChainPEM: certChainPEM,

@ -6,11 +6,8 @@ import (
"encoding/base64"
"encoding/json"
"net/http"
"net/url"
"strings"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
@ -256,19 +253,19 @@ type SSHBastionResponse struct {
func SSHSign(w http.ResponseWriter, r *http.Request) {
var body SSHSignRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error reading request body"))
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
return
}
logOtt(w, body.OTT)
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
publicKey, err := ssh.ParsePublicKey(body.PublicKey)
if err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error parsing publicKey"))
render.Error(w, errs.BadRequestErr(err, "error parsing publicKey"))
return
}
@ -276,7 +273,7 @@ func SSHSign(w http.ResponseWriter, r *http.Request) {
if body.AddUserPublicKey != nil {
addUserPublicKey, err = ssh.ParsePublicKey(body.AddUserPublicKey)
if err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error parsing addUserPublicKey"))
render.Error(w, errs.BadRequestErr(err, "error parsing addUserPublicKey"))
return
}
}
@ -292,18 +289,17 @@ func SSHSign(w http.ResponseWriter, r *http.Request) {
ctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHSignMethod)
ctx = provisioner.NewContextWithToken(ctx, body.OTT)
ctx = provisioner.NewContextWithCertType(ctx, opts.CertType)
a := mustAuthority(ctx)
signOpts, err := a.Authorize(ctx, body.OTT)
if err != nil {
render.Error(w, r, errs.UnauthorizedErr(err))
render.Error(w, errs.UnauthorizedErr(err))
return
}
cert, err := a.SignSSH(ctx, publicKey, opts, signOpts...)
if err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error signing ssh certificate"))
render.Error(w, errs.ForbiddenErr(err, "error signing ssh certificate"))
return
}
@ -311,7 +307,7 @@ func SSHSign(w http.ResponseWriter, r *http.Request) {
if addUserPublicKey != nil && authority.IsValidForAddUser(cert) == nil {
addUserCert, err := a.SignSSHAddUser(ctx, addUserPublicKey, cert)
if err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error signing ssh certificate"))
render.Error(w, errs.ForbiddenErr(err, "error signing ssh certificate"))
return
}
addUserCertificate = &SSHCertificate{addUserCert}
@ -324,27 +320,26 @@ func SSHSign(w http.ResponseWriter, r *http.Request) {
ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignIdentityMethod)
signOpts, err := a.Authorize(ctx, body.OTT)
if err != nil {
render.Error(w, r, errs.UnauthorizedErr(err))
render.Error(w, errs.UnauthorizedErr(err))
return
}
// Enforce the same duration as ssh certificate.
signOpts = append(signOpts, &identityModifier{
Identity: getIdentityURI(cr),
NotBefore: time.Unix(int64(cert.ValidAfter), 0),
NotAfter: time.Unix(int64(cert.ValidBefore), 0),
})
certChain, err := a.SignWithContext(ctx, cr, provisioner.SignOptions{}, signOpts...)
if err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error signing identity certificate"))
render.Error(w, errs.ForbiddenErr(err, "error signing identity certificate"))
return
}
identityCertificate = certChainToPEM(certChain)
}
LogSSHCertificate(w, cert)
render.JSONStatus(w, r, &SSHSignResponse{
render.JSONStatus(w, &SSHSignResponse{
Certificate: SSHCertificate{cert},
AddUserCertificate: addUserCertificate,
IdentityCertificate: identityCertificate,
@ -357,12 +352,12 @@ func SSHRoots(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
keys, err := mustAuthority(ctx).GetSSHRoots(ctx)
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 {
render.Error(w, r, errs.NotFound("no keys found"))
render.Error(w, errs.NotFound("no keys found"))
return
}
@ -374,7 +369,7 @@ func SSHRoots(w http.ResponseWriter, r *http.Request) {
resp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k})
}
render.JSON(w, r, resp)
render.JSON(w, resp)
}
// SSHFederation is an HTTP handler that returns the federated SSH public keys
@ -383,12 +378,12 @@ func SSHFederation(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
keys, err := mustAuthority(ctx).GetSSHFederation(ctx)
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
if len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 {
render.Error(w, r, errs.NotFound("no keys found"))
render.Error(w, errs.NotFound("no keys found"))
return
}
@ -400,7 +395,7 @@ func SSHFederation(w http.ResponseWriter, r *http.Request) {
resp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k})
}
render.JSON(w, r, resp)
render.JSON(w, resp)
}
// SSHConfig is an HTTP handler that returns rendered templates for ssh clients
@ -408,18 +403,18 @@ func SSHFederation(w http.ResponseWriter, r *http.Request) {
func SSHConfig(w http.ResponseWriter, r *http.Request) {
var body SSHConfigRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error reading request body"))
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
return
}
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
ctx := r.Context()
ts, err := mustAuthority(ctx).GetSSHConfig(ctx, body.Type, body.Data)
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
@ -430,32 +425,32 @@ func SSHConfig(w http.ResponseWriter, r *http.Request) {
case provisioner.SSHHostCert:
cfg.HostTemplates = ts
default:
render.Error(w, r, errs.InternalServer("it should hot get here"))
render.Error(w, errs.InternalServer("it should hot get here"))
return
}
render.JSON(w, r, cfg)
render.JSON(w, cfg)
}
// SSHCheckHost is the HTTP handler that returns if a hosts certificate exists or not.
func SSHCheckHost(w http.ResponseWriter, r *http.Request) {
var body SSHCheckPrincipalRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error reading request body"))
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
return
}
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
ctx := r.Context()
exists, err := mustAuthority(ctx).CheckSSHHost(ctx, body.Principal, body.Token)
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
render.JSON(w, r, &SSHCheckPrincipalResponse{
render.JSON(w, &SSHCheckPrincipalResponse{
Exists: exists,
})
}
@ -470,10 +465,10 @@ func SSHGetHosts(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
hosts, err := mustAuthority(ctx).GetSSHHosts(ctx, cert)
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
render.JSON(w, r, &SSHGetHostsResponse{
render.JSON(w, &SSHGetHostsResponse{
Hosts: hosts,
})
}
@ -482,63 +477,35 @@ func SSHGetHosts(w http.ResponseWriter, r *http.Request) {
func SSHBastion(w http.ResponseWriter, r *http.Request) {
var body SSHBastionRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error reading request body"))
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
return
}
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
ctx := r.Context()
bastion, err := mustAuthority(ctx).GetSSHBastion(ctx, body.User, body.Hostname)
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
render.JSON(w, r, &SSHBastionResponse{
render.JSON(w, &SSHBastionResponse{
Hostname: body.Hostname,
Bastion: bastion,
})
}
// identityModifier is a custom modifier used to force a fixed duration, and set
// the identity URI.
// identityModifier is a custom modifier used to force a fixed duration.
type identityModifier struct {
Identity *url.URL
NotBefore time.Time
NotAfter time.Time
}
// Enforce implements the enforcer interface and sets the validity bounds and
// the identity uri to the certificate.
func (m *identityModifier) Enforce(cert *x509.Certificate) error {
cert.NotBefore = m.NotBefore
cert.NotAfter = m.NotAfter
if m.Identity != nil {
var identityURL = m.Identity.String()
for _, u := range cert.URIs {
if u.String() == identityURL {
return nil
}
}
cert.URIs = append(cert.URIs, m.Identity)
}
return nil
}
// getIdentityURI returns the first valid UUID URN from the given CSR.
func getIdentityURI(cr *x509.CertificateRequest) *url.URL {
for _, u := range cr.URIs {
s := u.String()
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if len(s) == 9+36 && strings.EqualFold(s[:9], "urn:uuid:") {
if _, err := uuid.Parse(s); err == nil {
return u
}
}
}
return nil
}

@ -42,19 +42,19 @@ type SSHRekeyResponse struct {
func SSHRekey(w http.ResponseWriter, r *http.Request) {
var body SSHRekeyRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error reading request body"))
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
return
}
logOtt(w, body.OTT)
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
publicKey, err := ssh.ParsePublicKey(body.PublicKey)
if err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error parsing publicKey"))
render.Error(w, errs.BadRequestErr(err, "error parsing publicKey"))
return
}
@ -64,18 +64,18 @@ func SSHRekey(w http.ResponseWriter, r *http.Request) {
a := mustAuthority(ctx)
signOpts, err := a.Authorize(ctx, body.OTT)
if err != nil {
render.Error(w, r, errs.UnauthorizedErr(err))
render.Error(w, errs.UnauthorizedErr(err))
return
}
oldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT)
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
newCert, err := a.RekeySSH(ctx, oldCert, publicKey, signOpts...)
if err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error rekeying ssh certificate"))
render.Error(w, errs.ForbiddenErr(err, "error rekeying ssh certificate"))
return
}
@ -85,12 +85,12 @@ func SSHRekey(w http.ResponseWriter, r *http.Request) {
identity, err := renewIdentityCertificate(r, notBefore, notAfter)
if err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error renewing identity certificate"))
render.Error(w, errs.ForbiddenErr(err, "error renewing identity certificate"))
return
}
LogSSHCertificate(w, newCert)
render.JSONStatus(w, r, &SSHRekeyResponse{
render.JSONStatus(w, &SSHRekeyResponse{
Certificate: SSHCertificate{newCert},
IdentityCertificate: identity,
}, http.StatusCreated)

@ -40,13 +40,13 @@ type SSHRenewResponse struct {
func SSHRenew(w http.ResponseWriter, r *http.Request) {
var body SSHRenewRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error reading request body"))
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
return
}
logOtt(w, body.OTT)
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -56,18 +56,18 @@ func SSHRenew(w http.ResponseWriter, r *http.Request) {
a := mustAuthority(ctx)
_, err := a.Authorize(ctx, body.OTT)
if err != nil {
render.Error(w, r, errs.UnauthorizedErr(err))
render.Error(w, errs.UnauthorizedErr(err))
return
}
oldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT)
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
newCert, err := a.RenewSSH(ctx, oldCert)
if err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error renewing ssh certificate"))
render.Error(w, errs.ForbiddenErr(err, "error renewing ssh certificate"))
return
}
@ -77,12 +77,12 @@ func SSHRenew(w http.ResponseWriter, r *http.Request) {
identity, err := renewIdentityCertificate(r, notBefore, notAfter)
if err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error renewing identity certificate"))
render.Error(w, errs.ForbiddenErr(err, "error renewing identity certificate"))
return
}
LogSSHCertificate(w, newCert)
render.JSONStatus(w, r, &SSHSignResponse{
render.JSONStatus(w, &SSHSignResponse{
Certificate: SSHCertificate{newCert},
IdentityCertificate: identity,
}, http.StatusCreated)

@ -51,12 +51,12 @@ func (r *SSHRevokeRequest) Validate() (err error) {
func SSHRevoke(w http.ResponseWriter, r *http.Request) {
var body SSHRevokeRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, errs.BadRequestErr(err, "error reading request body"))
render.Error(w, errs.BadRequestErr(err, "error reading request body"))
return
}
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -75,18 +75,18 @@ func SSHRevoke(w http.ResponseWriter, r *http.Request) {
logOtt(w, body.OTT)
if _, err := a.Authorize(ctx, body.OTT); err != nil {
render.Error(w, r, errs.UnauthorizedErr(err))
render.Error(w, errs.UnauthorizedErr(err))
return
}
opts.OTT = body.OTT
if err := a.Revoke(ctx, opts); err != nil {
render.Error(w, r, errs.ForbiddenErr(err, "error revoking ssh certificate"))
render.Error(w, errs.ForbiddenErr(err, "error revoking ssh certificate"))
return
}
logSSHRevoke(w, opts)
render.JSON(w, r, &SSHRevokeResponse{Status: "ok"})
render.JSON(w, &SSHRevokeResponse{Status: "ok"})
}
func logSSHRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {

@ -13,20 +13,18 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/ssh"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/logging"
"github.com/smallstep/certificates/templates"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
)
var (
@ -125,9 +123,9 @@ func getSignedHostCertificate() (*ssh.Certificate, error) {
func TestSSHCertificate_MarshalJSON(t *testing.T) {
user, err := getSignedUserCertificate()
require.NoError(t, err)
assert.FatalError(t, err)
host, err := getSignedHostCertificate()
require.NoError(t, err)
assert.FatalError(t, err)
userB64 := base64.StdEncoding.EncodeToString(user.Marshal())
hostB64 := base64.StdEncoding.EncodeToString(host.Marshal())
@ -163,9 +161,9 @@ func TestSSHCertificate_MarshalJSON(t *testing.T) {
func TestSSHCertificate_UnmarshalJSON(t *testing.T) {
user, err := getSignedUserCertificate()
require.NoError(t, err)
assert.FatalError(t, err)
host, err := getSignedHostCertificate()
require.NoError(t, err)
assert.FatalError(t, err)
userB64 := base64.StdEncoding.EncodeToString(user.Marshal())
hostB64 := base64.StdEncoding.EncodeToString(host.Marshal())
keyB64 := base64.StdEncoding.EncodeToString(user.Key.Marshal())
@ -255,9 +253,9 @@ func TestSignSSHRequest_Validate(t *testing.T) {
func Test_SSHSign(t *testing.T) {
user, err := getSignedUserCertificate()
require.NoError(t, err)
assert.FatalError(t, err)
host, err := getSignedHostCertificate()
require.NoError(t, err)
assert.FatalError(t, err)
userB64 := base64.StdEncoding.EncodeToString(user.Marshal())
hostB64 := base64.StdEncoding.EncodeToString(host.Marshal())
@ -266,24 +264,24 @@ func Test_SSHSign(t *testing.T) {
PublicKey: user.Key.Marshal(),
OTT: "ott",
})
require.NoError(t, err)
assert.FatalError(t, err)
hostReq, err := json.Marshal(SSHSignRequest{
PublicKey: host.Key.Marshal(),
OTT: "ott",
})
require.NoError(t, err)
assert.FatalError(t, err)
userAddReq, err := json.Marshal(SSHSignRequest{
PublicKey: user.Key.Marshal(),
OTT: "ott",
AddUserPublicKey: user.Key.Marshal(),
})
require.NoError(t, err)
assert.FatalError(t, err)
userIdentityReq, err := json.Marshal(SSHSignRequest{
PublicKey: user.Key.Marshal(),
OTT: "ott",
IdentityCSR: CertificateRequest{parseCertificateRequest(csrPEM)},
})
require.NoError(t, err)
assert.FatalError(t, err)
identityCerts := []*x509.Certificate{
parseCertificate(certPEM),
}
@ -357,11 +355,11 @@ func Test_SSHSign(t *testing.T) {
func Test_SSHRoots(t *testing.T) {
user, err := ssh.NewPublicKey(sshUserKey.Public())
require.NoError(t, err)
assert.FatalError(t, err)
userB64 := base64.StdEncoding.EncodeToString(user.Marshal())
host, err := ssh.NewPublicKey(sshHostKey.Public())
require.NoError(t, err)
assert.FatalError(t, err)
hostB64 := base64.StdEncoding.EncodeToString(host.Marshal())
tests := []struct {
@ -411,11 +409,11 @@ func Test_SSHRoots(t *testing.T) {
func Test_SSHFederation(t *testing.T) {
user, err := ssh.NewPublicKey(sshUserKey.Public())
require.NoError(t, err)
assert.FatalError(t, err)
userB64 := base64.StdEncoding.EncodeToString(user.Marshal())
host, err := ssh.NewPublicKey(sshHostKey.Public())
require.NoError(t, err)
assert.FatalError(t, err)
hostB64 := base64.StdEncoding.EncodeToString(host.Marshal())
tests := []struct {
@ -473,9 +471,9 @@ func Test_SSHConfig(t *testing.T) {
{Name: "ca.tpl", Type: templates.File, Comment: "#", Path: "/etc/ssh/ca.pub", Content: []byte("ecdsa-sha2-nistp256 AAAA...=")},
}
userJSON, err := json.Marshal(userOutput)
require.NoError(t, err)
assert.FatalError(t, err)
hostJSON, err := json.Marshal(hostOutput)
require.NoError(t, err)
assert.FatalError(t, err)
tests := []struct {
name string
@ -576,7 +574,7 @@ func Test_SSHGetHosts(t *testing.T) {
{HostID: "2", HostTags: []authority.HostTag{{ID: "1", Name: "group", Value: "1"}, {ID: "2", Name: "group", Value: "2"}}, Hostname: "host2"},
}
hostsJSON, err := json.Marshal(hosts)
require.NoError(t, err)
assert.FatalError(t, err)
tests := []struct {
name string
@ -678,7 +676,7 @@ func Test_SSHBastion(t *testing.T) {
func TestSSHPublicKey_MarshalJSON(t *testing.T) {
key, err := ssh.NewPublicKey(sshUserKey.Public())
require.NoError(t, err)
assert.FatalError(t, err)
keyB64 := base64.StdEncoding.EncodeToString(key.Marshal())
tests := []struct {
@ -707,7 +705,7 @@ func TestSSHPublicKey_MarshalJSON(t *testing.T) {
func TestSSHPublicKey_UnmarshalJSON(t *testing.T) {
key, err := ssh.NewPublicKey(sshUserKey.Public())
require.NoError(t, err)
assert.FatalError(t, err)
keyB64 := base64.StdEncoding.EncodeToString(key.Marshal())
type args struct {
@ -738,98 +736,3 @@ func TestSSHPublicKey_UnmarshalJSON(t *testing.T) {
})
}
}
func Test_identityModifier_Enforce(t *testing.T) {
now := time.Now()
type fields struct {
Identity *url.URL
NotBefore time.Time
NotAfter time.Time
}
type args struct {
cert *x509.Certificate
}
tests := []struct {
name string
fields fields
args args
want *x509.Certificate
assertion assert.ErrorAssertionFunc
}{
{"ok", fields{&url.URL{Scheme: "urn", Opaque: "uuid:0c4670b2-d9f1-42bb-9045-184836f16733"}, now, now.Add(time.Hour)},
args{&x509.Certificate{}}, &x509.Certificate{
NotBefore: now,
NotAfter: now.Add(time.Hour),
URIs: []*url.URL{{Scheme: "urn", Opaque: "uuid:0c4670b2-d9f1-42bb-9045-184836f16733"}},
}, assert.NoError},
{"ok exists", fields{&url.URL{Scheme: "urn", Opaque: "uuid:0c4670b2-d9f1-42bb-9045-184836f16733"}, now, now.Add(time.Hour)},
args{&x509.Certificate{
URIs: []*url.URL{{Scheme: "urn", Opaque: "uuid:0c4670b2-d9f1-42bb-9045-184836f16733"}},
}}, &x509.Certificate{
NotBefore: now,
NotAfter: now.Add(time.Hour),
URIs: []*url.URL{{Scheme: "urn", Opaque: "uuid:0c4670b2-d9f1-42bb-9045-184836f16733"}},
}, assert.NoError},
{"ok append", fields{&url.URL{Scheme: "urn", Opaque: "uuid:0c4670b2-d9f1-42bb-9045-184836f16733"}, now, now.Add(time.Hour)},
args{&x509.Certificate{
URIs: []*url.URL{{Scheme: "urn", Opaque: "uuid:27bb66db-e12a-4ff6-9161-aa6b0a98f914"}},
}}, &x509.Certificate{
NotBefore: now,
NotAfter: now.Add(time.Hour),
URIs: []*url.URL{
{Scheme: "urn", Opaque: "uuid:27bb66db-e12a-4ff6-9161-aa6b0a98f914"},
{Scheme: "urn", Opaque: "uuid:0c4670b2-d9f1-42bb-9045-184836f16733"},
},
}, assert.NoError},
{"ok no identity", fields{nil, now, now.Add(time.Hour)},
args{&x509.Certificate{}}, &x509.Certificate{
NotBefore: now,
NotAfter: now.Add(time.Hour),
}, assert.NoError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &identityModifier{
Identity: tt.fields.Identity,
NotBefore: tt.fields.NotBefore,
NotAfter: tt.fields.NotAfter,
}
tt.assertion(t, m.Enforce(tt.args.cert))
})
}
}
func Test_getIdentityURI(t *testing.T) {
id, err := uuid.Parse("54a2ec9d-a7d9-4b53-8f9a-efcd275e35e1")
require.NoError(t, err)
u, err := url.Parse(id.URN())
require.NoError(t, err)
type args struct {
cr *x509.CertificateRequest
}
tests := []struct {
name string
args args
want *url.URL
}{
{"ok", args{&x509.CertificateRequest{
URIs: []*url.URL{u},
}}, &url.URL{Scheme: "urn", Opaque: "uuid:54a2ec9d-a7d9-4b53-8f9a-efcd275e35e1"}},
{"ok multiple", args{&x509.CertificateRequest{
URIs: []*url.URL{u, {Scheme: "urn", Opaque: "uuid:f0e74f3a-95fe-4cf6-98e3-68e55b69ba48"}},
}}, &url.URL{Scheme: "urn", Opaque: "uuid:54a2ec9d-a7d9-4b53-8f9a-efcd275e35e1"}},
{"ok multiple with invalid", args{&x509.CertificateRequest{
URIs: []*url.URL{{Scheme: "urn", Opaque: "uuid:f0e74f3a+95fe+4cf6+98e3+68e55b69ba48"}, u},
}}, &url.URL{Scheme: "urn", Opaque: "uuid:54a2ec9d-a7d9-4b53-8f9a-efcd275e35e1"}},
{"ok missing", args{&x509.CertificateRequest{
URIs: []*url.URL{{Scheme: "https", Host: "example.com", Path: "/54a2ec9d-a7d9-4b53-8f9a-efcd275e35e1"}},
}}, nil},
{"ok empty", args{&x509.CertificateRequest{}}, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, getIdentityURI(tt.args.cr))
})
}
}

@ -40,12 +40,12 @@ func requireEABEnabled(next http.HandlerFunc) http.HandlerFunc {
acmeProvisioner := prov.GetDetails().GetACME()
if acmeProvisioner == nil {
render.Error(w, r, admin.NewErrorISE("error getting ACME details for provisioner '%s'", prov.GetName()))
render.Error(w, admin.NewErrorISE("error getting ACME details for provisioner '%s'", prov.GetName()))
return
}
if !acmeProvisioner.RequireEab {
render.Error(w, r, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner '%s'", prov.GetName()))
render.Error(w, admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner '%s'", prov.GetName()))
return
}
@ -69,18 +69,18 @@ func NewACMEAdminResponder() ACMEAdminResponder {
}
// GetExternalAccountKeys writes the response for the EAB keys GET endpoint
func (h *acmeAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {
render.Error(w, r, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
func (h *acmeAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, _ *http.Request) {
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
}
// CreateExternalAccountKey writes the response for the EAB key POST endpoint
func (h *acmeAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) {
render.Error(w, r, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
func (h *acmeAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, _ *http.Request) {
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
}
// DeleteExternalAccountKey writes the response for the EAB key DELETE endpoint
func (h *acmeAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {
render.Error(w, r, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
func (h *acmeAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, _ *http.Request) {
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm"))
}
func eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey {

@ -90,7 +90,7 @@ func GetAdmin(w http.ResponseWriter, r *http.Request) {
adm, ok := mustAuthority(r.Context()).LoadAdminByID(id)
if !ok {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType,
render.Error(w, admin.NewError(admin.ErrorNotFoundType,
"admin %s not found", id))
return
}
@ -101,17 +101,17 @@ func GetAdmin(w http.ResponseWriter, r *http.Request) {
func GetAdmins(w http.ResponseWriter, r *http.Request) {
cursor, limit, err := api.ParseCursor(r)
if err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err,
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err,
"error parsing cursor and limit from query params"))
return
}
admins, nextCursor, err := mustAuthority(r.Context()).GetAdmins(cursor, limit)
if err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error retrieving paginated admins"))
render.Error(w, admin.WrapErrorISE(err, "error retrieving paginated admins"))
return
}
render.JSON(w, r, &GetAdminsResponse{
render.JSON(w, &GetAdminsResponse{
Admins: admins,
NextCursor: nextCursor,
})
@ -121,19 +121,19 @@ func GetAdmins(w http.ResponseWriter, r *http.Request) {
func CreateAdmin(w http.ResponseWriter, r *http.Request) {
var body CreateAdminRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body"))
return
}
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
auth := mustAuthority(r.Context())
p, err := auth.LoadProvisionerByName(body.Provisioner)
if err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error loading provisioner %s", body.Provisioner))
render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", body.Provisioner))
return
}
adm := &linkedca.Admin{
@ -143,7 +143,7 @@ func CreateAdmin(w http.ResponseWriter, r *http.Request) {
}
// Store to authority collection.
if err := auth.StoreAdmin(r.Context(), adm, p); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error storing admin"))
render.Error(w, admin.WrapErrorISE(err, "error storing admin"))
return
}
@ -155,23 +155,23 @@ func DeleteAdmin(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := mustAuthority(r.Context()).RemoveAdmin(r.Context(), id); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error deleting admin %s", id))
render.Error(w, admin.WrapErrorISE(err, "error deleting admin %s", id))
return
}
render.JSON(w, r, &DeleteResponse{Status: "ok"})
render.JSON(w, &DeleteResponse{Status: "ok"})
}
// UpdateAdmin updates an existing admin.
func UpdateAdmin(w http.ResponseWriter, r *http.Request) {
var body UpdateAdminRequest
if err := read.JSON(r.Body, &body); err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error reading request body"))
return
}
if err := body.Validate(); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -179,7 +179,7 @@ func UpdateAdmin(w http.ResponseWriter, r *http.Request) {
auth := mustAuthority(r.Context())
adm, err := auth.UpdateAdmin(r.Context(), id, &linkedca.Admin{Type: body.Type})
if err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error updating admin %s", id))
render.Error(w, admin.WrapErrorISE(err, "error updating admin %s", id))
return
}

@ -1,6 +1,7 @@
package api
import (
"errors"
"net/http"
"github.com/go-chi/chi/v5"
@ -19,7 +20,7 @@ import (
func requireAPIEnabled(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !mustAuthority(r.Context()).IsAdminAPIEnabled() {
render.Error(w, r, admin.NewError(admin.ErrorNotImplementedType, "administration API not enabled"))
render.Error(w, admin.NewError(admin.ErrorNotImplementedType, "administration API not enabled"))
return
}
next(w, r)
@ -31,7 +32,7 @@ func extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tok := r.Header.Get("Authorization")
if tok == "" {
render.Error(w, r, admin.NewError(admin.ErrorUnauthorizedType,
render.Error(w, admin.NewError(admin.ErrorUnauthorizedType,
"missing authorization header token"))
return
}
@ -39,7 +40,7 @@ func extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc {
ctx := r.Context()
adm, err := mustAuthority(ctx).AuthorizeAdminToken(r, tok)
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -64,13 +65,13 @@ func loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc {
// TODO(hs): distinguish 404 vs. 500
if p, err = auth.LoadProvisionerByName(name); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error loading provisioner %s", name))
render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
return
}
prov, err := adminDB.GetProvisioner(ctx, p.GetID())
if err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error retrieving provisioner %s", name))
render.Error(w, admin.WrapErrorISE(err, "error retrieving provisioner %s", name))
return
}
@ -91,7 +92,7 @@ func checkAction(next http.HandlerFunc, supportedInStandalone bool) http.Handler
// when an action is not supported in standalone mode and when
// using a nosql.DB backend, actions are not supported
if _, ok := admin.MustFromContext(r.Context()).(*nosql.DB); ok {
render.Error(w, r, admin.NewError(admin.ErrorNotImplementedType,
render.Error(w, admin.NewError(admin.ErrorNotImplementedType,
"operation not supported in standalone mode"))
return
}
@ -124,16 +125,16 @@ func loadExternalAccountKey(next http.HandlerFunc) http.HandlerFunc {
}
if err != nil {
if acme.IsErrNotFound(err) {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found"))
if errors.Is(err, acme.ErrNotFound) {
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found"))
return
}
render.Error(w, r, admin.WrapErrorISE(err, "error retrieving ACME External Account Key"))
render.Error(w, admin.WrapErrorISE(err, "error retrieving ACME External Account Key"))
return
}
if eak == nil {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found"))
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME External Account Key not found"))
return
}

@ -35,7 +35,7 @@ type PolicyAdminResponder interface {
// policyAdminResponder implements PolicyAdminResponder.
type policyAdminResponder struct{}
// NewPolicyAdminResponder returns a new PolicyAdminResponder.
// NewACMEAdminResponder returns a new PolicyAdminResponder.
func NewPolicyAdminResponder() PolicyAdminResponder {
return &policyAdminResponder{}
}
@ -44,7 +44,7 @@ func NewPolicyAdminResponder() PolicyAdminResponder {
func (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -52,12 +52,12 @@ func (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *ht
authorityPolicy, err := auth.GetAuthorityPolicy(r.Context())
var ae *admin.Error
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, r, admin.WrapErrorISE(ae, "error retrieving authority policy"))
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
return
}
if authorityPolicy == nil {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
return
}
@ -68,7 +68,7 @@ func (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *ht
func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -77,26 +77,26 @@ func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r
var ae *admin.Error
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, r, admin.WrapErrorISE(err, "error retrieving authority policy"))
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
return
}
if authorityPolicy != nil {
adminErr := admin.NewError(admin.ErrorConflictType, "authority already has a policy")
render.Error(w, r, adminErr)
render.Error(w, adminErr)
return
}
var newPolicy = new(linkedca.Policy)
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
newPolicy.Deduplicate()
if err := validatePolicy(newPolicy); err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy"))
return
}
@ -105,11 +105,11 @@ func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r
var createdPolicy *linkedca.Policy
if createdPolicy, err = auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
if isBadRequest(err) {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy"))
return
}
render.Error(w, r, admin.WrapErrorISE(err, "error storing authority policy"))
render.Error(w, admin.WrapErrorISE(err, "error storing authority policy"))
return
}
@ -120,7 +120,7 @@ func (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r
func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -129,25 +129,25 @@ func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r
var ae *admin.Error
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, r, admin.WrapErrorISE(err, "error retrieving authority policy"))
render.Error(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
return
}
if authorityPolicy == nil {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
return
}
var newPolicy = new(linkedca.Policy)
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
newPolicy.Deduplicate()
if err := validatePolicy(newPolicy); err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating authority policy"))
return
}
@ -156,11 +156,11 @@ func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r
var updatedPolicy *linkedca.Policy
if updatedPolicy, err = auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
if isBadRequest(err) {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy"))
return
}
render.Error(w, r, admin.WrapErrorISE(err, "error updating authority policy"))
render.Error(w, admin.WrapErrorISE(err, "error updating authority policy"))
return
}
@ -171,7 +171,7 @@ func (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r
func (par *policyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -180,35 +180,35 @@ func (par *policyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r
var ae *admin.Error
if errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {
render.Error(w, r, admin.WrapErrorISE(ae, "error retrieving authority policy"))
render.Error(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
return
}
if authorityPolicy == nil {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "authority policy does not exist"))
return
}
if err := auth.RemoveAuthorityPolicy(ctx); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error deleting authority policy"))
render.Error(w, admin.WrapErrorISE(err, "error deleting authority policy"))
return
}
render.JSONStatus(w, r, DeleteResponse{Status: "ok"}, http.StatusOK)
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
}
// GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request
func (par *policyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
prov := linkedca.MustProvisionerFromContext(ctx)
provisionerPolicy := prov.GetPolicy()
if provisionerPolicy == nil {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
return
}
@ -219,7 +219,7 @@ func (par *policyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *
func (par *policyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -227,20 +227,20 @@ func (par *policyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter,
provisionerPolicy := prov.GetPolicy()
if provisionerPolicy != nil {
adminErr := admin.NewError(admin.ErrorConflictType, "provisioner %s already has a policy", prov.Name)
render.Error(w, r, adminErr)
render.Error(w, adminErr)
return
}
var newPolicy = new(linkedca.Policy)
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
newPolicy.Deduplicate()
if err := validatePolicy(newPolicy); err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy"))
return
}
@ -248,11 +248,11 @@ func (par *policyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter,
auth := mustAuthority(ctx)
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
if isBadRequest(err) {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner policy"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner policy"))
return
}
render.Error(w, r, admin.WrapErrorISE(err, "error creating provisioner policy"))
render.Error(w, admin.WrapErrorISE(err, "error creating provisioner policy"))
return
}
@ -263,27 +263,27 @@ func (par *policyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter,
func (par *policyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
prov := linkedca.MustProvisionerFromContext(ctx)
provisionerPolicy := prov.GetPolicy()
if provisionerPolicy == nil {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
return
}
var newPolicy = new(linkedca.Policy)
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
newPolicy.Deduplicate()
if err := validatePolicy(newPolicy); err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating provisioner policy"))
return
}
@ -291,11 +291,11 @@ func (par *policyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter,
auth := mustAuthority(ctx)
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
if isBadRequest(err) {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner policy"))
return
}
render.Error(w, r, admin.WrapErrorISE(err, "error updating provisioner policy"))
render.Error(w, admin.WrapErrorISE(err, "error updating provisioner policy"))
return
}
@ -306,13 +306,13 @@ func (par *policyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter,
func (par *policyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
prov := linkedca.MustProvisionerFromContext(ctx)
if prov.Policy == nil {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "provisioner policy does not exist"))
return
}
@ -321,24 +321,24 @@ func (par *policyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter,
auth := mustAuthority(ctx)
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error deleting provisioner policy"))
render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner policy"))
return
}
render.JSONStatus(w, r, DeleteResponse{Status: "ok"}, http.StatusOK)
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
}
func (par *policyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
eakPolicy := eak.GetPolicy()
if eakPolicy == nil {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
return
}
@ -348,7 +348,7 @@ func (par *policyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *
func (par *policyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -357,20 +357,20 @@ func (par *policyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter,
eakPolicy := eak.GetPolicy()
if eakPolicy != nil {
adminErr := admin.NewError(admin.ErrorConflictType, "ACME EAK %s already has a policy", eak.Id)
render.Error(w, r, adminErr)
render.Error(w, adminErr)
return
}
var newPolicy = new(linkedca.Policy)
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
newPolicy.Deduplicate()
if err := validatePolicy(newPolicy); err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy"))
return
}
@ -379,7 +379,7 @@ func (par *policyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter,
acmeEAK := linkedEAKToCertificates(eak)
acmeDB := acme.MustDatabaseFromContext(ctx)
if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error creating ACME EAK policy"))
render.Error(w, admin.WrapErrorISE(err, "error creating ACME EAK policy"))
return
}
@ -389,7 +389,7 @@ func (par *policyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter,
func (par *policyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -397,20 +397,20 @@ func (par *policyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter,
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
eakPolicy := eak.GetPolicy()
if eakPolicy == nil {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
return
}
var newPolicy = new(linkedca.Policy)
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
newPolicy.Deduplicate()
if err := validatePolicy(newPolicy); err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating ACME EAK policy"))
return
}
@ -418,7 +418,7 @@ func (par *policyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter,
acmeEAK := linkedEAKToCertificates(eak)
acmeDB := acme.MustDatabaseFromContext(ctx)
if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error updating ACME EAK policy"))
render.Error(w, admin.WrapErrorISE(err, "error updating ACME EAK policy"))
return
}
@ -428,7 +428,7 @@ func (par *policyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter,
func (par *policyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := blockLinkedCA(ctx); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -436,7 +436,7 @@ func (par *policyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter,
eak := linkedca.MustExternalAccountKeyFromContext(ctx)
eakPolicy := eak.GetPolicy()
if eakPolicy == nil {
render.Error(w, r, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
render.Error(w, admin.NewError(admin.ErrorNotFoundType, "ACME EAK policy does not exist"))
return
}
@ -446,11 +446,11 @@ func (par *policyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter,
acmeEAK := linkedEAKToCertificates(eak)
acmeDB := acme.MustDatabaseFromContext(ctx)
if err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error deleting ACME EAK policy"))
render.Error(w, admin.WrapErrorISE(err, "error deleting ACME EAK policy"))
return
}
render.JSONStatus(w, r, DeleteResponse{Status: "ok"}, http.StatusOK)
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
}
// blockLinkedCA blocks all API operations on linked deployments

@ -38,21 +38,21 @@ func GetProvisioner(w http.ResponseWriter, r *http.Request) {
auth := mustAuthority(ctx)
db := admin.MustFromContext(ctx)
if id != "" {
if len(id) > 0 {
if p, err = auth.LoadProvisionerByID(id); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error loading provisioner %s", id))
render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", id))
return
}
} else {
if p, err = auth.LoadProvisionerByName(name); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error loading provisioner %s", name))
render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
return
}
}
prov, err := db.GetProvisioner(ctx, p.GetID())
if err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
render.ProtoJSON(w, prov)
@ -62,17 +62,17 @@ func GetProvisioner(w http.ResponseWriter, r *http.Request) {
func GetProvisioners(w http.ResponseWriter, r *http.Request) {
cursor, limit, err := api.ParseCursor(r)
if err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err,
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err,
"error parsing cursor and limit from query params"))
return
}
p, next, err := mustAuthority(r.Context()).GetProvisioners(cursor, limit)
if err != nil {
render.Error(w, r, errs.InternalServerErr(err))
render.Error(w, errs.InternalServerErr(err))
return
}
render.JSON(w, r, &GetProvisionersResponse{
render.JSON(w, &GetProvisionersResponse{
Provisioners: p,
NextCursor: next,
})
@ -82,24 +82,24 @@ func GetProvisioners(w http.ResponseWriter, r *http.Request) {
func CreateProvisioner(w http.ResponseWriter, r *http.Request) {
var prov = new(linkedca.Provisioner)
if err := read.ProtoJSON(r.Body, prov); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
// TODO: Validate inputs
if err := authority.ValidateClaims(prov.Claims); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
// validate the templates and template data
if err := validateTemplates(prov.X509Template, prov.SshTemplate); err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "invalid template"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "invalid template"))
return
}
if err := mustAuthority(r.Context()).StoreProvisioner(r.Context(), prov); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error storing provisioner %s", prov.Name))
render.Error(w, admin.WrapErrorISE(err, "error storing provisioner %s", prov.Name))
return
}
render.ProtoJSONStatus(w, prov, http.StatusCreated)
@ -116,31 +116,31 @@ func DeleteProvisioner(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
auth := mustAuthority(r.Context())
if id != "" {
if len(id) > 0 {
if p, err = auth.LoadProvisionerByID(id); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error loading provisioner %s", id))
render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", id))
return
}
} else {
if p, err = auth.LoadProvisionerByName(name); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error loading provisioner %s", name))
render.Error(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
return
}
}
if err := auth.RemoveProvisioner(r.Context(), p.GetID()); err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error removing provisioner %s", p.GetName()))
render.Error(w, admin.WrapErrorISE(err, "error removing provisioner %s", p.GetName()))
return
}
render.JSON(w, r, &DeleteResponse{Status: "ok"})
render.JSON(w, &DeleteResponse{Status: "ok"})
}
// UpdateProvisioner updates an existing prov.
func UpdateProvisioner(w http.ResponseWriter, r *http.Request) {
var nu = new(linkedca.Provisioner)
if err := read.ProtoJSON(r.Body, nu); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -151,51 +151,51 @@ func UpdateProvisioner(w http.ResponseWriter, r *http.Request) {
p, err := auth.LoadProvisionerByName(name)
if err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error loading provisioner from cached configuration '%s'", name))
render.Error(w, admin.WrapErrorISE(err, "error loading provisioner from cached configuration '%s'", name))
return
}
old, err := db.GetProvisioner(r.Context(), p.GetID())
if err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error loading provisioner from db '%s'", p.GetID()))
render.Error(w, admin.WrapErrorISE(err, "error loading provisioner from db '%s'", p.GetID()))
return
}
if nu.Id != old.Id {
render.Error(w, r, admin.NewErrorISE("cannot change provisioner ID"))
render.Error(w, admin.NewErrorISE("cannot change provisioner ID"))
return
}
if nu.Type != old.Type {
render.Error(w, r, admin.NewErrorISE("cannot change provisioner type"))
render.Error(w, admin.NewErrorISE("cannot change provisioner type"))
return
}
if nu.AuthorityId != old.AuthorityId {
render.Error(w, r, admin.NewErrorISE("cannot change provisioner authorityID"))
render.Error(w, admin.NewErrorISE("cannot change provisioner authorityID"))
return
}
if !nu.CreatedAt.AsTime().Equal(old.CreatedAt.AsTime()) {
render.Error(w, r, admin.NewErrorISE("cannot change provisioner createdAt"))
render.Error(w, admin.NewErrorISE("cannot change provisioner createdAt"))
return
}
if !nu.DeletedAt.AsTime().Equal(old.DeletedAt.AsTime()) {
render.Error(w, r, admin.NewErrorISE("cannot change provisioner deletedAt"))
render.Error(w, admin.NewErrorISE("cannot change provisioner deletedAt"))
return
}
// TODO: Validate inputs
if err := authority.ValidateClaims(nu.Claims); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
// validate the templates and template data
if err := validateTemplates(nu.X509Template, nu.SshTemplate); err != nil {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "invalid template"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "invalid template"))
return
}
if err := auth.UpdateProvisioner(r.Context(), nu); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
render.ProtoJSON(w, nu)

@ -71,28 +71,28 @@ func (war *webhookAdminResponder) CreateProvisionerWebhook(w http.ResponseWriter
var newWebhook = new(linkedca.Webhook)
if err := read.ProtoJSON(r.Body, newWebhook); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
if err := validateWebhook(newWebhook); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
if newWebhook.Secret != "" {
err := admin.NewError(admin.ErrorBadRequestType, "webhook secret must not be set")
render.Error(w, r, err)
render.Error(w, err)
return
}
if newWebhook.Id != "" {
err := admin.NewError(admin.ErrorBadRequestType, "webhook ID must not be set")
render.Error(w, r, err)
render.Error(w, err)
return
}
id, err := randutil.UUIDv4()
if err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error generating webhook id"))
render.Error(w, admin.WrapErrorISE(err, "error generating webhook id"))
return
}
newWebhook.Id = id
@ -101,14 +101,14 @@ func (war *webhookAdminResponder) CreateProvisionerWebhook(w http.ResponseWriter
for _, wh := range prov.Webhooks {
if wh.Name == newWebhook.Name {
err := admin.NewError(admin.ErrorConflictType, "provisioner %q already has a webhook with the name %q", prov.Name, newWebhook.Name)
render.Error(w, r, err)
render.Error(w, err)
return
}
}
secret, err := randutil.Bytes(64)
if err != nil {
render.Error(w, r, admin.WrapErrorISE(err, "error generating webhook secret"))
render.Error(w, admin.WrapErrorISE(err, "error generating webhook secret"))
return
}
newWebhook.Secret = base64.StdEncoding.EncodeToString(secret)
@ -117,11 +117,11 @@ func (war *webhookAdminResponder) CreateProvisionerWebhook(w http.ResponseWriter
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
if isBadRequest(err) {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner webhook"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error creating provisioner webhook"))
return
}
render.Error(w, r, admin.WrapErrorISE(err, "error creating provisioner webhook"))
render.Error(w, admin.WrapErrorISE(err, "error creating provisioner webhook"))
return
}
@ -145,21 +145,21 @@ func (war *webhookAdminResponder) DeleteProvisionerWebhook(w http.ResponseWriter
}
}
if !found {
render.JSONStatus(w, r, DeleteResponse{Status: "ok"}, http.StatusOK)
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
return
}
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
if isBadRequest(err) {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error deleting provisioner webhook"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error deleting provisioner webhook"))
return
}
render.Error(w, r, admin.WrapErrorISE(err, "error deleting provisioner webhook"))
render.Error(w, admin.WrapErrorISE(err, "error deleting provisioner webhook"))
return
}
render.JSONStatus(w, r, DeleteResponse{Status: "ok"}, http.StatusOK)
render.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
}
func (war *webhookAdminResponder) UpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request) {
@ -170,12 +170,12 @@ func (war *webhookAdminResponder) UpdateProvisionerWebhook(w http.ResponseWriter
var newWebhook = new(linkedca.Webhook)
if err := read.ProtoJSON(r.Body, newWebhook); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
if err := validateWebhook(newWebhook); err != nil {
render.Error(w, r, err)
render.Error(w, err)
return
}
@ -186,13 +186,13 @@ func (war *webhookAdminResponder) UpdateProvisionerWebhook(w http.ResponseWriter
}
if newWebhook.Secret != "" && newWebhook.Secret != wh.Secret {
err := admin.NewError(admin.ErrorBadRequestType, "webhook secret cannot be updated")
render.Error(w, r, err)
render.Error(w, err)
return
}
newWebhook.Secret = wh.Secret
if newWebhook.Id != "" && newWebhook.Id != wh.Id {
err := admin.NewError(admin.ErrorBadRequestType, "webhook ID cannot be updated")
render.Error(w, r, err)
render.Error(w, err)
return
}
newWebhook.Id = wh.Id
@ -202,18 +202,18 @@ func (war *webhookAdminResponder) UpdateProvisionerWebhook(w http.ResponseWriter
}
if !found {
msg := fmt.Sprintf("provisioner %q has no webhook with the name %q", prov.Name, newWebhook.Name)
err := admin.NewError(admin.ErrorNotFoundType, msg) //nolint:govet // allow non-constant error messages
render.Error(w, r, err)
err := admin.NewError(admin.ErrorNotFoundType, msg)
render.Error(w, err)
return
}
if err := auth.UpdateProvisioner(ctx, prov); err != nil {
if isBadRequest(err) {
render.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner webhook"))
render.Error(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating provisioner webhook"))
return
}
render.Error(w, r, admin.WrapErrorISE(err, "error updating provisioner webhook"))
render.Error(w, admin.WrapErrorISE(err, "error updating provisioner webhook"))
return
}

@ -857,7 +857,7 @@ func TestDB_CreateAdmin(t *testing.T) {
var _dba = new(dbAdmin)
assert.FatalError(t, json.Unmarshal(nu, _dba))
assert.True(t, _dba.ID != "" && _dba.ID == string(key))
assert.True(t, len(_dba.ID) > 0 && _dba.ID == string(key))
assert.Equals(t, _dba.AuthorityID, adm.AuthorityId)
assert.Equals(t, _dba.ProvisionerID, adm.ProvisionerId)
assert.Equals(t, _dba.Subject, adm.Subject)
@ -890,7 +890,7 @@ func TestDB_CreateAdmin(t *testing.T) {
var _dba = new(dbAdmin)
assert.FatalError(t, json.Unmarshal(nu, _dba))
assert.True(t, _dba.ID != "" && _dba.ID == string(key))
assert.True(t, len(_dba.ID) > 0 && _dba.ID == string(key))
assert.Equals(t, _dba.AuthorityID, adm.AuthorityId)
assert.Equals(t, _dba.ProvisionerID, adm.ProvisionerId)
assert.Equals(t, _dba.Subject, adm.Subject)

@ -906,7 +906,7 @@ func TestDB_CreateProvisioner(t *testing.T) {
var _dbp = new(dbProvisioner)
assert.FatalError(t, json.Unmarshal(nu, _dbp))
assert.True(t, _dbp.ID != "" && _dbp.ID == string(key))
assert.True(t, len(_dbp.ID) > 0 && _dbp.ID == string(key))
assert.Equals(t, _dbp.AuthorityID, prov.AuthorityId)
assert.Equals(t, _dbp.Type, prov.Type)
assert.Equals(t, _dbp.Name, prov.Name)
@ -944,7 +944,7 @@ func TestDB_CreateProvisioner(t *testing.T) {
var _dbp = new(dbProvisioner)
assert.FatalError(t, json.Unmarshal(nu, _dbp))
assert.True(t, _dbp.ID != "" && _dbp.ID == string(key))
assert.True(t, len(_dbp.ID) > 0 && _dbp.ID == string(key))
assert.Equals(t, _dbp.AuthorityID, prov.AuthorityId)
assert.Equals(t, _dbp.Type, prov.Type)
assert.Equals(t, _dbp.Name, prov.Name)
@ -1093,7 +1093,7 @@ func TestDB_UpdateProvisioner(t *testing.T) {
var _dbp = new(dbProvisioner)
assert.FatalError(t, json.Unmarshal(nu, _dbp))
assert.True(t, _dbp.ID != "" && _dbp.ID == string(key))
assert.True(t, len(_dbp.ID) > 0 && _dbp.ID == string(key))
assert.Equals(t, _dbp.AuthorityID, prov.AuthorityId)
assert.Equals(t, _dbp.Type, prov.Type)
assert.Equals(t, _dbp.Name, prov.Name)
@ -1188,7 +1188,7 @@ func TestDB_UpdateProvisioner(t *testing.T) {
var _dbp = new(dbProvisioner)
assert.FatalError(t, json.Unmarshal(nu, _dbp))
assert.True(t, _dbp.ID != "" && _dbp.ID == string(key))
assert.True(t, len(_dbp.ID) > 0 && _dbp.ID == string(key))
assert.Equals(t, _dbp.AuthorityID, prov.AuthorityId)
assert.Equals(t, _dbp.Type, prov.Type)
assert.Equals(t, _dbp.Name, prov.Name)

@ -205,8 +205,8 @@ func (e *Error) ToLog() (interface{}, error) {
}
// Render implements render.RenderableError for Error.
func (e *Error) Render(w http.ResponseWriter, r *http.Request) {
func (e *Error) Render(w http.ResponseWriter) {
e.Message = e.Err.Error()
render.JSONStatus(w, r, e, e.StatusCode())
render.JSONStatus(w, e, e.StatusCode())
}

@ -49,7 +49,6 @@ type Authority struct {
templates *templates.Templates
linkedCAToken string
webhookClient *http.Client
httpClient *http.Client
// X509 CA
password []byte
@ -63,10 +62,9 @@ type Authority struct {
x509Enforcers []provisioner.CertificateEnforcer
// SCEP CA
scepOptions *scep.Options
validateSCEP bool
scepAuthority *scep.Authority
scepKeyManager provisioner.SCEPKeyManager
scepOptions *scep.Options
validateSCEP bool
scepAuthority *scep.Authority
// SSH CA
sshHostPassword []byte
@ -141,7 +139,7 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) {
}
}
if a.keyManager != nil {
a.keyManager = newInstrumentedKeyManager(a.keyManager, a.meter)
a.keyManager = &instrumentedKeyManager{a.keyManager, a.meter}
}
if !a.skipInit {
@ -170,7 +168,7 @@ func NewEmbedded(opts ...Option) (*Authority, error) {
}
}
if a.keyManager != nil {
a.keyManager = newInstrumentedKeyManager(a.keyManager, a.meter)
a.keyManager = &instrumentedKeyManager{a.keyManager, a.meter}
}
// Validate required options
@ -257,10 +255,7 @@ func (a *Authority) ReloadAdminResources(ctx context.Context) error {
provClxn := provisioner.NewCollection(provisionerConfig.Audiences)
for _, p := range provList {
if err := p.Init(provisionerConfig); err != nil {
log.Printf("failed to initialize %s provisioner %q: %v\n", p.GetType(), p.GetName(), err)
p = provisioner.Uninitialized{
Interface: p, Reason: err,
}
return err
}
if err := provClxn.Store(p); err != nil {
return err
@ -354,7 +349,7 @@ func (a *Authority) init() error {
return err
}
a.keyManager = newInstrumentedKeyManager(a.keyManager, a.meter)
a.keyManager = &instrumentedKeyManager{a.keyManager, a.meter}
}
// Initialize linkedca client if necessary. On a linked RA, the issuer
@ -451,7 +446,6 @@ func (a *Authority) init() error {
return err
}
a.rootX509Certs = append(a.rootX509Certs, resp.RootCertificate)
a.intermediateX509Certs = append(a.intermediateX509Certs, resp.IntermediateCertificates...)
}
}
@ -492,15 +486,6 @@ func (a *Authority) init() error {
a.certificates.Store(hex.EncodeToString(sum[:]), crt)
}
// Initialize HTTPClient with all root certs
clientRoots := make([]*x509.Certificate, 0, len(a.rootX509Certs)+len(a.federatedX509Certs))
clientRoots = append(clientRoots, a.rootX509Certs...)
clientRoots = append(clientRoots, a.federatedX509Certs...)
a.httpClient, err = newHTTPClient(clientRoots...)
if err != nil {
return err
}
// Decrypt and load SSH keys
var tmplVars templates.Step
if a.config.SSH != nil {
@ -709,42 +694,32 @@ func (a *Authority) init() error {
options := &scep.Options{
Roots: a.rootX509Certs,
Intermediates: a.intermediateX509Certs,
SignerCert: a.intermediateX509Certs[0],
}
// intermediate certificates can be empty in RA mode
if len(a.intermediateX509Certs) > 0 {
options.SignerCert = a.intermediateX509Certs[0]
if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey,
Password: a.password,
}); err != nil {
return err
}
// attempt to create the (default) SCEP signer if the intermediate
// key is configured.
if a.config.IntermediateKey != "" {
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.
_, isRSAKey := options.Signer.Public().(*rsa.PublicKey)
if km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSAKey {
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]
}
// 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]
}
}

@ -69,15 +69,6 @@ func testAuthority(t *testing.T, opts ...Option) *Authority {
EnableSSHCA: &enableSSHCA,
},
},
&provisioner.JWK{
Name: "uninitialized",
Type: "JWK",
Key: clijwk,
Claims: &provisioner.Claims{
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute},
MaxTLSDur: &provisioner.Duration{Duration: time.Minute},
},
},
}
c := &Config{
Address: "127.0.0.1:443",
@ -122,7 +113,7 @@ func TestAuthorityNew(t *testing.T) {
c.Root = []string{"foo"}
return &newTest{
config: c,
err: errors.New(`error reading "foo": no such file or directory`),
err: errors.New("error reading foo: no such file or directory"),
}
},
"fail bad password": func(t *testing.T) *newTest {
@ -140,7 +131,7 @@ func TestAuthorityNew(t *testing.T) {
c.IntermediateCert = "wrong"
return &newTest{
config: c,
err: errors.New(`error reading "wrong": no such file or directory`),
err: errors.New("error reading wrong: no such file or directory"),
}
},
}

@ -64,10 +64,6 @@ func (a *Authority) getProvisionerFromToken(token string) (provisioner.Interface
if !ok {
return nil, nil, fmt.Errorf("provisioner not found or invalid audience (%s)", strings.Join(claims.Audience, ", "))
}
// If the provisioner is disabled, send an appropriate message to the client
if _, ok := p.(provisioner.Uninitialized); ok {
return nil, nil, errs.New(http.StatusUnauthorized, "provisioner %q is disabled due to an initialization error", p.GetName())
}
return p, &claims, nil
}

@ -24,7 +24,6 @@ import (
"go.step.sm/crypto/randutil"
"go.step.sm/crypto/x509util"
"github.com/google/uuid"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/api/render"
"github.com/smallstep/certificates/authority/provisioner"
@ -89,39 +88,6 @@ func generateToken(sub, iss, aud string, sans []string, iat time.Time, jwk *jose
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func generateCustomToken(sub, iss, aud string, jwk *jose.JSONWebKey, extraHeaders, extraClaims map[string]any) (string, error) {
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader("kid", jwk.KeyID)
for k, v := range extraHeaders {
so.WithHeader(jose.HeaderKey(k), v)
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)
if err != nil {
return "", err
}
id, err := randutil.ASCII(64)
if err != nil {
return "", err
}
iat := time.Now()
claims := jose.Claims{
ID: id,
Subject: sub,
Issuer: iss,
IssuedAt: jose.NewNumericDate(iat),
NotBefore: jose.NewNumericDate(iat),
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
Audience: []string{aud},
}
return jose.Signed(sig).Claims(claims).Claims(extraClaims).CompactSerialize()
}
func TestAuthority_authorizeToken(t *testing.T) {
a := testAuthority(t)
@ -338,24 +304,6 @@ func TestAuthority_authorizeToken(t *testing.T) {
code: http.StatusUnauthorized,
}
},
"fail/uninitialized": func(t *testing.T) *authorizeTest {
cl := jose.Claims{
Subject: "test.smallstep.com",
Issuer: "uninitialized",
NotBefore: jose.NewNumericDate(now),
Expiry: jose.NewNumericDate(now.Add(time.Minute)),
Audience: validAudience,
ID: uuid.NewString(),
}
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
return &authorizeTest{
auth: a,
token: raw,
err: errors.New(`provisioner "uninitialized" is disabled due to an initialization error`),
code: http.StatusUnauthorized,
}
},
}
for name, genTestCase := range tests {
@ -543,7 +491,7 @@ func TestAuthority_authorizeSign(t *testing.T) {
}
} else {
if assert.Nil(t, tc.err) {
assert.Equals(t, 11, len(got)) // number of provisioner.SignOptions returned
assert.Equals(t, 10, len(got)) // number of provisioner.SignOptions returned
}
}
})

@ -1,34 +0,0 @@
package authority
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
)
// newHTTPClient returns an HTTP client that trusts the system cert pool and the
// given roots.
func newHTTPClient(roots ...*x509.Certificate) (*http.Client, error) {
pool, err := x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("error initializing http client: %w", err)
}
for _, crt := range roots {
pool.AddCert(crt)
}
tr, ok := http.DefaultTransport.(*http.Transport)
if !ok {
return nil, fmt.Errorf("error initializing http client: type is not *http.Transport")
}
tr = tr.Clone()
tr.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: pool,
}
return &http.Client{
Transport: tr,
}, nil
}

@ -1,105 +0,0 @@
package authority
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/x509util"
)
func mustCertificate(t *testing.T, a *Authority, csr *x509.CertificateRequest) []*x509.Certificate {
t.Helper()
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod)
now := time.Now()
signOpts := provisioner.SignOptions{
NotBefore: provisioner.NewTimeDuration(now),
NotAfter: provisioner.NewTimeDuration(now.Add(5 * time.Minute)),
Backdate: 1 * time.Minute,
}
sans := []string{}
sans = append(sans, csr.DNSNames...)
sans = append(sans, csr.EmailAddresses...)
for _, s := range csr.IPAddresses {
sans = append(sans, s.String())
}
for _, s := range csr.URIs {
sans = append(sans, s.String())
}
key, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
require.NoError(t, err)
token, err := generateToken(csr.Subject.CommonName, "step-cli", testAudiences.Sign[0], sans, now, key)
require.NoError(t, err)
extraOpts, err := a.Authorize(ctx, token)
require.NoError(t, err)
chain, err := a.SignWithContext(ctx, csr, signOpts, extraOpts...)
require.NoError(t, err)
return chain
}
func Test_newHTTPClient(t *testing.T) {
signer, err := keyutil.GenerateDefaultSigner()
require.NoError(t, err)
csr, err := x509util.CreateCertificateRequest("test", []string{"localhost", "127.0.0.1", "[::1]"}, signer)
require.NoError(t, err)
auth := testAuthority(t)
chain := mustCertificate(t, auth, csr)
t.Run("SystemCertPool", func(t *testing.T) {
resp, err := auth.httpClient.Get("https://smallstep.com")
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.NotEmpty(t, b)
assert.NoError(t, resp.Body.Close())
})
t.Run("LocalCertPool", func(t *testing.T) {
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ok")
}))
srv.TLS = &tls.Config{
Certificates: []tls.Certificate{
{Certificate: [][]byte{chain[0].Raw, chain[1].Raw}, PrivateKey: signer, Leaf: chain[0]},
},
}
srv.StartTLS()
defer srv.Close()
resp, err := auth.httpClient.Get(srv.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, []byte("ok"), b)
assert.NoError(t, resp.Body.Close())
t.Run("DefaultClient", func(t *testing.T) {
client := &http.Client{}
_, err := client.Get(srv.URL)
assert.Error(t, err)
})
})
}

@ -203,7 +203,7 @@ func matchURIConstraint(uri *url.URL, constraint string) (bool, error) {
// domainToReverseLabels converts a textual domain name like foo.example.com to
// the list of labels in reverse order, e.g. ["com", "example", "foo"].
func domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {
for domain != "" {
for len(domain) > 0 {
if i := strings.LastIndexByte(domain, '.'); i == -1 {
reverseLabels = append(reverseLabels, domain)
domain = ""
@ -316,7 +316,7 @@ func parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {
} else {
// Atom ("." Atom)*
NextChar:
for in != "" {
for len(in) > 0 {
// atext from RFC 2822, Section 3.2.4
c := in[0]

@ -110,7 +110,7 @@ func newLinkedCAClient(token string) (*linkedCaClient, error) {
tlsConfig.GetClientCertificate = renewer.GetClientCertificate
// Start mTLS client
conn, err := grpc.NewClient(u.Host, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
conn, err := grpc.Dial(u.Host, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
if err != nil {
return nil, errors.Wrapf(err, "error connecting %s", u.Host)
}
@ -478,7 +478,10 @@ func getAuthority(sans []string) (string, error) {
// getRootCertificate creates an insecure majordomo client and returns the
// verified root certificate.
func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error) {
conn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
//nolint:gosec // used in bootstrap protocol
InsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]
})))
@ -486,7 +489,7 @@ func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error)
return nil, errors.Wrapf(err, "error connecting %s", endpoint)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
client := linkedca.NewMajordomoClient(conn)
@ -528,7 +531,11 @@ func getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error)
// login creates a new majordomo client with just the root ca pool and returns
// the signed certificate and tls configuration.
func login(authority, token string, csr *x509.CertificateRequest, signer crypto.PrivateKey, endpoint string, rootCAs *x509.CertPool) (*tls.Certificate, *tls.Config, error) {
conn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
// Connect to majordomo
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
conn, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: rootCAs,
})))
@ -537,7 +544,7 @@ func login(authority, token string, csr *x509.CertificateRequest, signer crypto.
}
// Login to get the signed certificate
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
client := linkedca.NewMajordomoClient(conn)

@ -66,22 +66,6 @@ type instrumentedKeyManager struct {
meter Meter
}
type instrumentedKeyAndDecrypterManager struct {
kms.KeyManager
decrypter kmsapi.Decrypter
meter Meter
}
func newInstrumentedKeyManager(k kms.KeyManager, m Meter) kms.KeyManager {
decrypter, isDecrypter := k.(kmsapi.Decrypter)
switch {
case isDecrypter:
return &instrumentedKeyAndDecrypterManager{&instrumentedKeyManager{k, m}, decrypter, m}
default:
return &instrumentedKeyManager{k, m}
}
}
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}
@ -90,10 +74,6 @@ func (i *instrumentedKeyManager) CreateSigner(req *kmsapi.CreateSignerRequest) (
return
}
func (i *instrumentedKeyAndDecrypterManager) CreateDecrypter(req *kmsapi.CreateDecrypterRequest) (s crypto.Decrypter, err error) {
return i.decrypter.CreateDecrypter(req)
}
type instrumentedKMSSigner struct {
crypto.Signer
meter Meter
@ -105,7 +85,3 @@ func (i *instrumentedKMSSigner) Sign(rand io.Reader, digest []byte, opts crypto.
return
}
var _ kms.KeyManager = (*instrumentedKeyManager)(nil)
var _ kms.KeyManager = (*instrumentedKeyAndDecrypterManager)(nil)
var _ kmsapi.Decrypter = (*instrumentedKeyAndDecrypterManager)(nil)

@ -226,16 +226,6 @@ func WithFullSCEPOptions(options *scep.Options) Option {
}
}
// WithSCEPKeyManager defines the key manager used on SCEP provisioners.
//
// This feature is EXPERIMENTAL and might change at any time.
func WithSCEPKeyManager(skm provisioner.SCEPKeyManager) Option {
return func(a *Authority) error {
a.scepKeyManager = skm
return nil
}
}
// WithSSHUserSigner defines the signer used to sign SSH user certificates.
func WithSSHUserSigner(s crypto.Signer) Option {
return func(a *Authority) error {

@ -91,7 +91,7 @@ func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
// when no host policy engine is configured, but a user policy engine is
// configured, the host certificate is denied.
if e.sshHostPolicy == nil && e.sshUserPolicy != nil {
return errors.New("authority not allowed to sign SSH host certificates when SSH user certificate policy is active")
return errors.New("authority not allowed to sign ssh host certificates")
}
// return result of SSH host policy evaluation
@ -100,12 +100,12 @@ func (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
// when no user policy engine is configured, but a host policy engine is
// configured, the user certificate is denied.
if e.sshUserPolicy == nil && e.sshHostPolicy != nil {
return errors.New("authority not allowed to sign SSH user certificates when SSH host certificate policy is active")
return errors.New("authority not allowed to sign ssh user certificates")
}
// return result of SSH user policy evaluation
return e.sshUserPolicy.IsSSHCertificateAllowed(cert)
default:
return fmt.Errorf("unexpected SSH certificate type %q", cert.CertType)
return fmt.Errorf("unexpected ssh certificate type %q", cert.CertType)
}
}

@ -21,7 +21,6 @@ type HostPolicy policy.SSHNamePolicyEngine
func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, error) {
// return early if no policy engine options to configure
if policyOptions == nil {
//nolint:nilnil,nolintlint // expected values
return nil, nil
}
@ -51,7 +50,6 @@ func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy,
// ensure no policy engine is returned when no name options were provided
if len(options) == 0 {
//nolint:nilnil,nolintlint // expected values
return nil, nil
}
@ -95,7 +93,6 @@ func NewSSHHostPolicyEngine(policyOptions SSHPolicyOptionsInterface) (HostPolicy
func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) {
// return early if no policy engine options to configure
if policyOptions == nil {
//nolint:nilnil,nolintlint // expected values
return nil, nil
}
@ -137,7 +134,6 @@ func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEn
// ensure no policy engine is returned when no name options were provided
if len(options) == 0 {
//nolint:nilnil,nolintlint // expected values
return nil, nil
}

@ -1,89 +1,25 @@
# 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
# certificate for us-east-2
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUVJTc+hOU+8Gk3JlqsX438Dk5c58wDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE3MTE0OVoXDTI5MDQyODE3MTE0OVowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUVJTc+hOU
+8Gk3JlqsX438Dk5c58wEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQAywJQaVNWJqW0R0T0xVOSoN1GLk9x9kKEuN67RN9CLin4dA97qa7Mr5W4P
FZ6vnh5CjOhQBRXV9xJUeYSdqVItNAUFK/fEzDdjf1nUfPlQ3OJ49u6CV01NoJ9m
usvY9kWcV46dqn2bk2MyfTTgvmeqP8fiMRPxxnVRkSzlldP5Fg==
-----END CERTIFICATE-----
# certificate for us-east-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUE1y2NIKCU+Rg4uu4u32koG9QEYIwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE3MzQwMVoXDTI5MDQyODE3MzQwMVowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUE1y2NIKC
U+Rg4uu4u32koG9QEYIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQAlxSmwcWnhT4uAeSinJuz+1BTcKhVSWb5jT8pYjQb8ZoZkXXRGb09mvYeU
NeqOBr27rvRAnaQ/9LUQf72+SahDFuS4CMI8nwowytqbmwquqFr4dxA/SDADyRiF
ea1UoMuNHTY49J/1vPomqsVn7mugTp+TbjqCfOJTpu0temHcFA==
-----END CERTIFICATE-----
# certificate for us-west-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUK2zmY9PUSTR7rc1k2OwPYu4+g7wwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE3MDI0M1oXDTI5MDQyODE3MDI0M1owXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUK2zmY9PU
STR7rc1k2OwPYu4+g7wwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQA1Ng4QmN4n7iPh5CnadSOc0ZfM7by0dBePwZJyGvOHdaw6P6E/vEk76KsC
Q8p+akuzVzVPkU4kBK/TRqLp19wEWoVwhhTaxHjQ1tTRHqXIVlrkw4JrtFbeNM21
GlkSLonuzmNZdivn9WuQYeGe7nUD4w3q9GgiF3CPorJe+UxtbA==
-----END CERTIFICATE-----
# certificate for us-west-2
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUFx8PxCkbHwpD31bOyCtyz3GclbgwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE3MjM1OVoXDTI5MDQyODE3MjM1OVowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUFx8PxCkb
HwpD31bOyCtyz3GclbgwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQBzOl+9Xy1+UsbUBI95HO9mbbdnuX+aMJXgG9uFZNjgNEbMcvx+h8P9IMko
z7PzFdheQQ1NLjsHH9mSR1SyC4m9ja6BsejH5nLBWyCdjfdP3muZM4O5+r7vUa1O
dWU+hP/T7DUrPAIVMOE7mpYa+WPWJrN6BlRwQkKQ7twm9kDalA==
# default certificate for "other regions"
-----BEGIN CERTIFICATE-----
MIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw
FgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu
Y29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC
VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV
BAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN//SRK2knbjySG0ho3yqQM3
e2TDhWO8D2e8+XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD
jbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL
XeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs
77VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh
dHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h
em9uYXdzLmNvbYIJAKnL4UEDMN/FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
BQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF+j9uO6jz7VOmJqO+pRlAbRlvY8T
C1haGgSI/A1uZUKs/Zfnph0oEI0/hu1IIJ/SKBDtN5lvmZ/IzbOPIJWirlsllQIQ
7zvWbGd9c9+Rm3p04oTvhup99la7kZqevJK0QRdD/6NpCKsqP/0=
-----END CERTIFICATE-----
# certificate for eu-south-1
@ -157,7 +93,7 @@ NTpxxcXmUKquX+pHmIkK1LKDO8rNE84jqxrxRsfDi6by82fjVYf2pgjJW8R1FAw+
mL5WQRFexbfB5aXhcMo0AA==
-----END CERTIFICATE-----
# certificate for cn-north-1
# certificate for cn-north-1, cn-northwest-1
-----BEGIN CERTIFICATE-----
MIIDCzCCAnSgAwIBAgIJALSOMbOoU2svMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
@ -178,48 +114,6 @@ oADS0ph+YUz5P/bUCm61wFjlxaTfwKcuTR3ytj7bFLoW5Bm7Sa+TCl3lOGb2taon
SUDlRyNy1jJFstEZjOhs
-----END CERTIFICATE-----
# certificate for cn-northwest-1
-----BEGIN CERTIFICATE-----
MIIDCzCCAnSgAwIBAgIJALSOMbOoU2svMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0yMzA3MDQw
ODM1MzlaFw0yODA3MDIwODM1MzlaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBX
YXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6
b24gV2ViIFNlcnZpY2VzIExMQzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
uhhUNlqAZdcWWB/OSDVDGk3OA99EFzOn/mJlmciQ/Xwu2dFJWmSCqEAE6gjufCjQ
q3voxAhC2CF+elKtJW/C0Sz/LYo60PUqd6iXF4h+upB9HkOOGuWHXsHBTsvgkgGA
1CGgel4U0Cdq+23eANr8N8m28UzljjSnTlrYCHtzN4sCAwEAAaOB1DCB0TALBgNV
HQ8EBAMCB4AwHQYDVR0OBBYEFBkZu3wT27NnYgrfH+xJz4HJaNJoMIGOBgNVHSME
gYYwgYOAFBkZu3wT27NnYgrfH+xJz4HJaNJooWCkXjBcMQswCQYDVQQGEwJVUzEZ
MBcGA1UECBMQV2FzaGluZ3RvbiBTdGF0ZTEQMA4GA1UEBxMHU2VhdHRsZTEgMB4G
A1UEChMXQW1hem9uIFdlYiBTZXJ2aWNlcyBMTEOCCQC0jjGzqFNrLzASBgNVHRMB
Af8ECDAGAQH/AgEAMA0GCSqGSIb3DQEBCwUAA4GBAECji43p+oPkYqmzll7e8Hgb
oADS0ph+YUz5P/bUCm61wFjlxaTfwKcuTR3ytj7bFLoW5Bm7Sa+TCl3lOGb2taon
2h+9NirRK6JYk87LMNvbS40HGPFumJL2NzEsGUeK+MRiWu+Oh5/lJGii3qw4YByx
SUDlRyNy1jJFstEZjOhs
-----END CERTIFICATE-----
# certificate for eu-central-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUFD5GsmkxRuecttwsCG763m3u63UwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE1NTUyOVoXDTI5MDQyODE1NTUyOVowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUFD5Gsmkx
RuecttwsCG763m3u63UwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQBBh0WaXlBsW56Hqk588MmJxsOrvcKfDjF57RgEDgnGnQaJcStCVWDO9UYO
JX2tdsPw+E7AjDqjsuxYaotLn3Mr3mK0sNOXq9BljBnWD4pARg89KZnZI8FN35HQ
O/LYOVHCknuPL123VmVRNs51qQA9hkPjvw21UzpDLxaUxt9Z/w==
-----END CERTIFICATE-----
# certificate for eu-central-2
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXjSGFGiMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
@ -236,27 +130,6 @@ NBElvPCDKFvTJl4QQhToy056llO5GvdS9RK+H8xrP2mrqngApoKTApv93vHBixgF
Sn5KrczRO0YSm3OjkqbydU7DFlmkXXR7GYE+5jbHvQHYiT1J5sMu
-----END CERTIFICATE-----
# certificate for ap-south-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUDLA+x6tTAP3LRTr0z6nOxfsozdMwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE0MTMwMVoXDTI5MDQyODE0MTMwMVowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUDLA+x6tT
AP3LRTr0z6nOxfsozdMwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQAZ7rYKoAwwiiH1M5GJbrT/BEk3OO2VrEPw8ZxgpqQ/EKlzMlOs/0Cyrmp7
UYyUgYFQe5nq37Z94rOUSeMgv/WRxaMwrLlLqD78cuF9DSkXaZIX/kECtVaUnjk8
BZx0QhoIHOpQocJUSlm/dLeMuE0+0A3HNR6JVktGsUdv9ulmKw==
-----END CERTIFICATE-----
# certificate for ap-south-2
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXjwLj9CMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
@ -273,48 +146,6 @@ ETwUZ9mTq2vxlV0KvuetCDNS5u4cJsxe/TGGbYP0yP2qfMl0cCImzRI5W0gn8gog
dervfeT7nH5ih0TWEy/QDWfkQ601L4erm4yh4YQq8vcqAPSkf04N
-----END CERTIFICATE-----
# certificate for ap-southeast-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUSqP6ih+++5KF07NXngrWf26mhSUwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE0MzAxNFoXDTI5MDQyODE0MzAxNFowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUSqP6ih++
+5KF07NXngrWf26mhSUwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQAw13BxW11U/JL58j//Fmk7qqtrZTqXmaz1qm2WlIpJpW750MOcP4ux1uPy
eM0RdVZ4jHSMv5gtLAv/PjExBfw9n6vNCk+5GZG4Xec5DoapBZHXmfMo93sjxBFP
4x9rWn0GuwAVO9ukjYPevq2Rerilrq5VvppHtbATVNY2qecXDA==
-----END CERTIFICATE-----
# certificate for ap-southeast-2
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUFxWyAdk4oiXIOC9PxcgjYYh71mwwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE1MjE0M1oXDTI5MDQyODE1MjE0M1owXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUFxWyAdk4
oiXIOC9PxcgjYYh71mwwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQByjeQe6lr7fiIhoGdjBXYzDfkX0lGGvMIhRh57G1bbceQfaYdZd7Ptc0jl
bpycKGaTvhUdkpMOiV2Hi9dOOYawkdhyJDstmDNKu6P9+b6Kak8He5z3NU1tUR2Y
uTwcz7Ye8Nldx//ws3raErfTI7D6s9m63OX8cAJ/f8bNgikwpw==
-----END CERTIFICATE-----
# certificate for ap-southeast-3
-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIGAXbVDG2yMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT
@ -395,46 +226,25 @@ WX00FTEj4hRVjameE1nENoO8Z7fUVloAFDlDo69fhkJeSvn51D1WRrPnoWGgEfr1
+OfK1bAcKTtfkkkP9r4RdwSjKzO5Zu/B+Wqm3kVEz/QNcz6npmA6
-----END CERTIFICATE-----
# certificate for us-gov-east-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIULVyrqjjwZ461qelPCiShB1KCCj4wDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDUwNzE1MjIzNloXDTI5MDUwNjE1MjIzNlowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCpohwYUVPH9I7Vbkb3WMe/JB0Y/bmfVj3VpcK445YBRO9K80al
esjgBc2tAX4KYg4Lht4EBKccLHTzaNi51YEGX1aLNrSmxhz1+WtzNLNUsyY3zD9z
vwX/3k1+JB2dRA+m+Cpwx4mjzZyAeQtHtegVaAytkmqtxQrSCexBxvqRqQIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU1ZXneBYnPvYXkHVlVjg7918V
gE8wgZkGA1UdIwSBkTCBjoAU1ZXneBYnPvYXkHVlVjg7918VgE+hYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IULVyrqjjw
Z461qelPCiShB1KCCj4wEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQBfAL/YZv0y3zmVbXjyxQCsDloeDCJjFKIu3ameEckeIWJbST9LMto0zViZ
puIAf05x6GQiEqfBMk+YMxJfcTmJB4Ebaj4egFlslJPSHyC2xuydHlr3B04INOH5
Z2oCM68u6GGbj0jZjg7GJonkReG9N72kDva/ukwZKgq8zErQVQ==
-----END CERTIFICATE-----
# certificate for us-gov-west-1
# certificate for us-gov-east-1 and us-gov-west-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUe5wGF3jfb7lUHzvDxmM/ktGCLwwwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDUwNzE3MzAzMloXDTI5MDUwNjE3MzAzMlowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCpohwYUVPH9I7Vbkb3WMe/JB0Y/bmfVj3VpcK445YBRO9K80al
esjgBc2tAX4KYg4Lht4EBKccLHTzaNi51YEGX1aLNrSmxhz1+WtzNLNUsyY3zD9z
vwX/3k1+JB2dRA+m+Cpwx4mjzZyAeQtHtegVaAytkmqtxQrSCexBxvqRqQIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU1ZXneBYnPvYXkHVlVjg7918V
gE8wgZkGA1UdIwSBkTCBjoAU1ZXneBYnPvYXkHVlVjg7918VgE+hYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUe5wGF3jf
b7lUHzvDxmM/ktGCLwwwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQCbTdpx1Iob9SwUReY4exMnlwQlmkTLyA8tYGWzchCJOJJEPfsW0ryy1A0H
YIuvyUty3rJdp9ib8h3GZR71BkZnNddHhy06kPs4p8ewF8+d8OWtOJQcI+ZnFfG4
KyM4rUsBrljpG2aOCm12iACEyrvgJJrS8VZwUDZS6mZEnn/lhA==
MIIDCzCCAnSgAwIBAgIJAIe9Hnq82O7UMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0yMTA3MTQx
NDI3NTdaFw0yNDA3MTMxNDI3NTdaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBX
YXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6
b24gV2ViIFNlcnZpY2VzIExMQzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
qaIcGFFTx/SO1W5G91jHvyQdGP25n1Y91aXCuOOWAUTvSvNGpXrI4AXNrQF+CmIO
C4beBASnHCx082jYudWBBl9Wiza0psYc9flrczSzVLMmN8w/c78F/95NfiQdnUQP
pvgqcMeJo82cgHkLR7XoFWgMrZJqrcUK0gnsQcb6kakCAwEAAaOB1DCB0TALBgNV
HQ8EBAMCB4AwHQYDVR0OBBYEFNWV53gWJz72F5B1ZVY4O/dfFYBPMIGOBgNVHSME
gYYwgYOAFNWV53gWJz72F5B1ZVY4O/dfFYBPoWCkXjBcMQswCQYDVQQGEwJVUzEZ
MBcGA1UECBMQV2FzaGluZ3RvbiBTdGF0ZTEQMA4GA1UEBxMHU2VhdHRsZTEgMB4G
A1UEChMXQW1hem9uIFdlYiBTZXJ2aWNlcyBMTEOCCQCHvR56vNju1DASBgNVHRMB
Af8ECDAGAQH/AgEAMA0GCSqGSIb3DQEBCwUAA4GBACrKjWj460GUPZCGm3/z0dIz
M2BPuH769wcOsqfFZcMKEysSFK91tVtUb1soFwH4/Lb/T0PqNrvtEwD1Nva5k0h2
xZhNNRmDuhOhW1K9wCcnHGRBwY5t4lYL6hNV6hcrqYwGMjTjcAjBG2yMgznSNFle
Rwi/S3BFXISixNx9cILu
-----END CERTIFICATE-----
# certificate for ca-west-1
@ -451,188 +261,4 @@ RZWaBDBJy9x8C2hW+w9lMQjFHkJ7Jy/PHCJ69EzebQIDAQABMA0GCSqGSIb3DQEB
BQUAA4GBAGe9Snkz1A6rHBH6/5kDtYvtPYwhx2sXNxztbhkXErFk40Nw5l459NZx
EeudxJBLoCkkSgYjhRcOZ/gvDVtWG7qyb6fAqgoisyAbk8K9LzxSim2S1nmT9vD8
4B/t/VvwQBylc+ej8kRxMH7fquZLp7IXfmtBzyUqu6Dpbne+chG2
-----END CERTIFICATE-----
# certificate for ap-northeast-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIULgwDh7TiDrPPBJwscqDwiBHkEFQwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTEyMjMxMFoXDTI5MDQyODEyMjMxMFowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IULgwDh7Ti
DrPPBJwscqDwiBHkEFQwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQBtjAglBde1t4F9EHCZOj4qnY6Gigy07Ou54i+lR77MhbpzE8V28Li9l+YT
QMIn6SzJqU3/fIycIro1OVY1lHmaKYgPGSEZxBenSBHfzwDLRmC9oRp4QMe0BjOC
gepj1lUoiN7OA6PtA+ycNlsP0oJvdBjhvayLiuM3tUfLTrgHbw==
-----END CERTIFICATE-----
# certificate for ap-northeast-2
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUbBSn2UIO6vYk4iNWV0RPxJJtHlgwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTEzMzg0NloXDTI5MDQyODEzMzg0NlowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUbBSn2UIO
6vYk4iNWV0RPxJJtHlgwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQAmjTjalG8MGLqWTC2uYqEM8nzI3px1eo0ArvFRsyqQ3fgmWcQpxExqUqRy
l3+2134Kv8dFab04Gut5wlfRtc2OwPKKicmv/IXGN+9bKFnQFjTqif08NIzrDZch
aFT/uvxrIiM+oN2YsHq66GUhO2+xVRXDXVxM/VObFgPERbJpyA==
-----END CERTIFICATE-----
# certificate for ap-northeast-3
-----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-----
# certificate for ca-central-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUIrLgixJJB5C4G8z6pZ5rB0JU2aQwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE1MzU0M1oXDTI5MDQyODE1MzU0M1owXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUIrLgixJJ
B5C4G8z6pZ5rB0JU2aQwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQBHiQJmzyFAaSYs8SpiRijIDZW2RIo7qBKb/pI3rqK6yOWDlPuMr6yNI81D
IrKGGftg4Z+2KETYU4x76HSf0s//vfH3QA57qFaAwddhKYy4BhteFQl/Wex3xTlX
LiwI07kwJvJy3mS6UfQ4HcvZy219tY+0iyOWrz/jVxwq7TOkCw==
-----END CERTIFICATE-----
# certificate for eu-west-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUakDaQ1Zqy87Hy9ESXA1pFC116HkwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE2MTgxMFoXDTI5MDQyODE2MTgxMFowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUakDaQ1Zq
y87Hy9ESXA1pFC116HkwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQADIkn/MqaLGPuK5+prZZ5Ox4bBZLPtreO2C7r0pqU2kPM2lVPyYYydkvP0
lgSmmsErGu/oL9JNztDe2oCA+kNy17ehcsf8cw0uP861czNFKCeU8b7FgBbL+sIm
qi33rAq6owWGi/5uEcfCR+JP7W+oSYVir5r/yDmWzx+BVH5S/g==
-----END CERTIFICATE-----
# certificate for eu-west-2
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUCgCV/DPxYNND/swDgEKGiC5I+EwwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE2MjkxNFoXDTI5MDQyODE2MjkxNFowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUCgCV/DPx
YNND/swDgEKGiC5I+EwwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQATPu/sOE2esNa4+XPEGKlEJSgqzyBSQLQc+VWo6FAJhGG9fp7D97jhHeLC
5vwfmtTAfnGBxadfAOT3ASkxnOZhXtnRna460LtnNHm7ArCVgXKJo7uBn6ViXtFh
uEEw4y6p9YaLQna+VC8Xtgw6WKq2JXuKzuhuNKSFaGGw9vRcHg==
-----END CERTIFICATE-----
# certificate for eu-west-3
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUaC9fX57UDr6u1vBvsCsECKBZQyIwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE2MzczOFoXDTI5MDQyODE2MzczOFowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUaC9fX57U
Dr6u1vBvsCsECKBZQyIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQCARv1bQEDaMEzYI0nPlu8GHcMXgmgA94HyrXhMMcaIlQwocGBs6VILGVhM
TXP2r3JFaPEpmXSQNQHvGA13clKwAZbni8wtzv6qXb4L4muF34iQRHF0nYrEDoK7
mMPR8+oXKKuPO/mv/XKo6XAV5DDERdSYHX5kkA2R9wtvyZjPnQ==
-----END CERTIFICATE-----
# certificate for eu-north-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUN1c9U6U/xiVDFgJcYKZB4NkH1QEwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE2MDYwM1oXDTI5MDQyODE2MDYwM1owXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUN1c9U6U/
xiVDFgJcYKZB4NkH1QEwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQBTIQdoFSDRHkpqNPUbZ9WXR2O5v/9bpmHojMYZb3Hw46wsaRso7STiGGX/
tRqjIkPUIXsdhZ3+7S/RmhFznmZc8e0bjU4n5vi9CJtQSt+1u4E17+V2bF+D3h/7
wcfE0l3414Q8JaTDtfEf/aF3F0uyBvr4MDMd7mFvAMmDmBPSlA==
-----END CERTIFICATE-----
# certificate for sa-east-1
-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIUX4Bh4MQ86Roh37VDRRX1MNOB3TcwDQYJKoZIhvcNAQEL
BQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO
BgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD
MB4XDTI0MDQyOTE2NDYwOVoXDTI5MDQyODE2NDYwOVowXDELMAkGA1UEBhMCVVMx
GTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe
BgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB
UqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku
vGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB
o4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2
UTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ
BgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT
ZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUX4Bh4MQ8
6Roh37VDRRX1MNOB3TcwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF
AAOBgQBnhocfH6ZIX6F5K9+Y9V4HFk8vSaaKL5ytw/P5td1h9ej94KF3xkZ5fyjN
URvGQv3kNmNJBoNarcP9I7JIMjsNPmVzqWawyCEGCZImoARxSS3Fc5EAs2PyBfcD
9nCtzMTaKO09Xyq0wqXVYn1xJsE5d5yBDsGrzaTHKjxo61+ezQ==
-----END CERTIFICATE-----
-----END CERTIFICATE-----

@ -896,5 +896,5 @@ func TestAWS_HardcodedCertificates(t *testing.T) {
assert.True(t, cert.NotAfter.After(time.Now()))
certs = append(certs, cert)
}
assert.Len(t, 33, certs, "expected 33 certificates in aws_certificates.pem, but got %d", len(certs))
assert.Len(t, 15, certs, "expected 15 certificates in aws_certificates.pem")
}

@ -251,14 +251,14 @@ func (p *Azure) Init(config Config) (err error) {
p.assertConfig()
// Decode and validate openid-configuration endpoint
if err = getAndDecode(http.DefaultClient, p.config.oidcDiscoveryURL, &p.oidcConfig); err != nil {
if err = getAndDecode(p.config.oidcDiscoveryURL, &p.oidcConfig); err != nil {
return
}
if err := p.oidcConfig.Validate(); err != nil {
return errors.Wrapf(err, "error parsing %s", p.config.oidcDiscoveryURL)
}
// Get JWK key set
if p.keyStore, err = newKeyStore(http.DefaultClient, p.oidcConfig.JWKSetURI); err != nil {
if p.keyStore, err = newKeyStore(p.oidcConfig.JWKSetURI); err != nil {
return
}

@ -234,24 +234,24 @@ func (c *Claimer) IsSSHCAEnabled() bool {
// Validate validates and modifies the Claims with default values.
func (c *Claimer) Validate() error {
var (
minDur = c.MinTLSCertDuration()
maxDur = c.MaxTLSCertDuration()
defDur = c.DefaultTLSCertDuration()
min = c.MinTLSCertDuration()
max = c.MaxTLSCertDuration()
def = c.DefaultTLSCertDuration()
)
switch {
case minDur <= 0:
case min <= 0:
return errors.Errorf("claims: MinTLSCertDuration must be greater than 0")
case maxDur <= 0:
case max <= 0:
return errors.Errorf("claims: MaxTLSCertDuration must be greater than 0")
case defDur <= 0:
case def <= 0:
return errors.Errorf("claims: DefaultTLSCertDuration must be greater than 0")
case maxDur < minDur:
case max < min:
return errors.Errorf("claims: MaxCertDuration cannot be less "+
"than MinCertDuration: MaxCertDuration - %v, MinCertDuration - %v", maxDur, minDur)
case defDur < minDur:
return errors.Errorf("claims: DefaultCertDuration cannot be less than MinCertDuration: DefaultCertDuration - %v, MinCertDuration - %v", defDur, minDur)
case maxDur < defDur:
return errors.Errorf("claims: MaxCertDuration cannot be less than DefaultCertDuration: MaxCertDuration - %v, DefaultCertDuration - %v", maxDur, defDur)
"than MinCertDuration: MaxCertDuration - %v, MinCertDuration - %v", max, min)
case def < min:
return errors.Errorf("claims: DefaultCertDuration cannot be less than MinCertDuration: DefaultCertDuration - %v, MinCertDuration - %v", def, min)
case max < def:
return errors.Errorf("claims: MaxCertDuration cannot be less than DefaultCertDuration: MaxCertDuration - %v, DefaultCertDuration - %v", max, def)
default:
return nil
}

@ -125,7 +125,7 @@ func (c *Collection) LoadByToken(token *jose.JSONWebToken, claims *jose.Claims)
}
// Try with azp (OIDC)
if payload.AuthorizedParty != "" {
if len(payload.AuthorizedParty) > 0 {
if p, ok := c.LoadByTokenID(payload.AuthorizedParty); ok {
return p, ok
}

@ -26,7 +26,6 @@ type Controller struct {
policy *policyEngine
webhookClient *http.Client
webhooks []*Webhook
httpClient *http.Client
}
// NewController initializes a new provisioner controller.
@ -49,19 +48,9 @@ func NewController(p Interface, claims *Claims, config Config, options *Options)
policy: policy,
webhookClient: config.WebhookClient,
webhooks: options.GetWebhooks(),
httpClient: config.HTTPClient,
}, nil
}
// GetHTTPClient returns the configured HTTP client or the default one if none
// is configured.
func (c *Controller) GetHTTPClient() *http.Client {
if c.httpClient != nil {
return c.httpClient
}
return &http.Client{}
}
// GetIdentity returns the identity for a given email.
func (c *Controller) GetIdentity(ctx context.Context, email string) (*Identity, error) {
if c.IdentityFunc != nil {

@ -9,13 +9,13 @@ import (
"testing"
"time"
"github.com/smallstep/certificates/authority/policy"
"github.com/smallstep/certificates/webhook"
"github.com/stretchr/testify/assert"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x509util"
"go.step.sm/linkedca"
"golang.org/x/crypto/ssh"
"github.com/smallstep/certificates/authority/policy"
"github.com/smallstep/certificates/webhook"
)
var trueValue = true
@ -79,14 +79,12 @@ func TestNewController(t *testing.T) {
wantErr bool
}{
{"ok", args{&JWK{}, nil, Config{
Claims: globalProvisionerClaims,
Audiences: testAudiences,
HTTPClient: &http.Client{},
Claims: globalProvisionerClaims,
Audiences: testAudiences,
}, nil}, &Controller{
Interface: &JWK{},
Audiences: &testAudiences,
Claimer: mustClaimer(t, nil, globalProvisionerClaims),
httpClient: &http.Client{},
Interface: &JWK{},
Audiences: &testAudiences,
Claimer: mustClaimer(t, nil, globalProvisionerClaims),
}, false},
{"ok with claims", args{&JWK{}, &Claims{
DisableRenewal: &defaultDisableRenewal,
@ -147,30 +145,6 @@ func TestNewController(t *testing.T) {
}
}
func TestController_GetHTTPClient(t *testing.T) {
srv := generateTLSJWKServer(2)
defer srv.Close()
type fields struct {
httpClient *http.Client
}
tests := []struct {
name string
fields fields
want *http.Client
}{
{"ok custom", fields{srv.Client()}, srv.Client()},
{"ok default", fields{http.DefaultClient}, http.DefaultClient},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Controller{
httpClient: tt.fields.httpClient,
}
assert.Equal(t, tt.want, c.GetHTTPClient())
})
}
}
func TestController_GetIdentity(t *testing.T) {
ctx := context.Background()
type fields struct {

@ -30,12 +30,6 @@ const gcpCertsURL = "https://www.googleapis.com/oauth2/v3/certs"
// gcpIdentityURL is the base url for the identity document in GCP.
const gcpIdentityURL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity"
// DefaultDisableSSHCAHost is the default value for SSH Host CA used when DisableSSHCAHost is not set
var DefaultDisableSSHCAHost = false
// DefaultDisableSSHCAUser is the default value for SSH User CA used when DisableSSHCAUser is not set
var DefaultDisableSSHCAUser = true
// gcpPayload extends jwt.Claims with custom GCP attributes.
type gcpPayload struct {
jose.Claims
@ -95,8 +89,6 @@ type GCP struct {
ProjectIDs []string `json:"projectIDs"`
DisableCustomSANs bool `json:"disableCustomSANs"`
DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"`
DisableSSHCAUser *bool `json:"disableSSHCAUser,omitempty"`
DisableSSHCAHost *bool `json:"disableSSHCAHost,omitempty"`
InstanceAge Duration `json:"instanceAge,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
@ -207,14 +199,6 @@ func (p *GCP) GetIdentityToken(subject, caURL string) (string, error) {
// Init validates and initializes the GCP provisioner.
func (p *GCP) Init(config Config) (err error) {
if p.DisableSSHCAHost == nil {
p.DisableSSHCAHost = &DefaultDisableSSHCAHost
}
if p.DisableSSHCAUser == nil {
p.DisableSSHCAUser = &DefaultDisableSSHCAUser
}
switch {
case p.Type == "":
return errors.New("provisioner type cannot be empty")
@ -228,7 +212,7 @@ func (p *GCP) Init(config Config) (err error) {
p.assertConfig()
// Initialize key store
if p.keyStore, err = newKeyStore(http.DefaultClient, p.config.CertsURL); err != nil {
if p.keyStore, err = newKeyStore(p.config.CertsURL); err != nil {
return
}
@ -403,41 +387,31 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
}
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
certType, hasCertType := CertTypeFromContext(ctx)
if !hasCertType {
certType = SSHHostCert
}
err := p.isUnauthorizedToIssueSSHCert(certType)
if err != nil {
return nil, err
func (p *GCP) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, error) {
if !p.ctl.Claimer.IsSSHCAEnabled() {
return nil, errs.Unauthorized("gcp.AuthorizeSSHSign; sshCA is disabled for gcp provisioner '%s'", p.GetName())
}
claims, err := p.authorizeToken(token)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "gcp.AuthorizeSSHSign")
}
var principals []string
var keyID string
var defaults SignSSHOptions
var ct sshutil.CertType
var template string
ce := claims.Google.ComputeEngine
signOptions := []SignOption{}
switch certType {
case SSHHostCert:
defaults, keyID, principals, ct, template = p.genHostOptions(ctx, claims)
case SSHUserCert:
defaults, keyID, principals, ct, template = p.genUserOptions(ctx, claims)
default:
return nil, errs.Unauthorized("gcp.AuthorizeSSHSign; invalid requested certType")
// Enforce host certificate.
defaults := SignSSHOptions{
CertType: SSHHostCert,
}
signOptions := []SignOption{}
// Validated principals.
principals := []string{
fmt.Sprintf("%s.c.%s.internal", ce.InstanceName, ce.ProjectID),
fmt.Sprintf("%s.%s.c.%s.internal", ce.InstanceName, ce.Zone, ce.ProjectID),
}
// Only enforce known principals if disable custom sans is true, or it is a user cert request
if p.DisableCustomSANs || certType == SSHUserCert {
// Only enforce known principals if disable custom sans is true.
if p.DisableCustomSANs {
defaults.Principals = principals
} else {
// Check that at least one principal is sent in the request.
@ -447,12 +421,12 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
}
// Certificate templates.
data := sshutil.CreateTemplateData(ct, keyID, principals)
data := sshutil.CreateTemplateData(sshutil.HostCert, ce.InstanceName, principals)
if v, err := unsafeParseSigned(token); err == nil {
data.SetToken(v)
}
templateOptions, err := CustomSSHTemplateOptions(p.Options, data, template)
templateOptions, err := CustomSSHTemplateOptions(p.Options, data, sshutil.DefaultIIDTemplate)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "gcp.AuthorizeSSHSign")
}
@ -471,54 +445,12 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
// Require all the fields in the SSH certificate
&sshCertDefaultValidator{},
// Ensure that all principal names are allowed
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),
// Call webhooks
p.ctl.newWebhookController(
data,
linkedca.Webhook_SSH,
webhook.WithAuthorizationPrincipal(keyID),
webhook.WithAuthorizationPrincipal(ce.InstanceID),
),
), nil
}
func (p *GCP) genHostOptions(_ context.Context, claims *gcpPayload) (SignSSHOptions, string, []string, sshutil.CertType, string) {
ce := claims.Google.ComputeEngine
keyID := ce.InstanceName
principals := []string{
fmt.Sprintf("%s.c.%s.internal", ce.InstanceName, ce.ProjectID),
fmt.Sprintf("%s.%s.c.%s.internal", ce.InstanceName, ce.Zone, ce.ProjectID),
}
return SignSSHOptions{CertType: SSHHostCert}, keyID, principals, sshutil.HostCert, sshutil.DefaultIIDTemplate
}
func FormatServiceAccountUsername(serviceAccountID string) string {
return fmt.Sprintf("sa_%v", serviceAccountID)
}
func (p *GCP) genUserOptions(_ context.Context, claims *gcpPayload) (SignSSHOptions, string, []string, sshutil.CertType, string) {
keyID := claims.Email
principals := []string{
FormatServiceAccountUsername(claims.Subject),
claims.Email,
}
return SignSSHOptions{CertType: SSHUserCert}, keyID, principals, sshutil.UserCert, sshutil.DefaultTemplate
}
func (p *GCP) isUnauthorizedToIssueSSHCert(certType string) error {
if !p.ctl.Claimer.IsSSHCAEnabled() {
return errs.Unauthorized("gcp.AuthorizeSSHSign; sshCA is disabled for gcp provisioner '%s'", p.GetName())
}
if certType == SSHHostCert && *p.DisableSSHCAHost {
return errs.Unauthorized("gcp.AuthorizeSSHSign; sshCA for Hosts is disabled for gcp provisioner '%s'", p.GetName())
}
if certType == SSHUserCert && *p.DisableSSHCAUser {
return errs.Unauthorized("gcp.AuthorizeSSHSign; sshCA for Users is disabled for gcp provisioner '%s'", p.GetName())
}
return nil
}

@ -186,7 +186,6 @@ func TestGCP_Init(t *testing.T) {
args args
wantErr bool
}{
{"ok", fields{"GCP", "name", nil, zero, nil}, args{config, srv.URL}, false},
{"ok", fields{"GCP", "name", nil, zero, nil}, args{config, srv.URL}, false},
{"ok", fields{"GCP", "name", []string{"service-account"}, zero, nil}, args{config, srv.URL}, false},
{"ok", fields{"GCP", "name", []string{"service-account"}, Duration{Duration: 1 * time.Minute}, nil}, args{config, srv.URL}, false},
@ -212,14 +211,6 @@ func TestGCP_Init(t *testing.T) {
if err := p.Init(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("GCP.Init() error = %v, wantErr %v", err, tt.wantErr)
}
if *p.DisableSSHCAUser != true {
t.Errorf("By default DisableSSHCAUser should be true")
}
if *p.DisableSSHCAHost != false {
t.Errorf("By default DisableSSHCAHost should be false")
}
})
}
}
@ -601,9 +592,6 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
p1, err := generateGCP()
assert.FatalError(t, err)
p1.DisableCustomSANs = true
// enable ssh user CA
disableSSCAUser := false
p1.DisableSSHCAUser = &disableSSCAUser
p2, err := generateGCP()
assert.FatalError(t, err)
@ -617,12 +605,6 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
p3.ctl.Claimer, err = NewClaimer(p3.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
p4, err := generateGCP()
assert.FatalError(t, err)
// disable ssh host CA
disableSSCAHost := true
p4.DisableSSHCAHost = &disableSSCAHost
t1, err := generateGCPToken(p1.ServiceAccounts[0],
"https://accounts.google.com", p1.GetID(),
"instance-id", "instance-name", "project-id", "zone",
@ -665,10 +647,6 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
CertType: "host", Principals: []string{"foo.bar", "bar.foo"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),
}
expectedUserOptions := &SignSSHOptions{
CertType: "user", Principals: []string{FormatServiceAccountUsername(p1.ServiceAccounts[0]), "foo@developer.gserviceaccount.com"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(p1.ctl.Claimer.DefaultUserSSHCertDuration())),
}
type args struct {
token string
@ -686,29 +664,22 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
}{
{"ok", p1, args{t1, SignSSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type-host", p1, args{t1, SignSSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type-user", p1, args{t1, SignSSHOptions{CertType: "user"}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"ok-type", p1, args{t1, SignSSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SignSSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principal1", p1, args{t1, SignSSHOptions{Principals: []string{"instance-name.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal1, http.StatusOK, false, false},
{"ok-principal2", p1, args{t1, SignSSHOptions{Principals: []string{"instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal2, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SignSSHOptions{CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-custom", p2, args{t2, SignSSHOptions{Principals: []string{"foo.bar", "bar.foo"}}, pub}, expectedCustomOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},
{"fail-type", p1, args{t1, SignSSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true},
{"fail-principal", p1, args{t1, SignSSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-extra-principal", p1, args{t1, SignSSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-sshCA-disabled", p3, args{"foo", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"fail-type-host", p4, args{"foo", SignSSHOptions{CertType: "host"}, pub}, nil, http.StatusUnauthorized, true, false},
{"fail-type-user", p4, args{"foo", SignSSHOptions{CertType: "host"}, pub}, nil, http.StatusUnauthorized, true, false},
{"fail-invalid-token", p1, args{"foo", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
if tt.args.sshOpts.CertType == SSHUserCert {
ctx = NewContextWithCertType(ctx, SSHUserCert)
}
got, err := tt.gcp.AuthorizeSSHSign(ctx, tt.args.token)
got, err := tt.gcp.AuthorizeSSHSign(context.Background(), tt.args.token)
if (err != nil) != tt.wantErr {
t.Errorf("GCP.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr)
return

@ -19,9 +19,8 @@ import (
// jwtPayload extends jwt.Claims with step attributes.
type jwtPayload struct {
jose.Claims
SANs []string `json:"sans,omitempty"`
Step *stepPayload `json:"step,omitempty"`
Confirmation *cnfPayload `json:"cnf,omitempty"`
SANs []string `json:"sans,omitempty"`
Step *stepPayload `json:"step,omitempty"`
}
type stepPayload struct {
@ -29,10 +28,6 @@ type stepPayload struct {
RA *RAInfo `json:"ra,omitempty"`
}
type cnfPayload struct {
Fingerprint string `json:"x5rt#S256,omitempty"`
}
// JWK is the default provisioner, an entity that can sign tokens necessary for
// signature requests.
type JWK struct {
@ -92,7 +87,7 @@ func (p *JWK) GetType() Type {
// GetEncryptedKey returns the base provisioner encrypted key if it's defined.
func (p *JWK) GetEncryptedKey() (string, string, bool) {
return p.Key.KeyID, p.EncryptedKey, p.EncryptedKey != ""
return p.Key.KeyID, p.EncryptedKey, len(p.EncryptedKey) > 0
}
// Init initializes and validates the fields of a JWK type.
@ -188,12 +183,6 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
}
}
// Check the fingerprint of the certificate request if given.
var fingerprint string
if claims.Confirmation != nil {
fingerprint = claims.Confirmation.Fingerprint
}
return []SignOption{
self,
templateOptions,
@ -201,7 +190,6 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
newProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID).WithControllerOptions(p.ctl),
profileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),
// validators
csrFingerprintValidator(fingerprint),
commonNameSliceValidator(append([]string{claims.Subject}, claims.SANs...)),
defaultPublicKeyValidator{},
newDefaultSANsValidator(ctx, claims.SANs),
@ -249,7 +237,7 @@ func (p *JWK) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e
// Use options in the token.
if opts.CertType != "" {
if certType, err = sshutil.CertTypeFromString(opts.CertType); err != nil {
return nil, errs.BadRequestErr(err, err.Error()) //nolint:govet // allow non-constant error messages
return nil, errs.BadRequestErr(err, err.Error())
}
}
if opts.KeyID != "" {

@ -13,9 +13,7 @@ import (
"testing"
"time"
"go.step.sm/crypto/fingerprint"
"go.step.sm/crypto/jose"
"golang.org/x/crypto/ssh"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/api/render"
@ -249,9 +247,6 @@ func TestJWK_AuthorizeSign(t *testing.T) {
t2, err := generateToken("subject", p1.Name, testAudiences.Sign[0], "name@smallstep.com", []string{}, time.Now(), key1)
assert.FatalError(t, err)
t3, err := generateCustomToken("subject", p1.Name, testAudiences.Sign[0], key1, nil, map[string]any{"cnf": map[string]any{"x5rt#S256": "fingerprint"}})
assert.FatalError(t, err)
// invalid signature
failSig := t1[0 : len(t1)-2]
@ -259,13 +254,12 @@ func TestJWK_AuthorizeSign(t *testing.T) {
token string
}
tests := []struct {
name string
prov *JWK
args args
code int
err error
sans []string
fingerprint string
name string
prov *JWK
args args
code int
err error
sans []string
}{
{
name: "fail-signature",
@ -290,15 +284,6 @@ func TestJWK_AuthorizeSign(t *testing.T) {
err: nil,
sans: []string{"subject"},
},
{
name: "ok-cnf",
prov: p1,
args: args{t3},
code: http.StatusOK,
err: nil,
sans: []string{"subject"},
fingerprint: "fingerprint",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -312,7 +297,7 @@ func TestJWK_AuthorizeSign(t *testing.T) {
}
} else {
if assert.NotNil(t, got) {
assert.Equals(t, 11, len(got))
assert.Equals(t, 10, len(got))
for _, o := range got {
switch v := o.(type) {
case *JWK:
@ -336,8 +321,6 @@ func TestJWK_AuthorizeSign(t *testing.T) {
case *x509NamePolicyValidator:
assert.Equals(t, nil, v.policyEngine)
case *WebhookController:
case csrFingerprintValidator:
assert.Equals(t, tt.fingerprint, string(v))
default:
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
}
@ -410,25 +393,6 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) {
jwk, err := decryptJSONWebKey(p1.EncryptedKey)
assert.FatalError(t, err)
key, err := generateJSONWebKey()
assert.FatalError(t, err)
signer, err := generateJSONWebKey()
assert.FatalError(t, err)
pub := key.Public().Key
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
assert.FatalError(t, err)
//nolint:gosec // tests minimum size of the key
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
assert.FatalError(t, err)
// Calculate fingerprint
sshPub, err := ssh.NewPublicKey(pub)
assert.FatalError(t, err)
fp, err := fingerprint.New(sshPub.Marshal(), crypto.SHA256, fingerprint.Base64RawURLFingerprint)
assert.FatalError(t, err)
iss, aud := p1.Name, testAudiences.SSHSign[0]
t1, err := generateSimpleSSHUserToken(iss, aud, jwk)
@ -437,24 +401,21 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) {
t2, err := generateSimpleSSHHostToken(iss, aud, jwk)
assert.FatalError(t, err)
t3, err := generateCustomToken("sub", iss, aud, jwk, nil, map[string]any{
"step": map[string]any{
"ssh": map[string]any{"certType": "host", "principals": []string{"smallstep.com"}},
},
"cnf": map[string]any{"kid": fp},
})
// invalid signature
failSig := t1[0 : len(t1)-2]
key, err := generateJSONWebKey()
assert.FatalError(t, err)
t4, err := generateCustomToken("sub", iss, aud, jwk, nil, map[string]any{
"step": map[string]any{
"ssh": map[string]any{"certType": "host", "principals": []string{"smallstep.com"}},
},
"cnf": map[string]any{"kid": "bad-fingerprint"},
})
signer, err := generateJSONWebKey()
assert.FatalError(t, err)
// invalid signature
failSig := t1[0 : len(t1)-2]
pub := key.Public().Key
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
assert.FatalError(t, err)
//nolint:gosec // tests minimum size of the key
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
assert.FatalError(t, err)
userDuration := p1.ctl.Claimer.DefaultUserSSHCertDuration()
hostDuration := p1.ctl.Claimer.DefaultHostSSHCertDuration()
@ -490,11 +451,9 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) {
{"host-type", p1, args{t2, SignSSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-principals", p1, args{t2, SignSSHOptions{Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-options", p1, args{t2, SignSSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-cnf", p1, args{t3, SignSSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ignore-bad-cnf", p1, args{t4, SignSSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"fail-sshCA-disabled", p2, args{"foo", SignSSHOptions{}, pub}, expectedUserOptions, http.StatusUnauthorized, true, false},
{"fail-signature", p1, args{failSig, SignSSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
{"fail-rsa1024", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},
{"rail-rsa1024", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

@ -22,7 +22,6 @@ var maxAgeRegex = regexp.MustCompile(`max-age=(\d+)`)
type keyStore struct {
sync.RWMutex
client *http.Client
uri string
keySet jose.JSONWebKeySet
timer *time.Timer
@ -30,13 +29,12 @@ type keyStore struct {
jitter time.Duration
}
func newKeyStore(client *http.Client, uri string) (*keyStore, error) {
keys, age, err := getKeysFromJWKsURI(client, uri)
func newKeyStore(uri string) (*keyStore, error) {
keys, age, err := getKeysFromJWKsURI(uri)
if err != nil {
return nil, err
}
ks := &keyStore{
client: client,
uri: uri,
keySet: keys,
expiry: getExpirationTime(age),
@ -66,7 +64,7 @@ func (ks *keyStore) Get(kid string) (keys []jose.JSONWebKey) {
func (ks *keyStore) reload() {
var next time.Duration
keys, age, err := getKeysFromJWKsURI(ks.client, ks.uri)
keys, age, err := getKeysFromJWKsURI(ks.uri)
if err != nil {
next = ks.nextReloadDuration(ks.jitter / 2)
} else {
@ -92,9 +90,9 @@ func (ks *keyStore) nextReloadDuration(age time.Duration) time.Duration {
return abs(age)
}
func getKeysFromJWKsURI(client *http.Client, uri string) (jose.JSONWebKeySet, time.Duration, error) {
func getKeysFromJWKsURI(uri string) (jose.JSONWebKeySet, time.Duration, error) {
var keys jose.JSONWebKeySet
resp, err := client.Get(uri)
resp, err := http.Get(uri) //nolint:gosec // openid-configuration jwks_uri
if err != nil {
return keys, 0, errors.Wrapf(err, "failed to connect to %s", uri)
}
@ -107,7 +105,7 @@ func getKeysFromJWKsURI(client *http.Client, uri string) (jose.JSONWebKeySet, ti
func getCacheAge(cacheControl string) time.Duration {
age := defaultCacheAge
if cacheControl != "" {
if len(cacheControl) > 0 {
match := maxAgeRegex.FindAllStringSubmatch(cacheControl, -1)
if len(match) > 0 {
if len(match[0]) == 2 {

@ -3,8 +3,6 @@ package provisioner
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
@ -14,19 +12,14 @@ import (
)
func Test_newKeyStore(t *testing.T) {
srv := generateTLSJWKServer(2)
srv.Close()
srv = httptest.NewTLSServer(srv.Config.Handler)
srv := generateJWKServer(2)
defer srv.Close()
ks, err := newKeyStore(srv.Client(), srv.URL)
ks, err := newKeyStore(srv.URL)
assert.FatalError(t, err)
defer ks.Close()
type args struct {
client *http.Client
uri string
uri string
}
tests := []struct {
name string
@ -34,13 +27,12 @@ func Test_newKeyStore(t *testing.T) {
want jose.JSONWebKeySet
wantErr bool
}{
{"ok", args{srv.Client(), srv.URL}, ks.keySet, false},
{"fail", args{srv.Client(), srv.URL + "/error"}, jose.JSONWebKeySet{}, true},
{"fail client", args{http.DefaultClient, srv.URL}, jose.JSONWebKeySet{}, true},
{"ok", args{srv.URL}, ks.keySet, false},
{"fail", args{srv.URL + "/error"}, jose.JSONWebKeySet{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := newKeyStore(tt.args.client, tt.args.uri)
got, err := newKeyStore(tt.args.uri)
if (err != nil) != tt.wantErr {
t.Errorf("newKeyStore() error = %v, wantErr %v", err, tt.wantErr)
return
@ -59,7 +51,7 @@ func Test_keyStore(t *testing.T) {
srv := generateJWKServer(2)
defer srv.Close()
ks, err := newKeyStore(srv.Client(), srv.URL+"/random")
ks, err := newKeyStore(srv.URL + "/random")
assert.FatalError(t, err)
defer ks.Close()
ks.RLock()
@ -103,7 +95,7 @@ func Test_keyStore_noCache(t *testing.T) {
srv := generateJWKServer(2)
defer srv.Close()
ks, err := newKeyStore(srv.Client(), srv.URL+"/no-cache")
ks, err := newKeyStore(srv.URL + "/no-cache")
assert.FatalError(t, err)
defer ks.Close()
ks.RLock()
@ -145,7 +137,7 @@ func Test_keyStore_noCache(t *testing.T) {
func Test_keyStore_Get(t *testing.T) {
srv := generateJWKServer(2)
defer srv.Close()
ks, err := newKeyStore(srv.Client(), srv.URL)
ks, err := newKeyStore(srv.URL)
assert.FatalError(t, err)
defer ks.Close()

@ -78,17 +78,3 @@ func TokenFromContext(ctx context.Context) (string, bool) {
token, ok := ctx.Value(tokenKey{}).(string)
return token, ok
}
// The key to save the certTypeKey in the context.
type certTypeKey struct{}
// NewContextWithCertType creates a new context with the given CertType.
func NewContextWithCertType(ctx context.Context, certType string) context.Context {
return context.WithValue(ctx, certTypeKey{}, certType)
}
// CertTypeFromContext returns the certType stored in the given context.
func CertTypeFromContext(ctx context.Context) (string, bool) {
certType, ok := ctx.Value(certTypeKey{}).(string)
return certType, ok
}

@ -2,13 +2,9 @@ package provisioner
import (
"context"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/x509"
"encoding/base64"
"math/big"
"net"
"time"
@ -341,23 +337,10 @@ func (p *Nebula) authorizeToken(token string, audiences []string) (*nebula.Nebul
return nil, nil, errs.Unauthorized("token is not valid: failed to verify certificate against configured CA")
}
var pub any
switch {
case c.Details.Curve == nebula.Curve_P256:
// When Nebula is used with ECDSA P-256 keys, both CAs and clients use the same type.
ecdhPub, err := ecdh.P256().NewPublicKey(c.Details.PublicKey)
if err != nil {
return nil, nil, errs.UnauthorizedErr(err, errs.WithMessage("failed to parse nebula public key"))
}
publicKeyBytes := ecdhPub.Bytes()
pub = &ecdsa.PublicKey{ // convert back to *ecdsa.PublicKey, because our jose package nor go-jose supports *ecdh.PublicKey
Curve: elliptic.P256(),
X: big.NewInt(0).SetBytes(publicKeyBytes[1:33]),
Y: big.NewInt(0).SetBytes(publicKeyBytes[33:]),
}
case c.Details.IsCA:
var pub interface{}
if c.Details.IsCA {
pub = ed25519.PublicKey(c.Details.PublicKey)
default:
} else {
pub = x25519.PublicKey(c.Details.PublicKey)
}
@ -375,7 +358,6 @@ func (p *Nebula) authorizeToken(token string, audiences []string) (*nebula.Nebul
}, time.Minute); err != nil {
return nil, nil, errs.UnauthorizedErr(err, errs.WithMessage("token is not valid: invalid claims"))
}
// Validate token and subject too.
if !matchesAudience(claims.Audience, audiences) {
return nil, nil, errs.Unauthorized("token is not valid: invalid claims")

@ -3,9 +3,7 @@ package provisioner
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"net"
@ -15,21 +13,21 @@ import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/slackhq/nebula/cert"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/randutil"
"go.step.sm/crypto/x25519"
"go.step.sm/crypto/x509util"
"golang.org/x/crypto/ssh"
)
func mustNebulaIPNet(t *testing.T, s string) *net.IPNet {
t.Helper()
ip, ipNet, err := net.ParseCIDR(s)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
if ip = ip.To4(); ip == nil {
t.Fatalf("nebula only supports ipv4, have %s", s)
}
@ -40,7 +38,9 @@ func mustNebulaIPNet(t *testing.T, s string) *net.IPNet {
func mustNebulaCA(t *testing.T) (*cert.NebulaCertificate, ed25519.PrivateKey) {
t.Helper()
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
nc := &cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: "TestCA",
@ -53,51 +53,26 @@ func mustNebulaCA(t *testing.T) (*cert.NebulaCertificate, ed25519.PrivateKey) {
NotAfter: time.Now().Add(10 * time.Minute),
PublicKey: pub,
IsCA: true,
Curve: cert.Curve_CURVE25519,
},
}
err = nc.Sign(cert.Curve_CURVE25519, priv)
require.NoError(t, err)
return nc, priv
}
func mustNebulaP256CA(t *testing.T) (*cert.NebulaCertificate, *ecdsa.PrivateKey) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
priv, err := key.ECDH()
require.NoError(t, err)
nc := &cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: "TestCA",
Groups: []string{"test"},
Ips: []*net.IPNet{
mustNebulaIPNet(t, "10.1.0.0/16"),
},
Subnets: []*net.IPNet{},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * time.Minute),
PublicKey: priv.PublicKey().Bytes(),
IsCA: true,
Curve: cert.Curve_P256,
},
if err := nc.Sign(priv); err != nil {
t.Fatal(err)
}
err = nc.Sign(cert.Curve_P256, priv.Bytes())
require.NoError(t, err)
return nc, key
return nc, priv
}
func mustNebulaCert(t *testing.T, name string, ipNet *net.IPNet, groups []string, ca *cert.NebulaCertificate, signer ed25519.PrivateKey) (*cert.NebulaCertificate, crypto.Signer) {
t.Helper()
pub, priv, err := x25519.GenerateKey(rand.Reader)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
issuer, err := ca.Sha256Sum()
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
invertedGroups := make(map[string]struct{}, len(groups))
for _, name := range groups {
@ -117,56 +92,14 @@ func mustNebulaCert(t *testing.T, name string, ipNet *net.IPNet, groups []string
IsCA: false,
Issuer: issuer,
InvertedGroups: invertedGroups,
Curve: cert.Curve_CURVE25519,
},
}
err = nc.Sign(cert.Curve_CURVE25519, signer)
require.NoError(t, err)
return nc, priv
}
func mustNebulaP256Cert(t *testing.T, name string, ipNet *net.IPNet, groups []string, ca *cert.NebulaCertificate, signer *ecdsa.PrivateKey) (*cert.NebulaCertificate, crypto.Signer) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
priv, err := key.ECDH()
require.NoError(t, err)
issuer, err := ca.Sha256Sum()
require.NoError(t, err)
invertedGroups := make(map[string]struct{}, len(groups))
for _, name := range groups {
invertedGroups[name] = struct{}{}
}
t1 := time.Now().Truncate(time.Second)
nc := &cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: name,
Ips: []*net.IPNet{ipNet},
Subnets: []*net.IPNet{},
Groups: groups,
NotBefore: t1,
NotAfter: t1.Add(5 * time.Minute),
PublicKey: priv.PublicKey().Bytes(),
IsCA: false,
Issuer: issuer,
InvertedGroups: invertedGroups,
Curve: cert.Curve_P256,
},
if err := nc.Sign(signer); err != nil {
t.Fatal(err)
}
ecdhKey, err := signer.ECDH()
require.NoError(t, err)
err = nc.Sign(cert.Curve_P256, ecdhKey.Bytes())
require.NoError(t, err)
return nc, key
return nc, priv
}
func mustNebulaProvisioner(t *testing.T) (*Nebula, *cert.NebulaCertificate, ed25519.PrivateKey) {
@ -174,31 +107,9 @@ func mustNebulaProvisioner(t *testing.T) (*Nebula, *cert.NebulaCertificate, ed25
nc, signer := mustNebulaCA(t)
ncPem, err := nc.MarshalToPEM()
require.NoError(t, err)
bTrue := true
p := &Nebula{
Type: TypeNebula.String(),
Name: "nebulous",
Roots: ncPem,
Claims: &Claims{
EnableSSHCA: &bTrue,
},
if err != nil {
t.Fatal(err)
}
err = p.Init(Config{
Claims: globalProvisionerClaims,
Audiences: testAudiences,
})
require.NoError(t, err)
return p, nc, signer
}
func mustNebulaP256Provisioner(t *testing.T) (*Nebula, *cert.NebulaCertificate, *ecdsa.PrivateKey) {
t.Helper()
nc, signer := mustNebulaP256CA(t)
ncPem, err := nc.MarshalToPEM()
require.NoError(t, err)
bTrue := true
p := &Nebula{
Type: TypeNebula.String(),
@ -208,29 +119,36 @@ func mustNebulaP256Provisioner(t *testing.T) (*Nebula, *cert.NebulaCertificate,
EnableSSHCA: &bTrue,
},
}
err = p.Init(Config{
if err := p.Init(Config{
Claims: globalProvisionerClaims,
Audiences: testAudiences,
})
require.NoError(t, err)
}); err != nil {
t.Fatal(err)
}
return p, nc, signer
}
func mustNebulaToken(t *testing.T, sub, iss, aud string, iat time.Time, sans []string, nc *cert.NebulaCertificate, key crypto.Signer, algorithm jose.SignatureAlgorithm) string {
func mustNebulaToken(t *testing.T, sub, iss, aud string, iat time.Time, sans []string, nc *cert.NebulaCertificate, key crypto.Signer) string {
t.Helper()
ncDer, err := nc.Marshal()
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader(NebulaCertHeader, ncDer)
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: algorithm, Key: key}, so)
require.NoError(t, err)
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.XEdDSA, Key: key}, so)
if err != nil {
t.Fatal(err)
}
id, err := randutil.ASCII(64)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
claims := struct {
jose.Claims
@ -248,25 +166,32 @@ func mustNebulaToken(t *testing.T, sub, iss, aud string, iat time.Time, sans []s
SANS: sans,
}
tok, err := jose.Signed(sig).Claims(claims).CompactSerialize()
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
return tok
}
func mustNebulaSSHToken(t *testing.T, sub, iss, aud string, iat time.Time, opts *SignSSHOptions, nc *cert.NebulaCertificate, key crypto.Signer, algorithm jose.SignatureAlgorithm) string {
func mustNebulaSSHToken(t *testing.T, sub, iss, aud string, iat time.Time, opts *SignSSHOptions, nc *cert.NebulaCertificate, key crypto.Signer) string {
t.Helper()
ncDer, err := nc.Marshal()
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader(NebulaCertHeader, ncDer)
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: algorithm, Key: key}, so)
require.NoError(t, err)
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.XEdDSA, Key: key}, so)
if err != nil {
t.Fatal(err)
}
id, err := randutil.ASCII(64)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
claims := struct {
jose.Claims
@ -289,15 +214,18 @@ func mustNebulaSSHToken(t *testing.T, sub, iss, aud string, iat time.Time, opts
}
tok, err := jose.Signed(sig).Claims(claims).CompactSerialize()
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
return tok
}
func TestNebula_Init(t *testing.T) {
nc, _ := mustNebulaCA(t)
ncPem, err := nc.MarshalToPEM()
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
cfg := Config{
Claims: globalProvisionerClaims,
@ -399,9 +327,11 @@ func TestNebula_GetIDForToken(t *testing.T) {
func TestNebula_GetTokenID(t *testing.T) {
p, ca, signer := mustNebulaProvisioner(t)
c1, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"group"}, ca, signer)
t1 := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), []string{"test.lan", "10.1.0.1"}, c1, priv, jose.XEdDSA)
t1 := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), []string{"test.lan", "10.1.0.1"}, c1, priv)
_, claims, err := parseToken(t1)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
type args struct {
token string
@ -511,8 +441,8 @@ func TestNebula_AuthorizeSign(t *testing.T) {
ctx := context.TODO()
p, ca, signer := mustNebulaProvisioner(t)
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), []string{"test.lan", "10.1.0.1"}, crt, priv, jose.XEdDSA)
okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), nil, crt, priv, jose.XEdDSA)
ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), []string{"test.lan", "10.1.0.1"}, crt, priv)
okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], now(), nil, crt, priv)
pBadOptions, _, _ := mustNebulaProvisioner(t)
pBadOptions.caPool = p.caPool
@ -557,20 +487,20 @@ func TestNebula_AuthorizeSSHSign(t *testing.T) {
CertType: "host",
KeyID: "test.lan",
Principals: []string{"test.lan", "10.1.0.1"},
}, crt, priv, jose.XEdDSA)
okNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], now(), nil, crt, priv, jose.XEdDSA)
}, crt, priv)
okNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], now(), nil, crt, priv)
okWithValidity := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], now(), &SignSSHOptions{
ValidAfter: NewTimeDuration(now().Add(1 * time.Hour)),
ValidBefore: NewTimeDuration(now().Add(10 * time.Hour)),
}, crt, priv, jose.XEdDSA)
}, crt, priv)
failUserCert := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], now(), &SignSSHOptions{
CertType: "user",
}, crt, priv, jose.XEdDSA)
}, crt, priv)
failPrincipals := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], now(), &SignSSHOptions{
CertType: "host",
KeyID: "test.lan",
Principals: []string{"test.lan", "10.1.0.1", "foo.bar"},
}, crt, priv, jose.XEdDSA)
}, crt, priv)
// Provisioner with SSH disabled
var bFalse bool
@ -662,12 +592,12 @@ func TestNebula_AuthorizeRevoke(t *testing.T) {
// Ok provisioner
p, ca, signer := mustNebulaProvisioner(t)
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Revoke[0], now(), nil, crt, priv, jose.XEdDSA)
ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Revoke[0], now(), nil, crt, priv)
// Fail different CA
nc, signer := mustNebulaCA(t)
crt, priv = mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, nc, signer)
failToken := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Revoke[0], now(), nil, crt, priv, jose.XEdDSA)
failToken := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Revoke[0], now(), nil, crt, priv)
type args struct {
ctx context.Context
@ -696,12 +626,12 @@ func TestNebula_AuthorizeSSHRevoke(t *testing.T) {
// Ok provisioner
p, ca, signer := mustNebulaProvisioner(t)
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
ok := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRevoke[0], now(), nil, crt, priv, jose.XEdDSA)
ok := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRevoke[0], now(), nil, crt, priv)
// Fail different CA
nc, signer := mustNebulaCA(t)
crt, priv = mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, nc, signer)
failToken := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRevoke[0], now(), nil, crt, priv, jose.XEdDSA)
failToken := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRevoke[0], now(), nil, crt, priv)
// Provisioner with SSH disabled
var bFalse bool
@ -735,7 +665,7 @@ func TestNebula_AuthorizeSSHRevoke(t *testing.T) {
func TestNebula_AuthorizeSSHRenew(t *testing.T) {
p, ca, signer := mustNebulaProvisioner(t)
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
t1 := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRenew[0], now(), nil, crt, priv, jose.XEdDSA)
t1 := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRenew[0], now(), nil, crt, priv)
type args struct {
ctx context.Context
@ -767,7 +697,7 @@ func TestNebula_AuthorizeSSHRenew(t *testing.T) {
func TestNebula_AuthorizeSSHRekey(t *testing.T) {
p, ca, signer := mustNebulaProvisioner(t)
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
t1 := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRekey[0], now(), nil, crt, priv, jose.XEdDSA)
t1 := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHRekey[0], now(), nil, crt, priv)
type args struct {
ctx context.Context
@ -804,26 +734,30 @@ func TestNebula_authorizeToken(t *testing.T) {
t1 := now()
p, ca, signer := mustNebulaProvisioner(t)
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.XEdDSA)
okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, nil, crt, priv, jose.XEdDSA)
ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv)
okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, nil, crt, priv)
okSSH := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], t1, &SignSSHOptions{
CertType: "host",
KeyID: "test.lan",
Principals: []string{"test.lan"},
}, crt, priv, jose.XEdDSA)
okSSHNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], t1, nil, crt, priv, jose.XEdDSA)
}, crt, priv)
okSSHNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], t1, nil, crt, priv)
// Token with errors
failNotBefore := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1.Add(1*time.Hour), []string{"10.1.0.1"}, crt, priv, jose.XEdDSA)
failIssuer := mustNebulaToken(t, "test.lan", "foo", p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.XEdDSA)
failAudience := mustNebulaToken(t, "test.lan", p.Name, "foo", t1, []string{"10.1.0.1"}, crt, priv, jose.XEdDSA)
failSubject := mustNebulaToken(t, "", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.XEdDSA)
failNotBefore := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1.Add(1*time.Hour), []string{"10.1.0.1"}, crt, priv)
failIssuer := mustNebulaToken(t, "test.lan", "foo", p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv)
failAudience := mustNebulaToken(t, "test.lan", p.Name, "foo", t1, []string{"10.1.0.1"}, crt, priv)
failSubject := mustNebulaToken(t, "", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv)
// Not a nebula token
jwk, err := generateJSONWebKey()
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
simpleToken, err := generateSimpleToken("iss", "aud", jwk)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
// Provisioner with a different CA
p2, _, _ := mustNebulaProvisioner(t)
@ -890,128 +824,22 @@ func TestNebula_authorizeToken(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := tt.p.authorizeToken(tt.args.token, tt.args.audiences)
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
assert.Nil(t, got1)
if (err != nil) != tt.wantErr {
t.Errorf("Nebula.authorizeToken() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got1 != nil && tt.want1 != nil {
tt.want1.ID = got1.ID
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
assert.Equal(t, tt.want1, got1)
})
}
}
func TestNebula_authorizeToken_P256(t *testing.T) {
t1 := now()
p, ca, signer := mustNebulaP256Provisioner(t)
crt, priv := mustNebulaP256Cert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
ok := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.ES256)
okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1, nil, crt, priv, jose.ES256)
okSSH := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], t1, &SignSSHOptions{
CertType: "host",
KeyID: "test.lan",
Principals: []string{"test.lan"},
}, crt, priv, jose.ES256)
okSSHNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.ctl.Audiences.SSHSign[0], t1, nil, crt, priv, jose.ES256)
// Token with errors
failNotBefore := mustNebulaToken(t, "test.lan", p.Name, p.ctl.Audiences.Sign[0], t1.Add(1*time.Hour), []string{"10.1.0.1"}, crt, priv, jose.ES256)
failIssuer := mustNebulaToken(t, "test.lan", "foo", p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.ES256)
failAudience := mustNebulaToken(t, "test.lan", p.Name, "foo", t1, []string{"10.1.0.1"}, crt, priv, jose.ES256)
failSubject := mustNebulaToken(t, "", p.Name, p.ctl.Audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv, jose.ES256)
// Not a nebula token
jwk, err := generateJSONWebKey()
require.NoError(t, err)
simpleToken, err := generateSimpleToken("iss", "aud", jwk)
require.NoError(t, err)
// Provisioner with a different CA
p2, _, _ := mustNebulaP256Provisioner(t)
x509Claims := jose.Claims{
ID: "[REPLACEME]",
Subject: "test.lan",
Issuer: p.Name,
IssuedAt: jose.NewNumericDate(t1),
NotBefore: jose.NewNumericDate(t1),
Expiry: jose.NewNumericDate(t1.Add(5 * time.Minute)),
Audience: []string{p.ctl.Audiences.Sign[0]},
}
sshClaims := jose.Claims{
ID: "[REPLACEME]",
Subject: "test.lan",
Issuer: p.Name,
IssuedAt: jose.NewNumericDate(t1),
NotBefore: jose.NewNumericDate(t1),
Expiry: jose.NewNumericDate(t1.Add(5 * time.Minute)),
Audience: []string{p.ctl.Audiences.SSHSign[0]},
}
type args struct {
token string
audiences []string
}
tests := []struct {
name string
p *Nebula
args args
want *cert.NebulaCertificate
want1 *jwtPayload
wantErr bool
}{
{"ok x509", p, args{ok, p.ctl.Audiences.Sign}, crt, &jwtPayload{
Claims: x509Claims,
SANs: []string{"10.1.0.1"},
}, false},
{"ok x509 no sans", p, args{okNoSANs, p.ctl.Audiences.Sign}, crt, &jwtPayload{
Claims: x509Claims,
}, false},
{"ok ssh", p, args{okSSH, p.ctl.Audiences.SSHSign}, crt, &jwtPayload{
Claims: sshClaims,
Step: &stepPayload{
SSH: &SignSSHOptions{
CertType: "host",
KeyID: "test.lan",
Principals: []string{"test.lan"},
},
},
}, false},
{"ok ssh no principals", p, args{okSSHNoOptions, p.ctl.Audiences.SSHSign}, crt, &jwtPayload{
Claims: sshClaims,
}, false},
{"fail parse", p, args{"bad.token", p.ctl.Audiences.Sign}, nil, nil, true},
{"fail header", p, args{simpleToken, p.ctl.Audiences.Sign}, nil, nil, true},
{"fail verify", p2, args{ok, p.ctl.Audiences.Sign}, nil, nil, true},
{"fail claims nbf", p, args{failNotBefore, p.ctl.Audiences.Sign}, nil, nil, true},
{"fail claims iss", p, args{failIssuer, p.ctl.Audiences.Sign}, nil, nil, true},
{"fail claims aud", p, args{failAudience, p.ctl.Audiences.Sign}, nil, nil, true},
{"fail claims sub", p, args{failSubject, p.ctl.Audiences.Sign}, nil, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := tt.p.authorizeToken(tt.args.token, tt.args.audiences)
if tt.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
assert.Nil(t, got1)
return
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Nebula.authorizeToken() got = %#v, want %#v", got, tt.want)
t.Error(cmp.Equal(got, tt.want))
}
if got1 != nil && tt.want1 != nil {
tt.want1.ID = got1.ID
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
assert.Equal(t, tt.want1, got1)
if !reflect.DeepEqual(got1, tt.want1) {
t.Errorf("Nebula.authorizeToken() got1 = %v, want %v", got1, tt.want1)
}
})
}
}

@ -93,8 +93,6 @@ type OIDC struct {
ListenAddress string `json:"listenAddress,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
Scopes []string `json:"scopes,omitempty"`
AuthParams []string `json:"authParams,omitempty"`
configuration openIDConfiguration
keyStore *keyStore
ctl *Controller
@ -184,29 +182,23 @@ func (o *OIDC) Init(config Config) (err error) {
if !strings.Contains(u.Path, "/.well-known/openid-configuration") {
u.Path = path.Join(u.Path, "/.well-known/openid-configuration")
}
// Initialize the common provisioner controller
o.ctl, err = NewController(o, o.Claims, config, o.Options)
if err != nil {
return err
}
// Decode and validate openid-configuration
httpClient := o.ctl.GetHTTPClient()
if err := getAndDecode(httpClient, u.String(), &o.configuration); err != nil {
if err := getAndDecode(u.String(), &o.configuration); err != nil {
return err
}
if err := o.configuration.Validate(); err != nil {
return errors.Wrapf(err, "error parsing %s", o.ConfigurationEndpoint)
}
// Replace {tenantid} with the configured one
if o.TenantID != "" {
o.configuration.Issuer = strings.ReplaceAll(o.configuration.Issuer, "{tenantid}", o.TenantID)
}
// Get JWK key set
o.keyStore, err = newKeyStore(httpClient, o.configuration.JWKSetURI)
o.keyStore, err = newKeyStore(o.configuration.JWKSetURI)
if err != nil {
return err
}
o.ctl, err = NewController(o, o.Claims, config, o.Options)
return
}
@ -485,8 +477,8 @@ func (o *OIDC) AuthorizeSSHRevoke(_ context.Context, token string) error {
return errs.Unauthorized("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token")
}
func getAndDecode(client *http.Client, uri string, v interface{}) error {
resp, err := client.Get(uri)
func getAndDecode(uri string, v interface{}) error {
resp, err := http.Get(uri) //nolint:gosec // openid-configuration uri
if err != nil {
return errors.Wrapf(err, "failed to connect to %s", uri)
}

@ -9,7 +9,6 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"time"
@ -71,17 +70,8 @@ func TestOIDC_Getters(t *testing.T) {
func TestOIDC_Init(t *testing.T) {
srv := generateJWKServer(2)
defer srv.Close()
tlsSrv := generateTLSJWKServer(2)
defer tlsSrv.Close()
config := Config{
Claims: globalProvisionerClaims,
HTTPClient: tlsSrv.Client(),
}
badHTTPClientConfig := Config{
Claims: globalProvisionerClaims,
HTTPClient: http.DefaultClient,
Claims: globalProvisionerClaims,
}
badClaims := &Claims{
DefaultTLSDur: &Duration{0},
@ -108,7 +98,6 @@ func TestOIDC_Init(t *testing.T) {
wantErr bool
}{
{"ok", fields{"oidc", "name", "client-id", "client-secret", srv.URL, nil, nil, nil, ""}, args{config}, false},
{"ok tls", fields{"oidc", "name", "client-id", "client-secret", tlsSrv.URL, nil, nil, nil, ""}, args{config}, false},
{"ok-admins", fields{"oidc", "name", "client-id", "client-secret", srv.URL + "/.well-known/openid-configuration", nil, []string{"foo@smallstep.com"}, nil, ""}, args{config}, false},
{"ok-domains", fields{"oidc", "name", "client-id", "client-secret", srv.URL, nil, nil, []string{"smallstep.com"}, ""}, args{config}, false},
{"ok-listen-port", fields{"oidc", "name", "client-id", "client-secret", srv.URL, nil, nil, nil, ":10000"}, args{config}, false},
@ -123,7 +112,6 @@ func TestOIDC_Init(t *testing.T) {
{"bad-parse-url", fields{"oidc", "name", "client-id", "client-secret", ":", nil, nil, nil, ""}, args{config}, true},
{"bad-get-url", fields{"oidc", "name", "client-id", "client-secret", "https://", nil, nil, nil, ""}, args{config}, true},
{"bad-listen-address", fields{"oidc", "name", "client-id", "client-secret", srv.URL, nil, nil, nil, "127.0.0.1"}, args{config}, true},
{"bad-http-client", fields{"oidc", "name", "client-id", "client-secret", tlsSrv.URL, nil, nil, nil, ""}, args{badHTTPClientConfig}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -143,13 +131,9 @@ func TestOIDC_Init(t *testing.T) {
}
if tt.wantErr == false {
assert.Len(t, 2, p.keyStore.keySet.Keys)
u, err := url.Parse(tt.fields.ConfigurationEndpoint)
require.NoError(t, err)
assert.Equals(t, openIDConfiguration{
Issuer: "the-issuer",
JWKSetURI: u.ResolveReference(&url.URL{Path: "/jwks_uri"}).String(),
JWKSetURI: srv.URL + "/jwks_uri",
}, p.configuration)
}
})
@ -161,7 +145,7 @@ func TestOIDC_authorizeToken(t *testing.T) {
defer srv.Close()
var keys jose.JSONWebKeySet
assert.FatalError(t, getAndDecode(srv.Client(), srv.URL+"/private", &keys))
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
issuer := "the-issuer"
tenantID := "ab800f7d-2c87-45fb-b1d0-f90d0bc5ec25"
@ -279,7 +263,7 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
defer srv.Close()
var keys jose.JSONWebKeySet
assert.FatalError(t, getAndDecode(srv.Client(), srv.URL+"/private", &keys))
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
// Create test provisioners
p1, err := generateOIDC()
@ -372,7 +356,7 @@ func TestOIDC_AuthorizeRevoke(t *testing.T) {
defer srv.Close()
var keys jose.JSONWebKeySet
assert.FatalError(t, getAndDecode(srv.Client(), srv.URL+"/private", &keys))
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
// Create test provisioners
p1, err := generateOIDC()
@ -483,7 +467,7 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
defer srv.Close()
var keys jose.JSONWebKeySet
assert.FatalError(t, getAndDecode(srv.Client(), srv.URL+"/private", &keys))
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
// Create test provisioners
p1, err := generateOIDC()
@ -661,7 +645,7 @@ func TestOIDC_AuthorizeSSHRevoke(t *testing.T) {
srv := generateJWKServer(2)
defer srv.Close()
var keys jose.JSONWebKeySet
assert.FatalError(t, getAndDecode(srv.Client(), srv.URL+"/private", &keys))
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
config := Config{Claims: globalProvisionerClaims}
p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"

@ -189,7 +189,7 @@ func TestTemplateOptions(t *testing.T) {
func TestCustomTemplateOptions(t *testing.T) {
csr := parseCertificateRequest(t, "testdata/certs/ecdsa.csr")
csrCertificate := `{"version":0,"subject":{"commonName":"foo"},"rawSubject":"MA4xDDAKBgNVBAMTA2Zvbw==","dnsNames":["foo"],"emailAddresses":null,"ipAddresses":null,"uris":null,"sans":null,"extensions":[{"id":"2.5.29.17","critical":false,"value":"MAWCA2Zvbw=="}],"signatureAlgorithm":""}`
csrCertificate := `{"version":0,"subject":{"commonName":"foo"},"dnsNames":["foo"],"emailAddresses":null,"ipAddresses":null,"uris":null,"sans":null,"extensions":[{"id":"2.5.29.17","critical":false,"value":"MAWCA2Zvbw=="}],"signatureAlgorithm":""}`
data := x509util.TemplateData{
x509util.SubjectKey: x509util.Subject{
CommonName: "foobar",

@ -10,7 +10,6 @@ import (
"strings"
"github.com/pkg/errors"
kmsapi "go.step.sm/crypto/kms/apiv1"
"golang.org/x/crypto/ssh"
"github.com/smallstep/certificates/errs"
@ -34,31 +33,6 @@ type Interface interface {
AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error)
}
// Uninitialized represents a disabled provisioner. Uninitialized provisioners
// are created when the Init methods fails.
type Uninitialized struct {
Interface
Reason error
}
// MarshalJSON returns the JSON encoding of the provisioner with the disabled
// reason.
func (p Uninitialized) MarshalJSON() ([]byte, error) {
provisionerJSON, err := json.Marshal(p.Interface)
if err != nil {
return nil, err
}
reasonJSON, err := json.Marshal(struct {
State string `json:"state"`
StateReason string `json:"stateReason"`
}{"Uninitialized", p.Reason.Error()})
if err != nil {
return nil, err
}
reasonJSON[0] = ','
return append(provisionerJSON[:len(provisionerJSON)-1], reasonJSON...), nil
}
// ErrAllowTokenReuse is an error that is returned by provisioners that allows
// the reuse of tokens.
//
@ -232,13 +206,6 @@ type SSHKeys struct {
HostKeys []ssh.PublicKey
}
// SCEPKeyManager is a KMS interface that combines a KeyManager with a
// Decrypter.
type SCEPKeyManager interface {
kmsapi.KeyManager
kmsapi.Decrypter
}
// Config defines the default parameters used in the initialization of
// provisioners.
type Config struct {
@ -246,7 +213,7 @@ type Config struct {
Claims Claims
// Audiences are the audiences used in the default provisioner, (JWK).
Audiences Audiences
// SSHKeys are the root SSH public keys.
// SSHKeys are the root SSH public keys
SSHKeys *SSHKeys
// GetIdentityFunc is a function that returns an identity that will be
// used by the provisioner to populate certificate attributes.
@ -257,13 +224,8 @@ type Config struct {
// AuthorizeSSHRenewFunc is a function that returns nil if a given SSH
// certificate can be renewed.
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
// WebhookClient is an HTTP client used when performing webhook requests.
// WebhookClient is an http client to use in webhook request
WebhookClient *http.Client
// SCEPKeyManager, if defined, is the interface used by SCEP provisioners.
SCEPKeyManager SCEPKeyManager
// HTTPClient is an HTTP client that trusts the system cert pool and the CA
// roots.
HTTPClient *http.Client
}
type provisioner struct {
@ -358,7 +320,7 @@ func (b *base) AuthorizeSSHSign(context.Context, string) ([]SignOption, error) {
return nil, errs.Unauthorized("provisioner.AuthorizeSSHSign not implemented")
}
// AuthorizeSSHRevoke returns an unimplemented error. Provisioners should overwrite
// AuthorizeRevoke returns an unimplemented error. Provisioners should overwrite
// this method if they will support authorizing tokens for revoking SSH Certificates.
func (b *base) AuthorizeSSHRevoke(context.Context, string) error {
return errs.Unauthorized("provisioner.AuthorizeSSHRevoke not implemented")

@ -6,10 +6,10 @@ import (
"net/http"
"testing"
"github.com/go-jose/go-jose/v3"
"github.com/smallstep/certificates/api/render"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/ssh"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/api/render"
)
func TestType_String(t *testing.T) {
@ -149,11 +149,11 @@ func TestDefaultIdentityFunc(t *testing.T) {
identity, err := DefaultIdentityFunc(context.Background(), tc.p, tc.email)
if err != nil {
if assert.NotNil(t, tc.err) {
assert.Equal(t, tc.err.Error(), err.Error())
assert.Equals(t, tc.err.Error(), err.Error())
}
} else {
if assert.Nil(t, tc.err) {
assert.Equal(t, identity.Usernames, tc.identity.Usernames)
assert.Equals(t, identity.Usernames, tc.identity.Usernames)
}
}
})
@ -243,43 +243,9 @@ func TestUnimplementedMethods(t *testing.T) {
}
var sc render.StatusCodedError
if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface") {
assert.Equal(t, http.StatusUnauthorized, sc.StatusCode())
}
assert.Equal(t, msg, err.Error())
})
}
}
func TestUninitialized_MarshalJSON(t *testing.T) {
p := &JWK{
Name: "bad-provisioner",
Type: "JWK",
Key: &jose.JSONWebKey{
Key: []byte("foo"),
},
}
type fields struct {
Interface Interface
Reason error
}
tests := []struct {
name string
fields fields
want []byte
assertion assert.ErrorAssertionFunc
}{
{"ok", fields{p, errors.New("bad key")}, []byte(`{"type":"JWK","name":"bad-provisioner","key":{"kty":"oct","k":"Zm9v"},"state":"Uninitialized","stateReason":"bad key"}`), assert.NoError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := Uninitialized{
Interface: tt.fields.Interface,
Reason: tt.fields.Reason,
assert.Equals(t, sc.StatusCode(), http.StatusUnauthorized)
}
got, err := p.MarshalJSON()
tt.assertion(t, err)
assert.Equal(t, tt.want, got)
assert.Equals(t, err.Error(), msg)
})
}
}

@ -15,6 +15,7 @@ import (
"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"
@ -58,7 +59,7 @@ type SCEP struct {
encryptionAlgorithm int
challengeValidationController *challengeValidationController
notificationController *notificationController
keyManager SCEPKeyManager
keyManager kmsapi.KeyManager
decrypter crypto.Decrypter
decrypterCertificate *x509.Certificate
signer crypto.Signer
@ -268,38 +269,34 @@ func (s *SCEP) Init(config Config) (err error) {
)
// parse the decrypter key PEM contents if available
if len(s.DecrypterKeyPEM) > 0 {
if decryptionKeyPEM := s.DecrypterKeyPEM; len(decryptionKeyPEM) > 0 {
// try reading the PEM for validation
block, rest := pem.Decode(s.DecrypterKeyPEM)
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,
}
km, err := kms.New(context.Background(), opts)
if err != nil {
if s.keyManager, err = kms.New(context.Background(), opts); err != nil {
return fmt.Errorf("failed initializing kms: %w", err)
}
scepKeyManager, ok := km.(SCEPKeyManager)
kmsDecrypter, ok := s.keyManager.(kmsapi.Decrypter)
if !ok {
return fmt.Errorf("%q is not a kmsapi.Decrypter", opts.Type)
}
s.keyManager = scepKeyManager
if s.decrypter, err = s.keyManager.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKeyPEM: s.DecrypterKeyPEM,
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: s.DecrypterKeyPEM, // TODO(hs): support distinct signer key in the future?
SigningKeyPEM: decryptionKeyPEM, // TODO(hs): support distinct signer key in the future?
Password: []byte(s.DecrypterKeyPassword),
PasswordPrompter: kmsapi.NonInteractivePasswordPrompter,
}); err != nil {
@ -307,44 +304,41 @@ func (s *SCEP) Init(config Config) (err error) {
}
}
if s.DecrypterKeyURI != "" {
kmsType, err := kmsapi.TypeOf(s.DecrypterKeyURI)
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)
}
if config.SCEPKeyManager != nil {
s.keyManager = config.SCEPKeyManager
} else {
if kmsType == kmsapi.DefaultKMS {
kmsType = kmsapi.SoftKMS
}
opts := kms.Options{
Type: kmsType,
URI: s.DecrypterKeyURI,
}
km, err := kms.New(context.Background(), opts)
if err != nil {
return fmt.Errorf("failed initializing kms: %w", err)
}
scepKeyManager, ok := km.(SCEPKeyManager)
if !ok {
return fmt.Errorf("%q is not a kmsapi.Decrypter", opts.Type)
}
s.keyManager = scepKeyManager
}
// Create decrypter and signer with the same key:
// TODO(hs): support distinct signer key in the future?
if s.decrypter, err = s.keyManager.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: s.DecrypterKeyURI,
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: s.DecrypterKeyURI,
SigningKey: decryptionKeyURI, // TODO(hs): support distinct signer key in the future?
Password: []byte(s.DecrypterKeyPassword),
PasswordPrompter: kmsapi.NonInteractivePasswordPrompter,
}); err != nil {

@ -2,27 +2,19 @@ package provisioner
import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/smallstep/certificates/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/kms/softkms"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/pemutil"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/webhook"
)
func Test_challengeValidationController_Validate(t *testing.T) {
@ -374,270 +366,3 @@ func TestSCEP_ValidateChallenge(t *testing.T) {
})
}
}
func TestSCEP_Init(t *testing.T) {
serialize := func(key crypto.PrivateKey, password string) []byte {
var opts []pemutil.Options
if password == "" {
opts = append(opts, pemutil.WithPasswordPrompt("no password", func(s string) ([]byte, error) {
return nil, nil
}))
} else {
opts = append(opts, pemutil.WithPassword([]byte("password")))
}
block, err := pemutil.Serialize(key, opts...)
require.NoError(t, err)
return pem.EncodeToMemory(block)
}
ca, err := minica.New()
require.NoError(t, err)
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
badKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
cert, err := ca.Sign(&x509.Certificate{
Subject: pkix.Name{CommonName: "SCEP decryptor"},
PublicKey: key.Public(),
})
require.NoError(t, err)
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Bytes: cert.Raw,
})
certPEMWithIntermediate := append(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Bytes: cert.Raw,
}), pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Bytes: ca.Intermediate.Raw,
})...)
keyPEM := serialize(key, "password")
keyPEMNoPassword := serialize(key, "")
badKeyPEM := serialize(badKey, "password")
tmp := t.TempDir()
path := filepath.Join(tmp, "rsa.priv")
pathNoPassword := filepath.Join(tmp, "rsa.key")
require.NoError(t, os.WriteFile(path, keyPEM, 0600))
require.NoError(t, os.WriteFile(pathNoPassword, keyPEMNoPassword, 0600))
type args struct {
config Config
}
tests := []struct {
name string
s *SCEP
args args
wantErr bool
}{
{"ok", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyPEM: keyPEM,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, false},
{"ok no password", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyPEM: keyPEMNoPassword,
DecrypterKeyPassword: "",
EncryptionAlgorithmIdentifier: 1,
}, args{Config{Claims: globalProvisionerClaims}}, false},
{"ok with uri", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 1024,
DecrypterCertificate: certPEM,
DecrypterKeyURI: "softkms:path=" + path,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 2,
}, args{Config{Claims: globalProvisionerClaims}}, false},
{"ok with uri no password", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 2048,
DecrypterCertificate: certPEM,
DecrypterKeyURI: "softkms:path=" + pathNoPassword,
DecrypterKeyPassword: "",
EncryptionAlgorithmIdentifier: 3,
}, args{Config{Claims: globalProvisionerClaims}}, false},
{"ok with SCEPKeyManager", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 2048,
DecrypterCertificate: certPEM,
DecrypterKeyURI: "softkms:path=" + pathNoPassword,
DecrypterKeyPassword: "",
EncryptionAlgorithmIdentifier: 4,
}, args{Config{Claims: globalProvisionerClaims, SCEPKeyManager: &softkms.SoftKMS{}}}, false},
{"ok intermediate", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: nil,
DecrypterKeyPEM: nil,
DecrypterKeyPassword: "",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, false},
{"fail type", &SCEP{
Type: "",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyPEM: keyPEM,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail name", &SCEP{
Type: "SCEP",
Name: "",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyPEM: keyPEM,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail minimumPublicKeyLength", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 2001,
DecrypterCertificate: certPEM,
DecrypterKeyPEM: keyPEM,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail encryptionAlgorithmIdentifier", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyPEM: keyPEM,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 5,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail negative encryptionAlgorithmIdentifier", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyPEM: keyPEM,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: -1,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail key decode", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyPEM: []byte("not a pem"),
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail certificate decode", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: []byte("not a pem"),
DecrypterKeyPEM: keyPEM,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail certificate with intermediate", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEMWithIntermediate,
DecrypterKeyPEM: keyPEM,
DecrypterKeyPassword: "password",
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail decrypter password", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyPEM: keyPEM,
DecrypterKeyPassword: "badpassword",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail uri", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyURI: "softkms:path=missing.key",
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail uri password", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyURI: "softkms:path=" + path,
DecrypterKeyPassword: "badpassword",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail uri type", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyURI: "foo:path=" + path,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail missing certificate", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: nil,
DecrypterKeyPEM: keyPEM,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
{"fail key match", &SCEP{
Type: "SCEP",
Name: "scep",
ChallengePassword: "password123",
MinimumPublicKeyLength: 0,
DecrypterCertificate: certPEM,
DecrypterKeyPEM: badKeyPEM,
DecrypterKeyPassword: "password",
EncryptionAlgorithmIdentifier: 0,
}, args{Config{Claims: globalProvisionerClaims}}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.s.Init(tt.args.config); (err != nil) != tt.wantErr {
t.Errorf("SCEP.Init() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

@ -5,10 +5,7 @@ import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/sha256"
"crypto/subtle"
"crypto/x509"
"encoding/base64"
"encoding/json"
"net"
"net/http"
@ -302,19 +299,14 @@ func (v defaultSANsValidator) Valid(req *x509.CertificateRequest) (err error) {
// duration.
type profileDefaultDuration time.Duration
// Modify sets the certificate NotBefore and NotAfter using the following order:
// - From the SignOptions that we get from flags.
// - From x509.Certificate that we get from the template.
// - NotBefore from the current time with a backdate.
// - NotAfter from NotBefore plus the duration in v.
func (v profileDefaultDuration) Modify(cert *x509.Certificate, so SignOptions) error {
var backdate time.Duration
notBefore := timeOr(so.NotBefore.Time(), cert.NotBefore)
notBefore := so.NotBefore.Time()
if notBefore.IsZero() {
notBefore = now()
backdate = -1 * so.Backdate
}
notAfter := timeOr(so.NotAfter.RelativeTime(notBefore), cert.NotAfter)
notAfter := so.NotAfter.RelativeTime(notBefore)
if notAfter.IsZero() {
if v != 0 {
notAfter = notBefore.Add(time.Duration(v))
@ -335,17 +327,11 @@ type profileLimitDuration struct {
notBefore, notAfter time.Time
}
// Modify sets the certificate NotBefore and NotAfter but limits the validity
// period to the certificate to one that is superficially imposed.
//
// The expected NotBefore and NotAfter are set using the following order:
// - From the SignOptions that we get from flags.
// - From x509.Certificate that we get from the template.
// - NotBefore from the current time with a backdate.
// - NotAfter from NotBefore plus the duration v or the notAfter in v if lower.
// Option returns an x509util option that limits the validity period of a
// certificate to one that is superficially imposed.
func (v profileLimitDuration) Modify(cert *x509.Certificate, so SignOptions) error {
var backdate time.Duration
notBefore := timeOr(so.NotBefore.Time(), cert.NotBefore)
notBefore := so.NotBefore.Time()
if notBefore.IsZero() {
notBefore = now()
backdate = -1 * so.Backdate
@ -356,7 +342,7 @@ func (v profileLimitDuration) Modify(cert *x509.Certificate, so SignOptions) err
notBefore, v.notBefore)
}
notAfter := timeOr(so.NotAfter.RelativeTime(notBefore), cert.NotAfter)
notAfter := so.NotAfter.RelativeTime(notBefore)
if notAfter.After(v.notAfter) {
return errs.Forbidden(
"requested certificate notAfter (%s) is after the expiration of the provisioning credential (%s)",
@ -383,8 +369,8 @@ type validityValidator struct {
}
// newValidityValidator return a new validity validator.
func newValidityValidator(minDur, maxDur time.Duration) *validityValidator {
return &validityValidator{min: minDur, max: maxDur}
func newValidityValidator(min, max time.Duration) *validityValidator {
return &validityValidator{min: min, max: max}
}
// Valid validates the certificate validity settings (notBefore/notAfter) and
@ -506,21 +492,3 @@ func (o *provisionerExtensionOption) Modify(cert *x509.Certificate, _ SignOption
cert.ExtraExtensions = append(cert.ExtraExtensions, ext)
return nil
}
// csrFingerprintValidator is a CertificateRequestValidator that checks the
// fingerprint of the certificate request with the provided one.
type csrFingerprintValidator string
func (s csrFingerprintValidator) Valid(cr *x509.CertificateRequest) error {
if s != "" {
expected, err := base64.RawURLEncoding.DecodeString(string(s))
if err != nil {
return errs.ForbiddenErr(err, "error decoding fingerprint")
}
sum := sha256.Sum256(cr.Raw)
if subtle.ConstantTimeCompare(expected, sum[:]) != 1 {
return errs.Forbidden("certificate request fingerprint does not match %q", s)
}
}
return nil
}

@ -598,61 +598,9 @@ func Test_profileDefaultDuration_Option(t *testing.T) {
na := time.Now().Add(10 * time.Minute).UTC()
d := 4 * time.Hour
return test{
pdd: profileDefaultDuration(d),
so: SignOptions{NotBefore: NewTimeDuration(nb), NotAfter: NewTimeDuration(na)},
cert: &x509.Certificate{
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
},
valid: func(cert *x509.Certificate) {
assert.Equals(t, nb, cert.NotBefore)
assert.Equals(t, na, cert.NotAfter)
},
}
},
"ok/cert-with-validity": func() test {
nb := time.Now().Add(5 * time.Minute).UTC()
na := time.Now().Add(10 * time.Minute).UTC()
d := 4 * time.Hour
return test{
pdd: profileDefaultDuration(d),
so: SignOptions{},
cert: &x509.Certificate{
NotBefore: nb,
NotAfter: na,
},
valid: func(cert *x509.Certificate) {
assert.Equals(t, nb, cert.NotBefore)
assert.Equals(t, na, cert.NotAfter)
},
}
},
"ok/cert-notBefore-option-notafter": func() test {
nb := time.Now().Add(5 * time.Minute).UTC()
na := time.Now().Add(10 * time.Minute).UTC()
d := 4 * time.Hour
return test{
pdd: profileDefaultDuration(d),
so: SignOptions{NotAfter: NewTimeDuration(na)},
cert: &x509.Certificate{
NotBefore: nb,
},
valid: func(cert *x509.Certificate) {
assert.Equals(t, nb, cert.NotBefore)
assert.Equals(t, na, cert.NotAfter)
},
}
},
"ok/cert-notAfter-option-notBefore": func() test {
nb := time.Now().Add(5 * time.Minute).UTC()
na := time.Now().Add(10 * time.Minute).UTC()
d := 4 * time.Hour
return test{
pdd: profileDefaultDuration(d),
so: SignOptions{NotBefore: NewTimeDuration(nb)},
cert: &x509.Certificate{
NotAfter: na,
},
pdd: profileDefaultDuration(d),
so: SignOptions{NotBefore: NewTimeDuration(nb), NotAfter: NewTimeDuration(na)},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, nb)
assert.Equals(t, cert.NotAfter, na)
@ -777,28 +725,6 @@ func Test_profileLimitDuration_Option(t *testing.T) {
err: errors.New("requested certificate notAfter ("),
}
},
"fail/cert-validity-notBefore": func() test {
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},
so: SignOptions{},
cert: &x509.Certificate{
NotBefore: n.Add(-time.Second),
NotAfter: n.Add(5 * time.Hour),
},
err: errors.New("requested certificate notBefore ("),
}
},
"fail/cert-validity-notAfter": func() test {
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},
so: SignOptions{},
cert: &x509.Certificate{
NotBefore: n,
NotAfter: n.Add(6*time.Hour + time.Second),
},
err: errors.New("requested certificate notAfter ("),
}
},
"ok/valid-notAfter-requested": func() test {
d, err := ParseTimeDuration("2h")
assert.FatalError(t, err)
@ -856,72 +782,6 @@ func Test_profileLimitDuration_Option(t *testing.T) {
},
}
},
"ok/cert-validity": func() test {
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},
so: SignOptions{},
cert: &x509.Certificate{
NotBefore: n,
NotAfter: n.Add(5 * time.Hour),
},
valid: func(cert *x509.Certificate) {
assert.Equals(t, n, cert.NotBefore)
assert.Equals(t, n.Add(5*time.Hour), cert.NotAfter)
},
}
},
"ok/cert-notBefore-default": func() test {
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},
so: SignOptions{},
cert: &x509.Certificate{
NotBefore: n,
},
valid: func(cert *x509.Certificate) {
assert.Equals(t, n, cert.NotBefore)
assert.Equals(t, n.Add(4*time.Hour), cert.NotAfter)
},
}
},
"ok/cert-notAfter-default": func() test {
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},
so: SignOptions{},
cert: &x509.Certificate{
NotAfter: n.Add(5 * time.Hour),
},
valid: func(cert *x509.Certificate) {
assert.Equals(t, n, cert.NotBefore)
assert.Equals(t, n.Add(5*time.Hour), cert.NotAfter)
},
}
},
"ok/cert-notBefore-option": func() test {
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},
so: SignOptions{NotAfter: NewTimeDuration(n.Add(5 * time.Hour))},
cert: &x509.Certificate{
NotBefore: n,
},
valid: func(cert *x509.Certificate) {
assert.Equals(t, n, cert.NotBefore)
assert.Equals(t, n.Add(5*time.Hour), cert.NotAfter)
},
}
},
"ok/cert-notAfter-option": func() test {
return test{
pld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},
so: SignOptions{NotBefore: NewTimeDuration(n.Add(4 * time.Hour))},
cert: &x509.Certificate{
NotAfter: n.Add(5 * time.Hour),
},
valid: func(cert *x509.Certificate) {
assert.Equals(t, n.Add(4*time.Hour), cert.NotBefore)
assert.Equals(t, n.Add(5*time.Hour), cert.NotAfter)
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {

@ -44,13 +44,6 @@ type SSHCertOptionsValidator interface {
Valid(got SignSSHOptions) error
}
// SSHPublicKeyValidator is the interface used to validate the public key of an
// SSH certificate.
type SSHPublicKeyValidator interface {
SignOption
Valid(got ssh.PublicKey) error
}
// SignSSHOptions contains the options that can be passed to the SignSSH method.
type SignSSHOptions struct {
CertType string `json:"certType"`
@ -283,14 +276,14 @@ func (v *sshCertValidityValidator) Valid(cert *ssh.Certificate, opts SignSSHOpti
return errs.BadRequest("ssh certificate validBefore cannot be before validAfter")
}
var minDur, maxDur time.Duration
var min, max time.Duration
switch cert.CertType {
case ssh.UserCert:
minDur = v.MinUserSSHCertDuration()
maxDur = v.MaxUserSSHCertDuration()
min = v.MinUserSSHCertDuration()
max = v.MaxUserSSHCertDuration()
case ssh.HostCert:
minDur = v.MinHostSSHCertDuration()
maxDur = v.MaxHostSSHCertDuration()
min = v.MinHostSSHCertDuration()
max = v.MaxHostSSHCertDuration()
case 0:
return errs.BadRequest("ssh certificate type has not been set")
default:
@ -302,10 +295,10 @@ func (v *sshCertValidityValidator) Valid(cert *ssh.Certificate, opts SignSSHOpti
dur := time.Duration(cert.ValidBefore-cert.ValidAfter) * time.Second
switch {
case dur < minDur:
return errs.Forbidden("requested duration of %s is less than minimum accepted duration for selected provisioner of %s", dur, minDur)
case dur > maxDur+opts.Backdate:
return errs.Forbidden("requested duration of %s is greater than maximum accepted duration for selected provisioner of %s", dur, maxDur+opts.Backdate)
case dur < min:
return errs.Forbidden("requested duration of %s is less than minimum accepted duration for selected provisioner of %s", dur, min)
case dur > max+opts.Backdate:
return errs.Forbidden("requested duration of %s is greater than maximum accepted duration for selected provisioner of %s", dur, max+opts.Backdate)
default:
return nil
}

@ -90,7 +90,7 @@ func signSSHCertificate(key crypto.PublicKey, opts SignSSHOptions, signOpts []Si
var templErr *sshutil.TemplateError
if errors.As(err, &templErr) {
return nil, errs.NewErr(http.StatusBadRequest, templErr,
errs.WithMessage(templErr.Error()), //nolint:govet // allow non-constant error messages
errs.WithMessage(templErr.Error()),
errs.WithKeyVal("signOptions", signOpts),
)
}

@ -11,17 +11,6 @@ var now = func() time.Time {
return time.Now().UTC()
}
// timeOr returns the first of its arguments that is not equal to the zero time.
// This method can be replaced with cmp.Or when step-ca requires Go 1.22.
func timeOr(ts ...time.Time) time.Time {
for _, t := range ts {
if !t.IsZero() {
return t
}
}
return time.Time{}
}
// TimeDuration is a type that represents a time but the JSON unmarshaling can
// use a time using the RFC 3339 format or a time.Duration string. If a duration
// is used, the time will be set on the first call to TimeDuration.Time.

@ -363,13 +363,11 @@ func generateGCP() (*GCP, error) {
return nil, err
}
p := &GCP{
Type: "GCP",
Name: name,
ServiceAccounts: []string{serviceAccount},
Claims: &globalProvisionerClaims,
DisableSSHCAHost: &DefaultDisableSSHCAHost,
DisableSSHCAUser: &DefaultDisableSSHCAUser,
config: newGCPConfig(),
Type: "GCP",
Name: name,
ServiceAccounts: []string{serviceAccount},
Claims: &globalProvisionerClaims,
config: newGCPConfig(),
keyStore: &keyStore{
keySet: jose.JSONWebKeySet{Keys: []jose.JSONWebKey{*jwk}},
expiry: time.Now().Add(24 * time.Hour),
@ -767,37 +765,6 @@ func generateToken(sub, iss, aud, email string, sans []string, iat time.Time, jw
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func generateCustomToken(sub, iss, aud string, jwk *jose.JSONWebKey, extraHeaders, extraClaims map[string]any) (string, error) {
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader("kid", jwk.KeyID)
for k, v := range extraHeaders {
so.WithHeader(jose.HeaderKey(k), v)
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)
if err != nil {
return "", err
}
id, err := randutil.ASCII(64)
if err != nil {
return "", err
}
iat := time.Now()
claims := jose.Claims{
ID: id,
Subject: sub,
Issuer: iss,
IssuedAt: jose.NewNumericDate(iat),
NotBefore: jose.NewNumericDate(iat),
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
Audience: []string{aud},
}
return jose.Signed(sig).Claims(claims).Claims(extraClaims).CompactSerialize()
}
func generateOIDCToken(sub, iss, aud, email, preferredUsername string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) {
so := new(jose.SignerOptions)
so.WithType("JWT")
@ -1099,7 +1066,7 @@ func parseAWSToken(token string) (*jose.JSONWebToken, *awsPayload, error) {
return tok, claims, nil
}
func generateJWKServerHandler(n int, srv *httptest.Server) http.Handler {
func generateJWKServer(n int) *httptest.Server {
hits := struct {
Hits int `json:"hits"`
}{}
@ -1122,7 +1089,8 @@ func generateJWKServerHandler(n int, srv *httptest.Server) http.Handler {
}
defaultKeySet := must(generateJSONWebKeySet(n))[0].(jose.JSONWebKeySet)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
srv := httptest.NewUnstartedServer(nil)
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Hits++
switch r.RequestURI {
case "/error":
@ -1148,22 +1116,11 @@ func generateJWKServerHandler(n int, srv *httptest.Server) http.Handler {
writeJSON(w, getPublic(defaultKeySet))
}
})
}
func generateJWKServer(n int) *httptest.Server {
srv := httptest.NewUnstartedServer(nil)
srv.Config.Handler = generateJWKServerHandler(n, srv)
srv.Start()
return srv
}
func generateTLSJWKServer(n int) *httptest.Server {
srv := httptest.NewUnstartedServer(nil)
srv.Config.Handler = generateJWKServerHandler(n, srv)
srv.StartTLS()
return srv
}
func generateACME() (*ACME, error) {
// Initialize provisioners
p := &ACME{

@ -15,7 +15,7 @@ import (
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/middleware/requestid"
"github.com/smallstep/certificates/internal/requestid"
"github.com/smallstep/certificates/templates"
"github.com/smallstep/certificates/webhook"
"go.step.sm/linkedca"

@ -24,7 +24,7 @@ import (
"go.step.sm/crypto/x509util"
"go.step.sm/linkedca"
"github.com/smallstep/certificates/middleware/requestid"
"github.com/smallstep/certificates/internal/requestid"
"github.com/smallstep/certificates/webhook"
)

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

Loading…
Cancel
Save