mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 22:27:48 +00:00
More error handling
This commit is contained in:
@@ -5,7 +5,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal";
|
import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal";
|
||||||
import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal";
|
import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal";
|
||||||
import { AcmeIncorrectResponseError } from "./pki-acme-errors";
|
import { AcmeConnectionError, AcmeDnsFailureError, AcmeIncorrectResponseError } from "./pki-acme-errors";
|
||||||
import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas";
|
import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas";
|
||||||
import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types";
|
import { TPkiAcmeChallengeServiceFactory } from "./pki-acme-types";
|
||||||
import { TPkiAcmeChallenges } from "@app/db/schemas";
|
import { TPkiAcmeChallenges } from "@app/db/schemas";
|
||||||
@@ -25,7 +25,7 @@ export const pkiAcmeChallengeServiceFactory = ({
|
|||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const validateChallengeResponse = async (challengeId: string): Promise<void> => {
|
const validateChallengeResponse = async (challengeId: string): Promise<void> => {
|
||||||
return await acmeChallengeDAL.transaction(async (tx) => {
|
const error = await acmeChallengeDAL.transaction(async (tx) => {
|
||||||
logger.info({ challengeId }, "Validating ACME challenge response");
|
logger.info({ challengeId }, "Validating ACME challenge response");
|
||||||
const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx);
|
const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx);
|
||||||
if (!challenge) {
|
if (!challenge) {
|
||||||
@@ -54,6 +54,7 @@ export const pkiAcmeChallengeServiceFactory = ({
|
|||||||
? `${baseUrl}:${appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_PORT}`
|
? `${baseUrl}:${appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_PORT}`
|
||||||
: baseUrl;
|
: baseUrl;
|
||||||
const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, actualBaseUrl);
|
const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, actualBaseUrl);
|
||||||
|
logger.info({ challengeUrl }, "Performing ACME HTTP-01 challenge validation");
|
||||||
try {
|
try {
|
||||||
// Notice: well, we are in a transaction, ideally we should not hold transaction and perform
|
// Notice: well, we are in a transaction, ideally we should not hold transaction and perform
|
||||||
// a long running operation for long time. But assuming we are not performing a tons of
|
// a long running operation for long time. But assuming we are not performing a tons of
|
||||||
@@ -66,17 +67,34 @@ export const pkiAcmeChallengeServiceFactory = ({
|
|||||||
const challengeResponseBody = await challengeResponse.text();
|
const challengeResponseBody = await challengeResponse.text();
|
||||||
const thumbprint = Buffer.from(challenge.auth.account.publicKeyThumbprint, "utf-8").toString("base64url");
|
const thumbprint = Buffer.from(challenge.auth.account.publicKeyThumbprint, "utf-8").toString("base64url");
|
||||||
const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`;
|
const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`;
|
||||||
if (challengeResponseBody !== expectedChallengeResponseBody) {
|
if (challengeResponseBody.trimEnd() !== expectedChallengeResponseBody) {
|
||||||
throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" });
|
throw new AcmeIncorrectResponseError({ message: "ACME challenge response is not correct" });
|
||||||
}
|
}
|
||||||
await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx);
|
await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error, "Error validating ACME challenge response");
|
|
||||||
// TODO: we should retry the challenge validation a few times, but let's keep it simple for now
|
// TODO: we should retry the challenge validation a few times, but let's keep it simple for now
|
||||||
await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx);
|
await acmeChallengeDAL.markAsValidCascadeById(challengeId, tx);
|
||||||
throw error;
|
// Properly type and inspect the error
|
||||||
|
if (error instanceof TypeError && error.message.includes("fetch failed")) {
|
||||||
|
const cause = error.cause as AggregateError;
|
||||||
|
if (cause?.errors?.[0]?.code === "ECONNREFUSED") {
|
||||||
|
logger.error(error, "Connection refused.");
|
||||||
|
return new AcmeConnectionError({ message: "Connection refused." });
|
||||||
|
} else if (cause?.errors?.[0]?.code === "ENOTFOUND") {
|
||||||
|
logger.error(error, "Hostname could not be resolved (DNS failure).");
|
||||||
|
return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)." });
|
||||||
|
}
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
logger.error(error, "Error validating ACME challenge response");
|
||||||
|
} else {
|
||||||
|
logger.error(error, "Unknown error validating ACME challenge response");
|
||||||
|
}
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { validateChallengeResponse };
|
return { validateChallengeResponse };
|
||||||
|
|||||||
@@ -3,15 +3,43 @@
|
|||||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-6.2
|
* https://datatracker.ietf.org/doc/html/rfc8555#section-6.2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// RFC 8555 Section 6.7 - Error Types
|
||||||
|
export enum AcmeErrorType {
|
||||||
|
AccountDoesNotExist = "accountDoesNotExist",
|
||||||
|
AlreadyRevoked = "alreadyRevoked",
|
||||||
|
BadCsr = "badCSR",
|
||||||
|
BadNonce = "badNonce",
|
||||||
|
BadPublicKey = "badPublicKey",
|
||||||
|
BadRevocationReason = "badRevocationReason",
|
||||||
|
BadSignatureAlgorithm = "badSignatureAlgorithm",
|
||||||
|
CAA = "CAA",
|
||||||
|
Compound = "compound",
|
||||||
|
Connection = "connection",
|
||||||
|
DNS = "DNS",
|
||||||
|
ExternalAccountRequired = "externalAccountRequired",
|
||||||
|
IncorrectResponse = "incorrectResponse",
|
||||||
|
IncorrectContact = "incorrectContact",
|
||||||
|
Malformed = "malformed",
|
||||||
|
OrderNotReady = "orderNotReady",
|
||||||
|
RateLimited = "rateLimited",
|
||||||
|
RejectedIdentifier = "rejectedIdentifier",
|
||||||
|
ServerInternal = "serverInternal",
|
||||||
|
TLS = "tls",
|
||||||
|
Unauthorized = "unauthorized",
|
||||||
|
UnsupportedContact = "unsupportedContact",
|
||||||
|
UnsupportedIdentifier = "unsupportedIdentifier",
|
||||||
|
UserActionRequired = "userActionRequired"
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAcmeError {
|
export interface IAcmeError {
|
||||||
type: string;
|
type: AcmeErrorType;
|
||||||
detail: string;
|
detail: string;
|
||||||
status: number;
|
status: number;
|
||||||
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
|
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AcmeError extends Error implements IAcmeError {
|
export class AcmeError extends Error implements IAcmeError {
|
||||||
type: string;
|
type: AcmeErrorType;
|
||||||
|
|
||||||
detail: string;
|
detail: string;
|
||||||
|
|
||||||
@@ -29,7 +57,7 @@ export class AcmeError extends Error implements IAcmeError {
|
|||||||
error,
|
error,
|
||||||
message
|
message
|
||||||
}: {
|
}: {
|
||||||
type: string;
|
type: AcmeErrorType;
|
||||||
detail: string;
|
detail: string;
|
||||||
status: number;
|
status: number;
|
||||||
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
|
subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>;
|
||||||
@@ -69,7 +97,7 @@ export class AcmeMalformedError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "malformed",
|
type: AcmeErrorType.Malformed,
|
||||||
detail,
|
detail,
|
||||||
status: 400,
|
status: 400,
|
||||||
error,
|
error,
|
||||||
@@ -93,7 +121,7 @@ export class AcmeUnauthorizedError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "unauthorized",
|
type: AcmeErrorType.Unauthorized,
|
||||||
detail,
|
detail,
|
||||||
status: 403,
|
status: 403,
|
||||||
error,
|
error,
|
||||||
@@ -118,7 +146,7 @@ export class AcmeAccountDoesNotExistError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "accountDoesNotExist",
|
type: AcmeErrorType.AccountDoesNotExist,
|
||||||
detail,
|
detail,
|
||||||
status: 400,
|
status: 400,
|
||||||
error,
|
error,
|
||||||
@@ -142,7 +170,7 @@ export class AcmeBadNonceError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "badNonce",
|
type: AcmeErrorType.BadNonce,
|
||||||
detail,
|
detail,
|
||||||
status: 400,
|
status: 400,
|
||||||
error,
|
error,
|
||||||
@@ -153,11 +181,11 @@ export class AcmeBadNonceError extends AcmeError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* badSignature - The JWS signature is invalid (RFC 8555 Section 6.7.5)
|
* badSignatureAlgorithm - The signature algorithm is invalid (RFC 8555 Section 6.7.5)
|
||||||
*/
|
*/
|
||||||
export class AcmeBadSignatureError extends AcmeError {
|
export class AcmeBadSignatureAlgorithmError extends AcmeError {
|
||||||
constructor({
|
constructor({
|
||||||
detail = "The JWS signature is invalid",
|
detail = "The signature algorithm is invalid",
|
||||||
error,
|
error,
|
||||||
message
|
message
|
||||||
}: {
|
}: {
|
||||||
@@ -166,13 +194,13 @@ export class AcmeBadSignatureError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "badSignature",
|
type: AcmeErrorType.BadSignatureAlgorithm,
|
||||||
detail,
|
detail,
|
||||||
status: 401,
|
status: 401,
|
||||||
error,
|
error,
|
||||||
message
|
message
|
||||||
});
|
});
|
||||||
this.name = "AcmeBadSignatureError";
|
this.name = "AcmeBadSignatureAlgorithmError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +218,7 @@ export class AcmeBadPublicKeyError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "badPublicKey",
|
type: AcmeErrorType.BadPublicKey,
|
||||||
detail,
|
detail,
|
||||||
status: 400,
|
status: 400,
|
||||||
error,
|
error,
|
||||||
@@ -214,7 +242,7 @@ export class AcmeBadCsrError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "badCSR",
|
type: AcmeErrorType.BadCsr,
|
||||||
detail,
|
detail,
|
||||||
status: 400,
|
status: 400,
|
||||||
error,
|
error,
|
||||||
@@ -239,7 +267,7 @@ export class AcmeBadRevocationReasonError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "badRevocationReason",
|
type: AcmeErrorType.BadRevocationReason,
|
||||||
detail,
|
detail,
|
||||||
status: 400,
|
status: 400,
|
||||||
error,
|
error,
|
||||||
@@ -263,7 +291,7 @@ export class AcmeRateLimitedError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "rateLimited",
|
type: AcmeErrorType.RateLimited,
|
||||||
detail,
|
detail,
|
||||||
status: 429,
|
status: 429,
|
||||||
error,
|
error,
|
||||||
@@ -290,7 +318,7 @@ export class AcmeRejectedIdentifierError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "rejectedIdentifier",
|
type: AcmeErrorType.RejectedIdentifier,
|
||||||
detail,
|
detail,
|
||||||
status: 400,
|
status: 400,
|
||||||
subproblems,
|
subproblems,
|
||||||
@@ -315,7 +343,7 @@ export class AcmeServerInternalError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "serverInternal",
|
type: AcmeErrorType.ServerInternal,
|
||||||
detail,
|
detail,
|
||||||
status: 500,
|
status: 500,
|
||||||
error,
|
error,
|
||||||
@@ -325,30 +353,6 @@ export class AcmeServerInternalError extends AcmeError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* serviceUnavailable - The service is unavailable (RFC 8555 Section 6.7.12)
|
|
||||||
*/
|
|
||||||
export class AcmeServiceUnavailableError extends AcmeError {
|
|
||||||
constructor({
|
|
||||||
detail = "The service is unavailable",
|
|
||||||
error,
|
|
||||||
message
|
|
||||||
}: {
|
|
||||||
detail?: string;
|
|
||||||
error?: unknown;
|
|
||||||
message?: string;
|
|
||||||
} = {}) {
|
|
||||||
super({
|
|
||||||
type: "serviceUnavailable",
|
|
||||||
detail,
|
|
||||||
status: 503,
|
|
||||||
error,
|
|
||||||
message
|
|
||||||
});
|
|
||||||
this.name = "AcmeServiceUnavailableError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* unsupportedContact - A contact URL is of an unsupported type (RFC 8555 Section 6.7.13)
|
* unsupportedContact - A contact URL is of an unsupported type (RFC 8555 Section 6.7.13)
|
||||||
*/
|
*/
|
||||||
@@ -363,7 +367,7 @@ export class AcmeUnsupportedContactError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "unsupportedContact",
|
type: AcmeErrorType.UnsupportedContact,
|
||||||
detail,
|
detail,
|
||||||
status: 400,
|
status: 400,
|
||||||
error,
|
error,
|
||||||
@@ -388,7 +392,7 @@ export class AcmeUnsupportedIdentifierError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "unsupportedIdentifier",
|
type: AcmeErrorType.UnsupportedIdentifier,
|
||||||
detail,
|
detail,
|
||||||
status: 400,
|
status: 400,
|
||||||
error,
|
error,
|
||||||
@@ -417,7 +421,7 @@ export class AcmeUserActionRequiredError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "userActionRequired",
|
type: AcmeErrorType.UserActionRequired,
|
||||||
detail,
|
detail,
|
||||||
status: 403,
|
status: 403,
|
||||||
error,
|
error,
|
||||||
@@ -449,7 +453,7 @@ export class AcmeIncorrectResponseError extends AcmeError {
|
|||||||
message?: string;
|
message?: string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
super({
|
super({
|
||||||
type: "incorrectResponse",
|
type: AcmeErrorType.IncorrectResponse,
|
||||||
detail,
|
detail,
|
||||||
status: 400,
|
status: 400,
|
||||||
error,
|
error,
|
||||||
@@ -458,3 +462,48 @@ export class AcmeIncorrectResponseError extends AcmeError {
|
|||||||
this.name = "AcmeIncorrectResponseError";
|
this.name = "AcmeIncorrectResponseError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* connectionError - A connection error occurred (RFC 8555 Section 6.7.17)
|
||||||
|
*/
|
||||||
|
export class AcmeConnectionError extends AcmeError {
|
||||||
|
constructor({
|
||||||
|
detail = "A connection error occurred",
|
||||||
|
error,
|
||||||
|
message
|
||||||
|
}: {
|
||||||
|
detail?: string;
|
||||||
|
error?: unknown;
|
||||||
|
message?: string;
|
||||||
|
} = {}) {
|
||||||
|
super({
|
||||||
|
type: AcmeErrorType.Connection,
|
||||||
|
detail,
|
||||||
|
status: 400,
|
||||||
|
error,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
this.name = "AcmeConnectionError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AcmeDnsFailureError extends AcmeError {
|
||||||
|
constructor({
|
||||||
|
detail = "Hostname could not be resolved (DNS failure)",
|
||||||
|
error,
|
||||||
|
message
|
||||||
|
}: {
|
||||||
|
detail?: string;
|
||||||
|
error?: unknown;
|
||||||
|
message?: string;
|
||||||
|
} = {}) {
|
||||||
|
super({
|
||||||
|
type: AcmeErrorType.DNS,
|
||||||
|
detail,
|
||||||
|
status: 400,
|
||||||
|
error,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
this.name = "AcmeDnsFailureError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const GetAcmeAuthorizationResponseSchema = z.object({
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RespondToAcmeChallengeBodySchema = z.object({});
|
export const RespondToAcmeChallengeBodySchema = z.object({}).strict();
|
||||||
|
|
||||||
export const RespondToAcmeChallengeResponseSchema = z.object({
|
export const RespondToAcmeChallengeResponseSchema = z.object({
|
||||||
type: z.enum(Object.values(AcmeChallengeType) as [string, ...string[]]),
|
type: z.enum(Object.values(AcmeChallengeType) as [string, ...string[]]),
|
||||||
|
|||||||
Reference in New Issue
Block a user