0f66ddb8f8
Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
193 lines
7.7 KiB
Go
193 lines
7.7 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package pki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
// Error prefix; see RFC 8555 Section 6.7. Errors.
|
|
const ErrorPrefix = "urn:ietf:params:acme:error:"
|
|
const ErrorContentType = "application/problem+json"
|
|
|
|
// See RFC 8555 Section 6.7. Errors.
|
|
var ErrAccountDoesNotExist = errors.New("The request specified an account that does not exist")
|
|
|
|
var ErrAcmeDisabled = errors.New("ACME feature is disabled")
|
|
|
|
var (
|
|
ErrAlreadyRevoked = errors.New("The request specified a certificate to be revoked that has already been revoked")
|
|
ErrBadCSR = errors.New("The CSR is unacceptable")
|
|
ErrBadNonce = errors.New("The client sent an unacceptable anti-replay nonce")
|
|
ErrBadPublicKey = errors.New("The JWS was signed by a public key the server does not support")
|
|
ErrBadRevocationReason = errors.New("The revocation reason provided is not allowed by the server")
|
|
ErrBadSignatureAlgorithm = errors.New("The JWS was signed with an algorithm the server does not support")
|
|
ErrCAA = errors.New("Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate")
|
|
ErrCompound = errors.New("Specific error conditions are indicated in the 'subproblems' array")
|
|
ErrConnection = errors.New("The server could not connect to validation target")
|
|
ErrDNS = errors.New("There was a problem with a DNS query during identifier validation")
|
|
ErrExternalAccountRequired = errors.New("The request must include a value for the 'externalAccountBinding' field")
|
|
ErrIncorrectResponse = errors.New("Response received didn't match the challenge's requirements")
|
|
ErrInvalidContact = errors.New("A contact URL for an account was invalid")
|
|
ErrMalformed = errors.New("The request message was malformed")
|
|
ErrOrderNotReady = errors.New("The request attempted to finalize an order that is not ready to be finalized")
|
|
ErrRateLimited = errors.New("The request exceeds a rate limit")
|
|
ErrRejectedIdentifier = errors.New("The server will not issue certificates for the identifier")
|
|
ErrServerInternal = errors.New("The server experienced an internal error")
|
|
ErrTLS = errors.New("The server received a TLS error during validation")
|
|
ErrUnauthorized = errors.New("The client lacks sufficient authorization")
|
|
ErrUnsupportedContact = errors.New("A contact URL for an account used an unsupported protocol scheme")
|
|
ErrUnsupportedIdentifier = errors.New("An identifier is of an unsupported type")
|
|
ErrUserActionRequired = errors.New("Visit the 'instance' URL and take actions specified there")
|
|
)
|
|
|
|
// Mapping of err->name; see table in RFC 8555 Section 6.7. Errors.
|
|
var errIdMappings = map[error]string{
|
|
ErrAccountDoesNotExist: "accountDoesNotExist",
|
|
ErrAlreadyRevoked: "alreadyRevoked",
|
|
ErrBadCSR: "badCSR",
|
|
ErrBadNonce: "badNonce",
|
|
ErrBadPublicKey: "badPublicKey",
|
|
ErrBadRevocationReason: "badRevocationReason",
|
|
ErrBadSignatureAlgorithm: "badSignatureAlgorithm",
|
|
ErrCAA: "caa",
|
|
ErrCompound: "compound",
|
|
ErrConnection: "connection",
|
|
ErrDNS: "dns",
|
|
ErrExternalAccountRequired: "externalAccountRequired",
|
|
ErrIncorrectResponse: "incorrectResponse",
|
|
ErrInvalidContact: "invalidContact",
|
|
ErrMalformed: "malformed",
|
|
ErrOrderNotReady: "orderNotReady",
|
|
ErrRateLimited: "rateLimited",
|
|
ErrRejectedIdentifier: "rejectedIdentifier",
|
|
ErrServerInternal: "serverInternal",
|
|
ErrTLS: "tls",
|
|
ErrUnauthorized: "unauthorized",
|
|
ErrUnsupportedContact: "unsupportedContact",
|
|
ErrUnsupportedIdentifier: "unsupportedIdentifier",
|
|
ErrUserActionRequired: "userActionRequired",
|
|
}
|
|
|
|
// Mapping of err->status codes; see table in RFC 8555 Section 6.7. Errors.
|
|
var errCodeMappings = map[error]int{
|
|
ErrAccountDoesNotExist: http.StatusBadRequest, // See RFC 8555 Section 7.3.1. Finding an Account URL Given a Key.
|
|
ErrAlreadyRevoked: http.StatusBadRequest,
|
|
ErrBadCSR: http.StatusBadRequest,
|
|
ErrBadNonce: http.StatusBadRequest,
|
|
ErrBadPublicKey: http.StatusBadRequest,
|
|
ErrBadRevocationReason: http.StatusBadRequest,
|
|
ErrBadSignatureAlgorithm: http.StatusBadRequest,
|
|
ErrCAA: http.StatusForbidden,
|
|
ErrCompound: http.StatusBadRequest,
|
|
ErrConnection: http.StatusInternalServerError,
|
|
ErrDNS: http.StatusInternalServerError,
|
|
ErrExternalAccountRequired: http.StatusUnauthorized,
|
|
ErrIncorrectResponse: http.StatusBadRequest,
|
|
ErrInvalidContact: http.StatusBadRequest,
|
|
ErrMalformed: http.StatusBadRequest,
|
|
ErrOrderNotReady: http.StatusForbidden, // See RFC 8555 Section 7.4. Applying for Certificate Issuance.
|
|
ErrRateLimited: http.StatusTooManyRequests,
|
|
ErrRejectedIdentifier: http.StatusBadRequest,
|
|
ErrServerInternal: http.StatusInternalServerError,
|
|
ErrTLS: http.StatusInternalServerError,
|
|
ErrUnauthorized: http.StatusUnauthorized,
|
|
ErrUnsupportedContact: http.StatusBadRequest,
|
|
ErrUnsupportedIdentifier: http.StatusBadRequest,
|
|
ErrUserActionRequired: http.StatusUnauthorized,
|
|
}
|
|
|
|
type ErrorResponse struct {
|
|
StatusCode int `json:"-"`
|
|
Type string `json:"type"`
|
|
Detail string `json:"detail"`
|
|
Subproblems []*ErrorResponse `json:"subproblems"`
|
|
}
|
|
|
|
func (e *ErrorResponse) Marshal() (*logical.Response, error) {
|
|
body, err := json.Marshal(e)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed marshalling of error response: %w", err)
|
|
}
|
|
|
|
var resp logical.Response
|
|
resp.Data = map[string]interface{}{
|
|
logical.HTTPContentType: ErrorContentType,
|
|
logical.HTTPRawBody: body,
|
|
logical.HTTPStatusCode: e.StatusCode,
|
|
}
|
|
|
|
return &resp, nil
|
|
}
|
|
|
|
func FindType(given error) (err error, id string, code int, found bool) {
|
|
matchedError := false
|
|
for err, id = range errIdMappings {
|
|
if errors.Is(given, err) {
|
|
matchedError = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// If the given error was not matched from one of the standard ACME errors
|
|
// make this error, force ErrServerInternal
|
|
if !matchedError {
|
|
err = ErrServerInternal
|
|
id = errIdMappings[err]
|
|
}
|
|
|
|
code = errCodeMappings[err]
|
|
|
|
return
|
|
}
|
|
|
|
func TranslateError(given error) (*logical.Response, error) {
|
|
if errors.Is(given, logical.ErrReadOnly) {
|
|
return nil, given
|
|
}
|
|
|
|
if errors.Is(given, ErrAcmeDisabled) {
|
|
return logical.RespondWithStatusCode(nil, nil, http.StatusNotFound)
|
|
}
|
|
|
|
// We're multierror aware here: if we're given a list of errors, assume
|
|
// they're structured so the first error is the outer error and the inner
|
|
// subproblems are subsequent in the multierror.
|
|
var remaining []error
|
|
if unwrapped, ok := given.(*multierror.Error); ok {
|
|
remaining = unwrapped.Errors[1:]
|
|
given = unwrapped.Errors[0]
|
|
}
|
|
|
|
_, id, code, found := FindType(given)
|
|
if !found && len(remaining) > 0 {
|
|
// Translate multierrors into a generic error code.
|
|
id = errIdMappings[ErrCompound]
|
|
code = errCodeMappings[ErrCompound]
|
|
}
|
|
|
|
var body ErrorResponse
|
|
body.Type = ErrorPrefix + id
|
|
body.Detail = given.Error()
|
|
body.StatusCode = code
|
|
|
|
for _, subgiven := range remaining {
|
|
_, subid, _, _ := FindType(subgiven)
|
|
|
|
var sub ErrorResponse
|
|
sub.Type = ErrorPrefix + subid
|
|
body.Detail = subgiven.Error()
|
|
|
|
body.Subproblems = append(body.Subproblems, &sub)
|
|
}
|
|
|
|
return body.Marshal()
|
|
}
|