336 lines
9.7 KiB
Go
336 lines
9.7 KiB
Go
// Package errors provide a special error type for use in Boulder. This error
|
|
// type carries additional type information with it, and has two special powers:
|
|
//
|
|
// 1. It is recognized by our gRPC code, and the type metadata and detail string
|
|
// will cross gRPC boundaries intact.
|
|
//
|
|
// 2. It is recognized by our frontend API "rendering" code, and will be
|
|
// automatically converted to the corresponding urn:ietf:params:acme:error:...
|
|
// ACME Problem Document.
|
|
//
|
|
// This means that a deeply-nested service (such as the SA) that wants to ensure
|
|
// that the ACME client sees a particular problem document (such as NotFound)
|
|
// can return a BoulderError and be sure that it will be propagated all the way
|
|
// to the client.
|
|
//
|
|
// Note, however, that any additional context wrapped *around* the BoulderError
|
|
// (such as by fmt.Errorf("oops: %w")) will be lost when the error is converted
|
|
// into a problem document. Similarly, any type information wrapped *by* a
|
|
// BoulderError (such as a sql.ErrNoRows) is lost at the gRPC serialization
|
|
// boundary.
|
|
package errors
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/letsencrypt/boulder/identifier"
|
|
)
|
|
|
|
// ErrorType provides a coarse category for BoulderErrors.
|
|
// Objects of type ErrorType should never be directly returned by other
|
|
// functions; instead use the methods below to create an appropriate
|
|
// BoulderError wrapping one of these types.
|
|
type ErrorType int
|
|
|
|
// These numeric constants are used when sending berrors through gRPC.
|
|
const (
|
|
// InternalServer is deprecated. Instead, pass a plain Go error. That will get
|
|
// turned into a probs.InternalServerError by the WFE.
|
|
InternalServer ErrorType = iota
|
|
_ // Reserved, previously NotSupported
|
|
Malformed
|
|
Unauthorized
|
|
NotFound
|
|
RateLimit
|
|
RejectedIdentifier
|
|
InvalidEmail
|
|
ConnectionFailure
|
|
_ // Reserved, previously WrongAuthorizationState
|
|
CAA
|
|
MissingSCTs
|
|
Duplicate
|
|
OrderNotReady
|
|
DNS
|
|
BadPublicKey
|
|
BadCSR
|
|
AlreadyRevoked
|
|
BadRevocationReason
|
|
UnsupportedContact
|
|
// The requested serial number does not exist in the `serials` table.
|
|
UnknownSerial
|
|
Conflict
|
|
// Defined in https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/00/
|
|
InvalidProfile
|
|
// The certificate being indicated for replacement already has a replacement
|
|
// order.
|
|
AlreadyReplaced
|
|
BadSignatureAlgorithm
|
|
AccountDoesNotExist
|
|
BadNonce
|
|
)
|
|
|
|
func (ErrorType) Error() string {
|
|
return "urn:ietf:params:acme:error"
|
|
}
|
|
|
|
// BoulderError represents internal Boulder errors
|
|
type BoulderError struct {
|
|
Type ErrorType
|
|
Detail string
|
|
SubErrors []SubBoulderError
|
|
|
|
// RetryAfter the duration a client should wait before retrying the request
|
|
// which resulted in this error.
|
|
RetryAfter time.Duration
|
|
}
|
|
|
|
// SubBoulderError represents sub-errors specific to an identifier that are
|
|
// related to a top-level internal Boulder error.
|
|
type SubBoulderError struct {
|
|
*BoulderError
|
|
Identifier identifier.ACMEIdentifier
|
|
}
|
|
|
|
// Error implements the error interface, returning a string representation of
|
|
// this error.
|
|
func (be *BoulderError) Error() string {
|
|
return be.Detail
|
|
}
|
|
|
|
// Unwrap implements the optional error-unwrapping interface. It returns the
|
|
// underlying type, all of when themselves implement the error interface, so
|
|
// that `if errors.Is(someError, berrors.Malformed)` works.
|
|
func (be *BoulderError) Unwrap() error {
|
|
return be.Type
|
|
}
|
|
|
|
// GRPCStatus implements the interface implicitly defined by gRPC's
|
|
// status.FromError, which uses this function to detect if the error produced
|
|
// by the gRPC server implementation code is a gRPC status.Status. Implementing
|
|
// this means that BoulderErrors serialized in gRPC response metadata can be
|
|
// accompanied by a gRPC status other than "UNKNOWN".
|
|
func (be *BoulderError) GRPCStatus() *status.Status {
|
|
var c codes.Code
|
|
switch be.Type {
|
|
case InternalServer:
|
|
c = codes.Internal
|
|
case Malformed:
|
|
c = codes.InvalidArgument
|
|
case Unauthorized:
|
|
c = codes.PermissionDenied
|
|
case NotFound:
|
|
c = codes.NotFound
|
|
case RateLimit:
|
|
c = codes.Unknown
|
|
case RejectedIdentifier:
|
|
c = codes.InvalidArgument
|
|
case InvalidEmail:
|
|
c = codes.InvalidArgument
|
|
case ConnectionFailure:
|
|
c = codes.Unavailable
|
|
case CAA:
|
|
c = codes.FailedPrecondition
|
|
case MissingSCTs:
|
|
c = codes.Internal
|
|
case Duplicate:
|
|
c = codes.AlreadyExists
|
|
case OrderNotReady:
|
|
c = codes.FailedPrecondition
|
|
case DNS:
|
|
c = codes.Unknown
|
|
case BadPublicKey:
|
|
c = codes.InvalidArgument
|
|
case BadCSR:
|
|
c = codes.InvalidArgument
|
|
case AlreadyRevoked:
|
|
c = codes.AlreadyExists
|
|
case BadRevocationReason:
|
|
c = codes.InvalidArgument
|
|
case UnsupportedContact:
|
|
c = codes.InvalidArgument
|
|
default:
|
|
c = codes.Unknown
|
|
}
|
|
return status.New(c, be.Error())
|
|
}
|
|
|
|
// WithSubErrors returns a new BoulderError instance created by adding the
|
|
// provided subErrs to the existing BoulderError.
|
|
func (be *BoulderError) WithSubErrors(subErrs []SubBoulderError) *BoulderError {
|
|
return &BoulderError{
|
|
Type: be.Type,
|
|
Detail: be.Detail,
|
|
SubErrors: append(be.SubErrors, subErrs...),
|
|
RetryAfter: be.RetryAfter,
|
|
}
|
|
}
|
|
|
|
// New is a convenience function for creating a new BoulderError.
|
|
func New(errType ErrorType, msg string) error {
|
|
return &BoulderError{
|
|
Type: errType,
|
|
Detail: msg,
|
|
}
|
|
}
|
|
|
|
// newf is a convenience function for creating a new BoulderError with a
|
|
// formatted message.
|
|
func newf(errType ErrorType, msg string, args ...any) error {
|
|
return &BoulderError{
|
|
Type: errType,
|
|
Detail: fmt.Sprintf(msg, args...),
|
|
}
|
|
}
|
|
|
|
func InternalServerError(msg string, args ...any) error {
|
|
return newf(InternalServer, msg, args...)
|
|
}
|
|
|
|
func MalformedError(msg string, args ...any) error {
|
|
return newf(Malformed, msg, args...)
|
|
}
|
|
|
|
func UnauthorizedError(msg string, args ...any) error {
|
|
return newf(Unauthorized, msg, args...)
|
|
}
|
|
|
|
func NotFoundError(msg string, args ...any) error {
|
|
return newf(NotFound, msg, args...)
|
|
}
|
|
|
|
func RateLimitError(retryAfter time.Duration, msg string, args ...any) error {
|
|
return &BoulderError{
|
|
Type: RateLimit,
|
|
Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/", args...),
|
|
RetryAfter: retryAfter,
|
|
}
|
|
}
|
|
|
|
func RegistrationsPerIPAddressError(retryAfter time.Duration, msg string, args ...any) error {
|
|
return &BoulderError{
|
|
Type: RateLimit,
|
|
Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#new-registrations-per-ip-address", args...),
|
|
RetryAfter: retryAfter,
|
|
}
|
|
}
|
|
|
|
func RegistrationsPerIPv6RangeError(retryAfter time.Duration, msg string, args ...any) error {
|
|
return &BoulderError{
|
|
Type: RateLimit,
|
|
Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#new-registrations-per-ipv6-range", args...),
|
|
RetryAfter: retryAfter,
|
|
}
|
|
}
|
|
|
|
func NewOrdersPerAccountError(retryAfter time.Duration, msg string, args ...any) error {
|
|
return &BoulderError{
|
|
Type: RateLimit,
|
|
Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#new-orders-per-account", args...),
|
|
RetryAfter: retryAfter,
|
|
}
|
|
}
|
|
|
|
func CertificatesPerDomainError(retryAfter time.Duration, msg string, args ...any) error {
|
|
return &BoulderError{
|
|
Type: RateLimit,
|
|
Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-registered-domain", args...),
|
|
RetryAfter: retryAfter,
|
|
}
|
|
}
|
|
|
|
func CertificatesPerFQDNSetError(retryAfter time.Duration, msg string, args ...any) error {
|
|
return &BoulderError{
|
|
Type: RateLimit,
|
|
Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-exact-set-of-hostnames", args...),
|
|
RetryAfter: retryAfter,
|
|
}
|
|
}
|
|
|
|
func FailedAuthorizationsPerDomainPerAccountError(retryAfter time.Duration, msg string, args ...any) error {
|
|
return &BoulderError{
|
|
Type: RateLimit,
|
|
Detail: fmt.Sprintf(msg+": see https://letsencrypt.org/docs/rate-limits/#authorization-failures-per-hostname-per-account", args...),
|
|
RetryAfter: retryAfter,
|
|
}
|
|
}
|
|
|
|
func RejectedIdentifierError(msg string, args ...any) error {
|
|
return newf(RejectedIdentifier, msg, args...)
|
|
}
|
|
|
|
func InvalidEmailError(msg string, args ...any) error {
|
|
return newf(InvalidEmail, msg, args...)
|
|
}
|
|
|
|
func UnsupportedContactError(msg string, args ...any) error {
|
|
return newf(UnsupportedContact, msg, args...)
|
|
}
|
|
|
|
func ConnectionFailureError(msg string, args ...any) error {
|
|
return newf(ConnectionFailure, msg, args...)
|
|
}
|
|
|
|
func CAAError(msg string, args ...any) error {
|
|
return newf(CAA, msg, args...)
|
|
}
|
|
|
|
func MissingSCTsError(msg string, args ...any) error {
|
|
return newf(MissingSCTs, msg, args...)
|
|
}
|
|
|
|
func DuplicateError(msg string, args ...any) error {
|
|
return newf(Duplicate, msg, args...)
|
|
}
|
|
|
|
func OrderNotReadyError(msg string, args ...any) error {
|
|
return newf(OrderNotReady, msg, args...)
|
|
}
|
|
|
|
func DNSError(msg string, args ...any) error {
|
|
return newf(DNS, msg, args...)
|
|
}
|
|
|
|
func BadPublicKeyError(msg string, args ...any) error {
|
|
return newf(BadPublicKey, msg, args...)
|
|
}
|
|
|
|
func BadCSRError(msg string, args ...any) error {
|
|
return newf(BadCSR, msg, args...)
|
|
}
|
|
|
|
func AlreadyReplacedError(msg string, args ...any) error {
|
|
return newf(AlreadyReplaced, msg, args...)
|
|
}
|
|
|
|
func AlreadyRevokedError(msg string, args ...any) error {
|
|
return newf(AlreadyRevoked, msg, args...)
|
|
}
|
|
|
|
func BadRevocationReasonError(reason int64) error {
|
|
return newf(BadRevocationReason, "disallowed revocation reason: %d", reason)
|
|
}
|
|
|
|
func UnknownSerialError() error {
|
|
return newf(UnknownSerial, "unknown serial")
|
|
}
|
|
|
|
func InvalidProfileError(msg string, args ...any) error {
|
|
return newf(InvalidProfile, msg, args...)
|
|
}
|
|
|
|
func BadSignatureAlgorithmError(msg string, args ...any) error {
|
|
return newf(BadSignatureAlgorithm, msg, args...)
|
|
}
|
|
|
|
func AccountDoesNotExistError(msg string, args ...any) error {
|
|
return newf(AccountDoesNotExist, msg, args...)
|
|
}
|
|
|
|
func BadNonceError(msg string, args ...any) error {
|
|
return newf(BadNonce, msg, args...)
|
|
}
|