More challenge logic

This commit is contained in:
Fang-Pen Lin
2025-10-31 13:23:30 -07:00
parent bc00710df3
commit 0613a49795
2 changed files with 36 additions and 23 deletions

View File

@@ -2,6 +2,7 @@ import { Knex } from "knex";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal";
import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal";
import { AcmeAuthStatus, AcmeChallengeStatus, AcmeChallengeType } from "./pki-acme-schemas";
@@ -18,15 +19,16 @@ export const pkiAcmeChallengeServiceFactory = ({
}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => {
const appCfg = getConfig();
const validateChallengeResponse = async (challengeId: string): Promise<void> => {
const validateChallengeResponse = async (challengeId: string, tx?: Knex): Promise<void> => {
return await acmeChallengeDAL.transaction(async (tx: Knex) => {
logger.info({ challengeId }, "Validating ACME challenge response");
const challenge = await acmeChallengeDAL.findByIdForChallengeValidation(challengeId, tx);
if (!challenge) {
throw new NotFoundError({ message: "ACME challenge not found" });
}
if (challenge.status !== AcmeChallengeStatus.Processing) {
if (challenge.status !== AcmeChallengeStatus.Pending) {
throw new BadRequestError({
message: `ACME challenge is ${challenge.status} instead of ${AcmeChallengeStatus.Processing}`
message: `ACME challenge is ${challenge.status} instead of ${AcmeChallengeStatus.Pending}`
});
}
if (challenge.auth.expiresAt < new Date()) {
@@ -46,28 +48,36 @@ export const pkiAcmeChallengeServiceFactory = ({
const actualBaseUrl = appCfg.isAcmeDevelopmentMode
? `${baseUrl}:${appCfg.ACME_DEVELOPMENT_HTTP01_CHALLENGE_PORT}`
: baseUrl;
const challengeUrl = new URL(`/.well-known/acme-challenge/${challenge.auth.token}`, actualBaseUrl);
// 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
// challenge validation at the same time, it should be fine.
// TODO: bound it with timeout of the fetch request
const challengeResponse = await fetch(challengeUrl);
if (challengeResponse.status !== 200) {
throw new BadRequestError({ message: "ACME challenge response is not 200" });
try {
// 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
// challenge validation at the same time, it should be fine.
// TODO: bound it with timeout of the fetch request
const challengeResponse = await fetch(challengeUrl);
if (challengeResponse.status !== 200) {
throw new BadRequestError({ message: "ACME challenge response is not 200" });
}
const challengeResponseBody = await challengeResponse.text();
const thumbprint = Buffer.from(challenge.auth.account.publicKeyThumbprint, "utf-8").toString("base64url");
const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`;
if (challengeResponseBody !== expectedChallengeResponseBody) {
throw new BadRequestError({ message: "ACME challenge response is not correct" });
}
await acmeChallengeDAL.updateById(
challengeId,
{ status: AcmeChallengeStatus.Valid, validatedAt: new Date() },
tx
);
await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Valid }, tx);
await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Valid }, tx);
} 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
await acmeChallengeDAL.updateById(challengeId, { status: AcmeChallengeStatus.Invalid }, tx);
await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Invalid }, tx);
throw error;
}
const challengeResponseBody = await challengeResponse.text();
const thumbprint = Buffer.from(challenge.auth.account.publicKeyThumbprint, "utf-8").toString("base64url");
const expectedChallengeResponseBody = `${challenge.auth.token}.${thumbprint}`;
if (challengeResponseBody !== expectedChallengeResponseBody) {
throw new BadRequestError({ message: "ACME challenge response is not correct" });
}
await acmeChallengeDAL.updateById(
challengeId,
{ status: AcmeChallengeStatus.Valid, validatedAt: new Date() },
tx
);
await acmeAuthDAL.updateById(challenge.auth.account.id, { status: AcmeAuthStatus.Valid }, tx);
});
};

View File

@@ -631,6 +631,9 @@ export const pkiAcmeServiceFactory = ({
if (!challenge.auth.token) {
throw new AcmeServerInternalError({ message: "ACME challenge token is required" });
}
if (challenge.type !== AcmeChallengeType.HTTP_01) {
throw new BadRequestError({ message: "Only HTTP-01 challenges are supported for now" });
}
const updatedChallenge = await acmeChallengeDAL.updateById(
challengeId,
{ status: AcmeChallengeStatus.Pending },