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"` 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"` // 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"` } // NewError creates a new 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 ...interface{}) *Error { return NewError(pt, msg, args...).withDetail() } func (e *Error) withDetail() *Error { if e == nil || e.Status >= 500 || e.Err == nil { return e } e.Detail = fmt.Sprintf("%s: %s", e.Detail, e.Err) return e } // AddSubproblems adds the Subproblems to Error. It // returns the Error, allowing for fluent addition. func (e *Error) AddSubproblems(subproblems ...Subproblem) *Error { e.Subproblems = append(e.Subproblems, subproblems...) return e } // NewSubproblem creates a new Subproblem. The msg and args // are used to create a new error, which is set as the Detail, allowing // for more detailed error messages to be returned to the ACME client. 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...)) } } func WrapDetailedError(typ ProblemType, err error, msg string, args ...interface{}) *Error { return WrapError(typ, err, msg, args...).withDetail() } // WrapErrorISE shortcut to wrap an internal server error type. func WrapErrorISE(err error, msg string, args ...interface{}) *Error { return WrapError(ErrorServerInternalType, err, msg, args...) } // 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()) }