From 7b15dee2e6002b69d6e909c9a4e66433c44bfb18 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 19:29:49 -0700 Subject: [PATCH] Add challenge --- backend/bdd/features/pki/acme/auth.feature | 1 + backend/src/ee/routes/v1/pki-acme-router.ts | 26 +++++++------------ .../pki-acme/pki-acme-challenge-dal.ts | 13 ++++++++++ .../ee/services/pki-acme/pki-acme-schemas.ts | 10 +++++-- .../ee/services/pki-acme/pki-acme-service.ts | 17 ++++++++++-- 5 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index e81d3dd61..7252d868f 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -18,6 +18,7 @@ Feature: Order Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then the value order.authorizations[0].uri with jq . should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) Then the value order.authorizations[0].body with jq .status should be equal to "pending" + Then the value order.authorizations[0].body with jq .challenge should be equal to "pending" Then the value order.authorizations[0].body with jq .identifier should be equal to json """ { diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 95ef93d18..6935150ac 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -186,8 +186,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: DeactivateAcmeAccountResponseSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { payload, profileId, accountId } = await validateExistingAccount({ req, @@ -223,8 +221,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 201: AcmeOrderResourceSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await validateExistingAccount({ req, @@ -262,8 +258,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: AcmeOrderResourceSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await validateExistingAccount({ req, @@ -301,8 +295,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: AcmeOrderResourceSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await validateExistingAccount({ req, @@ -378,8 +370,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: z.string() } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await validateExistingAccount({ req, @@ -413,8 +403,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: GetAcmeAuthorizationResponseSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await validateExistingAccount({ req }); if (payload !== "") { @@ -451,10 +439,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { 200: RespondToAcmeChallengeResponseSchema } }, - // TODO: replace with verify ACME signature here instead - // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const challenge = await server.services.pkiAcme.respondToAcmeChallenge(req.params.profileId, req.params.authzId); + handler: async (req, res) => { + const { profileId, accountId, payload } = await validateExistingAccount({ req }); + if (payload !== "") { + throw new AcmeMalformedError({ detail: "Payload should be empty" }); + } + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.respondToAcmeChallenge({ profileId, authzId: req.params.authzId }) + ); return challenge; } }); diff --git a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts new file mode 100644 index 000000000..77a585efd --- /dev/null +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -0,0 +1,13 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TPkiAcmeChallengeDALFactory = ReturnType; + +export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { + const pkiAcmeChallengeOrm = ormify(db, TableName.PkiAcmeChallenge); + + return { + ...pkiAcmeChallengeOrm + }; +}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index ad40ea17b..e09aa81c8 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -21,6 +21,12 @@ export enum AcmeAuthStatus { Revoked = "revoked" } +export enum AcmeChallengeType { + HTTP_01 = "http-01", + DNS_01 = "dns-01", + TLS_ALPN_01 = "tls-alpn-01" +} + export const ProtectedHeaderSchema = z .object({ alg: z.string(), @@ -136,7 +142,7 @@ export const GetAcmeAuthorizationResponseSchema = z.object({ }), challenges: z.array( z.object({ - type: z.string(), + type: z.enum(Object.values(AcmeChallengeType) as [string, ...string[]]), url: z.string(), status: z.string(), token: z.string(), @@ -146,7 +152,7 @@ export const GetAcmeAuthorizationResponseSchema = z.object({ }); export const RespondToAcmeChallengeResponseSchema = z.object({ - type: z.string(), + type: z.enum(Object.values(AcmeChallengeType) as [string, ...string[]]), url: z.string(), status: z.string(), token: z.string(), diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 3508d8c93..679457814 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -15,6 +15,7 @@ import { errors, flattenedVerify, FlattenedVerifyResult, importJWK, JWSHeaderPar import { z, ZodError } from "zod"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; +import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; import { AcmeAccountDoesNotExistError, AcmeBadPublicKeyError, @@ -28,6 +29,7 @@ import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { AcmeAuthStatus, + AcmeChallengeType, AcmeIdentifierType, AcmeOrderStatus, CreateAcmeAccountBodySchema, @@ -58,6 +60,7 @@ type TPkiAcmeServiceFactoryDep = { acmeOrderDAL: Pick; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; + acmeChallengeDAL: Pick; }; export const pkiAcmeServiceFactory = ({ @@ -65,7 +68,8 @@ export const pkiAcmeServiceFactory = ({ acmeAccountDAL, acmeOrderDAL, acmeAuthDAL, - acmeOrderAuthDAL + acmeOrderAuthDAL, + acmeChallengeDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => { const validateAcmeProfile = async (profileId: string): Promise => { const profile = await certificateProfileDAL.findById(profileId); @@ -376,7 +380,7 @@ export const pkiAcmeServiceFactory = ({ payload.identifiers.map(async (identifier) => { if (identifier.type === AcmeIdentifierType.DNS) { // TODO: reuse existing authorizations for this identifier if they exist - return await acmeAuthDAL.create( + const auth = await acmeAuthDAL.create( { accountId: account.id, status: AcmeAuthStatus.Pending, @@ -391,6 +395,15 @@ export const pkiAcmeServiceFactory = ({ }, tx ); + // TODO: support other challenge types here. Currently only HTTP-01 is supported. + await acmeChallengeDAL.create( + { + authId: auth.id, + type: AcmeChallengeType.HTTP_01 + }, + tx + ); + return auth; } else { throw new AcmeUnsupportedIdentifierError({ detail: "Only DNS identifiers are supported" }); }