This commit is contained in:
Fang-Pen Lin
2025-11-04 16:05:54 -08:00
parent 7faac62c8a
commit 4791b16b00
2 changed files with 28 additions and 20 deletions

View File

@@ -6,6 +6,10 @@ import { AcmeConnectionError, AcmeDnsFailureError, AcmeIncorrectResponseError }
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";
type FetchError = Error & {
code?: string;
};
type TPkiAcmeChallengeServiceFactoryDep = { type TPkiAcmeChallengeServiceFactoryDep = {
acmeChallengeDAL: Pick< acmeChallengeDAL: Pick<
TPkiAcmeChallengeDALFactory, TPkiAcmeChallengeDALFactory,
@@ -69,27 +73,33 @@ export const pkiAcmeChallengeServiceFactory = ({
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 (exp) {
// 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.markAsInvalidCascadeById(challengeId, tx); await acmeChallengeDAL.markAsInvalidCascadeById(challengeId, tx);
// Properly type and inspect the error // Properly type and inspect the error
if (error instanceof TypeError && error.message.includes("fetch failed")) { if (exp instanceof TypeError && exp.message.includes("fetch failed")) {
const cause = error.cause; const { cause } = exp;
const errors = cause instanceof AggregateError ? cause.errors : cause instanceof Error ? [cause] : []; let errors: Error[] = [];
if (cause instanceof AggregateError) {
errors = cause.errors;
} else if (cause instanceof Error) {
errors = [cause];
}
for (const err of errors) { for (const err of errors) {
// TODO: handle multiple errors, return a compound error instead of just the first error // TODO: handle multiple errors, return a compound error instead of just the first error
if (err?.code === "ECONNREFUSED" || err?.message?.includes("ECONNREFUSED")) { const fetchError = err as FetchError;
if (fetchError.code === "ECONNREFUSED" || fetchError.message.includes("ECONNREFUSED")) {
return new AcmeConnectionError({ message: "Connection refused" }); return new AcmeConnectionError({ message: "Connection refused" });
} else if (err?.code === "ENOTFOUND" || err?.message?.includes("ENOTFOUND")) { } else if (fetchError.code === "ENOTFOUND" || fetchError.message.includes("ENOTFOUND")) {
return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" }); return new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
} }
} }
} else if (error instanceof Error) { } else if (exp instanceof Error) {
logger.error(error, "Error validating ACME challenge response"); logger.error(exp, "Error validating ACME challenge response");
} else { } else {
logger.error(error, "Unknown error validating ACME challenge response"); logger.error(exp, "Unknown error validating ACME challenge response");
} }
return error; return exp;
} }
}); });
if (error) { if (error) {

View File

@@ -129,7 +129,7 @@ export const pkiAcmeServiceFactory = ({
}; };
const validateJwsPayload = async < const validateJwsPayload = async <
TSchema extends z.ZodSchema<any> | undefined = undefined, TSchema extends z.ZodSchema<unknown> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string T = TSchema extends z.ZodSchema<infer R> ? R : string
>({ >({
url, url,
@@ -149,7 +149,8 @@ export const pkiAcmeServiceFactory = ({
throw new AcmeMalformedError({ detail: "Protected header is required" }); throw new AcmeMalformedError({ detail: "Protected header is required" });
} }
const jwk = await getJWK(protectedHeader); const jwk = await getJWK(protectedHeader);
return await importJWK(jwk, protectedHeader.alg); const key = await importJWK(jwk, protectedHeader.alg);
return key;
}); });
} catch (error) { } catch (error) {
if (error instanceof AcmeError) { if (error instanceof AcmeError) {
@@ -186,7 +187,7 @@ export const pkiAcmeServiceFactory = ({
const payload = schema ? schema.parse(JSON.parse(textPayload)) : textPayload; const payload = schema ? schema.parse(JSON.parse(textPayload)) : textPayload;
return { return {
protectedHeader, protectedHeader,
payload payload: payload as T
}; };
} catch (error) { } catch (error) {
if (error instanceof AcmeError) { if (error instanceof AcmeError) {
@@ -200,14 +201,14 @@ export const pkiAcmeServiceFactory = ({
} }
}; };
const validateNewAccountJwsPayload = async ({ const validateNewAccountJwsPayload = ({
url, url,
rawJwsPayload rawJwsPayload
}: { }: {
url: URL; url: URL;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
}): Promise<TJwsPayload<TCreateAcmeAccountPayload>> => { }): Promise<TJwsPayload<TCreateAcmeAccountPayload>> => {
return await validateJwsPayload({ return validateJwsPayload({
url, url,
rawJwsPayload, rawJwsPayload,
getJWK: async (protectedHeader) => { getJWK: async (protectedHeader) => {
@@ -355,11 +356,8 @@ export const pkiAcmeServiceFactory = ({
const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig!.encryptedEabSecret! }); const eabSecret = await kmsDecryptor({ cipherTextBlob: profile.acmeConfig!.encryptedEabSecret! });
const { eabPayload, eabProtectedHeader } = await (async () => { const { eabPayload, eabProtectedHeader } = await (async () => {
try { try {
const { payload: eabPayload, protectedHeader: eabProtectedHeader } = await flattenedVerify( const result = await flattenedVerify(externalAccountBinding, eabSecret);
externalAccountBinding, return { eabPayload: result.payload, eabProtectedHeader: result.protectedHeader };
eabSecret
);
return { eabPayload, eabProtectedHeader };
} catch (error) { } catch (error) {
if (error instanceof errors.JWSSignatureVerificationFailed) { if (error instanceof errors.JWSSignatureVerificationFailed) {
throw new AcmeMalformedError({ detail: "Invalid external account binding JWS signature" }); throw new AcmeMalformedError({ detail: "Invalid external account binding JWS signature" });