mirror of
https://github.com/smallstep/certificates.git
synced 2024-11-19 09:25:37 +00:00
1c38113e44
When validating an ACME challenge (`device-attest-01` in this case, but it's also true for others), and validation fails, the CA didn't return a lot of information about why the challenge had failed. By introducing the ACME `Subproblem` type, an ACME `Error` can include some additional information about what went wrong when validating the challenge. This is a WIP commit. The `Subproblem` isn't created in many code paths yet, just for the `step` format at the moment. Will probably follow up with some more improvements to how the ACME error is handled. Also need to cleanup some debug things (q.Q)
413 lines
14 KiB
Go
413 lines
14 KiB
Go
package acme
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
|
||
"github.com/pkg/errors"
|
||
"github.com/smallstep/certificates/api/render"
|
||
)
|
||
|
||
// ProblemType is the type of the ACME problem.
|
||
type ProblemType int
|
||
|
||
const (
|
||
// ErrorAccountDoesNotExistType request specified an account that does not exist
|
||
ErrorAccountDoesNotExistType ProblemType = iota
|
||
// ErrorAlreadyRevokedType request specified a certificate to be revoked that has already been revoked
|
||
ErrorAlreadyRevokedType
|
||
// ErrorBadAttestationStatementType WebAuthn attestation statement could not be verified
|
||
ErrorBadAttestationStatementType
|
||
// ErrorBadCSRType CSR is unacceptable (e.g., due to a short key)
|
||
ErrorBadCSRType
|
||
// ErrorBadNonceType client sent an unacceptable anti-replay nonce
|
||
ErrorBadNonceType
|
||
// ErrorBadPublicKeyType JWS was signed by a public key the server does not support
|
||
ErrorBadPublicKeyType
|
||
// ErrorBadRevocationReasonType revocation reason provided is not allowed by the server
|
||
ErrorBadRevocationReasonType
|
||
// ErrorBadSignatureAlgorithmType JWS was signed with an algorithm the server does not support
|
||
ErrorBadSignatureAlgorithmType
|
||
// ErrorCaaType Authority Authorization (CAA) records forbid the CA from issuing a certificate
|
||
ErrorCaaType
|
||
// ErrorCompoundType error conditions are indicated in the “subproblems” array.
|
||
ErrorCompoundType
|
||
// ErrorConnectionType server could not connect to validation target
|
||
ErrorConnectionType
|
||
// ErrorDNSType was a problem with a DNS query during identifier validation
|
||
ErrorDNSType
|
||
// ErrorExternalAccountRequiredType request must include a value for the “externalAccountBinding” field
|
||
ErrorExternalAccountRequiredType
|
||
// ErrorIncorrectResponseType received didn’t match the challenge’s requirements
|
||
ErrorIncorrectResponseType
|
||
// ErrorInvalidContactType URL for an account was invalid
|
||
ErrorInvalidContactType
|
||
// ErrorMalformedType request message was malformed
|
||
ErrorMalformedType
|
||
// ErrorOrderNotReadyType request attempted to finalize an order that is not ready to be finalized
|
||
ErrorOrderNotReadyType
|
||
// ErrorRateLimitedType request exceeds a rate limit
|
||
ErrorRateLimitedType
|
||
// ErrorRejectedIdentifierType server will not issue certificates for the identifier
|
||
ErrorRejectedIdentifierType
|
||
// ErrorServerInternalType server experienced an internal error
|
||
ErrorServerInternalType
|
||
// ErrorTLSType server received a TLS error during validation
|
||
ErrorTLSType
|
||
// ErrorUnauthorizedType client lacks sufficient authorization
|
||
ErrorUnauthorizedType
|
||
// ErrorUnsupportedContactType URL for an account used an unsupported protocol scheme
|
||
ErrorUnsupportedContactType
|
||
// ErrorUnsupportedIdentifierType identifier is of an unsupported type
|
||
ErrorUnsupportedIdentifierType
|
||
// ErrorUserActionRequiredType the “instance” URL and take actions specified there
|
||
ErrorUserActionRequiredType
|
||
// ErrorNotImplementedType operation is not implemented
|
||
ErrorNotImplementedType
|
||
)
|
||
|
||
// String returns the string representation of the acme problem type,
|
||
// fulfilling the Stringer interface.
|
||
func (ap ProblemType) String() string {
|
||
switch ap {
|
||
case ErrorAccountDoesNotExistType:
|
||
return "accountDoesNotExist"
|
||
case ErrorAlreadyRevokedType:
|
||
return "alreadyRevoked"
|
||
case ErrorBadAttestationStatementType:
|
||
return "badAttestationStatement"
|
||
case ErrorBadCSRType:
|
||
return "badCSR"
|
||
case ErrorBadNonceType:
|
||
return "badNonce"
|
||
case ErrorBadPublicKeyType:
|
||
return "badPublicKey"
|
||
case ErrorBadRevocationReasonType:
|
||
return "badRevocationReason"
|
||
case ErrorBadSignatureAlgorithmType:
|
||
return "badSignatureAlgorithm"
|
||
case ErrorCaaType:
|
||
return "caa"
|
||
case ErrorCompoundType:
|
||
return "compound"
|
||
case ErrorConnectionType:
|
||
return "connection"
|
||
case ErrorDNSType:
|
||
return "dns"
|
||
case ErrorExternalAccountRequiredType:
|
||
return "externalAccountRequired"
|
||
case ErrorInvalidContactType:
|
||
return "incorrectResponse"
|
||
case ErrorMalformedType:
|
||
return "malformed"
|
||
case ErrorOrderNotReadyType:
|
||
return "orderNotReady"
|
||
case ErrorRateLimitedType:
|
||
return "rateLimited"
|
||
case ErrorRejectedIdentifierType:
|
||
return "rejectedIdentifier"
|
||
case ErrorServerInternalType:
|
||
return "serverInternal"
|
||
case ErrorTLSType:
|
||
return "tls"
|
||
case ErrorUnauthorizedType:
|
||
return "unauthorized"
|
||
case ErrorUnsupportedContactType:
|
||
return "unsupportedContact"
|
||
case ErrorUnsupportedIdentifierType:
|
||
return "unsupportedIdentifier"
|
||
case ErrorUserActionRequiredType:
|
||
return "userActionRequired"
|
||
case ErrorNotImplementedType:
|
||
return "notImplemented"
|
||
default:
|
||
return fmt.Sprintf("unsupported type ACME error type '%d'", int(ap))
|
||
}
|
||
}
|
||
|
||
type errorMetadata struct {
|
||
details string
|
||
status int
|
||
typ string
|
||
String string
|
||
}
|
||
|
||
var (
|
||
officialACMEPrefix = "urn:ietf:params:acme:error:"
|
||
errorServerInternalMetadata = errorMetadata{
|
||
typ: officialACMEPrefix + ErrorServerInternalType.String(),
|
||
details: "The server experienced an internal error",
|
||
status: 500,
|
||
}
|
||
errorMap = map[ProblemType]errorMetadata{
|
||
ErrorAccountDoesNotExistType: {
|
||
typ: officialACMEPrefix + ErrorAccountDoesNotExistType.String(),
|
||
details: "Account does not exist",
|
||
status: 400,
|
||
},
|
||
ErrorAlreadyRevokedType: {
|
||
typ: officialACMEPrefix + ErrorAlreadyRevokedType.String(),
|
||
details: "Certificate already revoked",
|
||
status: 400,
|
||
},
|
||
ErrorBadCSRType: {
|
||
typ: officialACMEPrefix + ErrorBadCSRType.String(),
|
||
details: "The CSR is unacceptable",
|
||
status: 400,
|
||
},
|
||
ErrorBadNonceType: {
|
||
typ: officialACMEPrefix + ErrorBadNonceType.String(),
|
||
details: "Unacceptable anti-replay nonce",
|
||
status: 400,
|
||
},
|
||
ErrorBadPublicKeyType: {
|
||
typ: officialACMEPrefix + ErrorBadPublicKeyType.String(),
|
||
details: "The jws was signed by a public key the server does not support",
|
||
status: 400,
|
||
},
|
||
ErrorBadRevocationReasonType: {
|
||
typ: officialACMEPrefix + ErrorBadRevocationReasonType.String(),
|
||
details: "The revocation reason provided is not allowed by the server",
|
||
status: 400,
|
||
},
|
||
ErrorBadSignatureAlgorithmType: {
|
||
typ: officialACMEPrefix + ErrorBadSignatureAlgorithmType.String(),
|
||
details: "The JWS was signed with an algorithm the server does not support",
|
||
status: 400,
|
||
},
|
||
ErrorBadAttestationStatementType: {
|
||
typ: officialACMEPrefix + ErrorBadAttestationStatementType.String(),
|
||
details: "Attestation statement cannot be verified",
|
||
status: 400,
|
||
},
|
||
ErrorCaaType: {
|
||
typ: officialACMEPrefix + ErrorCaaType.String(),
|
||
details: "Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate",
|
||
status: 400,
|
||
},
|
||
ErrorCompoundType: {
|
||
typ: officialACMEPrefix + ErrorCompoundType.String(),
|
||
details: "Specific error conditions are indicated in the “subproblems” array",
|
||
status: 400,
|
||
},
|
||
ErrorConnectionType: {
|
||
typ: officialACMEPrefix + ErrorConnectionType.String(),
|
||
details: "The server could not connect to validation target",
|
||
status: 400,
|
||
},
|
||
ErrorDNSType: {
|
||
typ: officialACMEPrefix + ErrorDNSType.String(),
|
||
details: "There was a problem with a DNS query during identifier validation",
|
||
status: 400,
|
||
},
|
||
ErrorExternalAccountRequiredType: {
|
||
typ: officialACMEPrefix + ErrorExternalAccountRequiredType.String(),
|
||
details: "The request must include a value for the \"externalAccountBinding\" field",
|
||
status: 400,
|
||
},
|
||
ErrorIncorrectResponseType: {
|
||
typ: officialACMEPrefix + ErrorIncorrectResponseType.String(),
|
||
details: "Response received didn't match the challenge's requirements",
|
||
status: 400,
|
||
},
|
||
ErrorInvalidContactType: {
|
||
typ: officialACMEPrefix + ErrorInvalidContactType.String(),
|
||
details: "A contact URL for an account was invalid",
|
||
status: 400,
|
||
},
|
||
ErrorMalformedType: {
|
||
typ: officialACMEPrefix + ErrorMalformedType.String(),
|
||
details: "The request message was malformed",
|
||
status: 400,
|
||
},
|
||
ErrorOrderNotReadyType: {
|
||
typ: officialACMEPrefix + ErrorOrderNotReadyType.String(),
|
||
details: "The request attempted to finalize an order that is not ready to be finalized",
|
||
status: 400,
|
||
},
|
||
ErrorRateLimitedType: {
|
||
typ: officialACMEPrefix + ErrorRateLimitedType.String(),
|
||
details: "The request exceeds a rate limit",
|
||
status: 400,
|
||
},
|
||
ErrorRejectedIdentifierType: {
|
||
typ: officialACMEPrefix + ErrorRejectedIdentifierType.String(),
|
||
details: "The server will not issue certificates for the identifier",
|
||
status: 400,
|
||
},
|
||
ErrorNotImplementedType: {
|
||
typ: officialACMEPrefix + ErrorRejectedIdentifierType.String(),
|
||
details: "The requested operation is not implemented",
|
||
status: 501,
|
||
},
|
||
ErrorTLSType: {
|
||
typ: officialACMEPrefix + ErrorTLSType.String(),
|
||
details: "The server received a TLS error during validation",
|
||
status: 400,
|
||
},
|
||
ErrorUnauthorizedType: {
|
||
typ: officialACMEPrefix + ErrorUnauthorizedType.String(),
|
||
details: "The client lacks sufficient authorization",
|
||
status: 401,
|
||
},
|
||
ErrorUnsupportedContactType: {
|
||
typ: officialACMEPrefix + ErrorUnsupportedContactType.String(),
|
||
details: "A contact URL for an account used an unsupported protocol scheme",
|
||
status: 400,
|
||
},
|
||
ErrorUnsupportedIdentifierType: {
|
||
typ: officialACMEPrefix + ErrorUnsupportedIdentifierType.String(),
|
||
details: "An identifier is of an unsupported type",
|
||
status: 400,
|
||
},
|
||
ErrorUserActionRequiredType: {
|
||
typ: officialACMEPrefix + ErrorUserActionRequiredType.String(),
|
||
details: "Visit the “instance” URL and take actions specified there",
|
||
status: 400,
|
||
},
|
||
ErrorServerInternalType: errorServerInternalMetadata,
|
||
}
|
||
)
|
||
|
||
// Error represents an ACME Error
|
||
type Error struct {
|
||
Type string `json:"type"`
|
||
Detail string `json:"detail"`
|
||
Subproblems []Subproblem `json:"subproblems,omitempty"`
|
||
|
||
// The "identifier" field MUST NOT be present at the top level in ACME
|
||
// problem documents. It can only be present in subproblems.
|
||
// Subproblems need not all have the same type, and they do not need to
|
||
// match the top level type.
|
||
Identifier Identifier `json:"identifier,omitempty"` // TODO(hs): seems unused and MUST NOT be present; this can likely be removed
|
||
Err error `json:"-"`
|
||
Status int `json:"-"`
|
||
}
|
||
|
||
// Subproblem represents an ACME subproblem. It's fairly
|
||
// similar to an ACME error, but differs in that it can't
|
||
// include subproblems itself, the error is reflected
|
||
// in the Detail property and doesn't have a Status.
|
||
type Subproblem struct {
|
||
Type string `json:"type"`
|
||
Detail string `json:"detail"`
|
||
Identifier *Identifier `json:"identifier,omitempty"`
|
||
}
|
||
|
||
// AddSubproblems adds the Subproblems to Error. It
|
||
// returns the Error, allowing for fluent addition.
|
||
func (e *Error) AddSubproblems(subproblems ...Subproblem) *Error {
|
||
e.Subproblems = append(e.Subproblems, subproblems...)
|
||
return e
|
||
}
|
||
|
||
// NewError creates a new Error type.
|
||
func NewError(pt ProblemType, msg string, args ...interface{}) *Error {
|
||
return newError(pt, errors.Errorf(msg, args...))
|
||
}
|
||
|
||
// NewSubproblem creates a new Subproblem. The msg and args
|
||
// are used to create a new error, which is set as the Detail, allowing
|
||
// for more detailed error messages to be returned to the ACME client.
|
||
func NewSubproblem(pt ProblemType, msg string, args ...interface{}) Subproblem {
|
||
e := newError(pt, fmt.Errorf(msg, args...))
|
||
s := Subproblem{
|
||
Type: e.Type,
|
||
Detail: e.Err.Error(),
|
||
}
|
||
return s
|
||
}
|
||
|
||
// NewSubproblemWithIdentifier creates a new Subproblem with a specific ACME
|
||
// Identifier. It calls NewSubproblem and sets the Identifier.
|
||
func NewSubproblemWithIdentifier(pt ProblemType, identifier Identifier, msg string, args ...interface{}) Subproblem {
|
||
s := NewSubproblem(pt, msg, args...)
|
||
s.Identifier = &identifier
|
||
return s
|
||
}
|
||
|
||
func newError(pt ProblemType, err error) *Error {
|
||
meta, ok := errorMap[pt]
|
||
if !ok {
|
||
meta = errorServerInternalMetadata
|
||
return &Error{
|
||
Type: meta.typ,
|
||
Detail: meta.details,
|
||
Status: meta.status,
|
||
Err: err,
|
||
}
|
||
}
|
||
|
||
return &Error{
|
||
Type: meta.typ,
|
||
Detail: meta.details,
|
||
Status: meta.status,
|
||
Err: err,
|
||
}
|
||
}
|
||
|
||
// NewErrorISE creates a new ErrorServerInternalType Error.
|
||
func NewErrorISE(msg string, args ...interface{}) *Error {
|
||
return NewError(ErrorServerInternalType, msg, args...)
|
||
}
|
||
|
||
// WrapError attempts to wrap the internal error.
|
||
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
||
var e *Error
|
||
switch {
|
||
case err == nil:
|
||
return nil
|
||
case errors.As(err, &e):
|
||
if e.Err == nil {
|
||
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
||
} else {
|
||
e.Err = errors.Wrapf(e.Err, msg, args...)
|
||
}
|
||
return e
|
||
default:
|
||
return newError(typ, errors.Wrapf(err, msg, args...))
|
||
}
|
||
}
|
||
|
||
// WrapErrorISE shortcut to wrap an internal server error type.
|
||
func WrapErrorISE(err error, msg string, args ...interface{}) *Error {
|
||
return WrapError(ErrorServerInternalType, err, msg, args...)
|
||
}
|
||
|
||
// StatusCode returns the status code and implements the StatusCoder interface.
|
||
func (e *Error) StatusCode() int {
|
||
return e.Status
|
||
}
|
||
|
||
// Error implements the error interface.
|
||
func (e *Error) Error() string {
|
||
if e.Err == nil {
|
||
return e.Detail
|
||
}
|
||
return e.Err.Error()
|
||
}
|
||
|
||
// Cause returns the internal error and implements the Causer interface.
|
||
func (e *Error) Cause() error {
|
||
if e.Err == nil {
|
||
return errors.New(e.Detail)
|
||
}
|
||
return e.Err
|
||
}
|
||
|
||
// ToLog implements the EnableLogger interface.
|
||
func (e *Error) ToLog() (interface{}, error) {
|
||
b, err := json.Marshal(e)
|
||
if err != nil {
|
||
return nil, WrapErrorISE(err, "error marshaling acme.Error for logging")
|
||
}
|
||
return string(b), nil
|
||
}
|
||
|
||
// Render implements render.RenderableError for Error.
|
||
func (e *Error) Render(w http.ResponseWriter) {
|
||
w.Header().Set("Content-Type", "application/problem+json")
|
||
render.JSONStatus(w, e, e.StatusCode())
|
||
}
|