From ea96cc87bbefc78ac6b772351a9a797ce81aac57 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 21:56:00 -0700 Subject: [PATCH] Add link headers --- backend/src/ee/routes/v1/pki-acme-router.ts | 13 ++- .../pki-acme/pki-acme-challenge-dal.ts | 32 ++++++- .../ee/services/pki-acme/pki-acme-service.ts | 93 +++++++++++-------- .../ee/services/pki-acme/pki-acme-types.ts | 8 +- 4 files changed, 101 insertions(+), 45 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 6935150ac..53bb81224 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -424,7 +424,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // Respond to Challenge (RFC 8555 Section 7.5.1) server.route({ method: "POST", - url: "/profiles/:profileId/authorizations/:authzId/challenges/http-01", + url: "/profiles/:profileId/authorizations/:authzId/challenges/:challengeId", config: { rateLimit: writeLimit }, @@ -433,7 +433,8 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { tags: [ApiDocsTags.PkiAcme], description: "ACME Respond to Challenge - let ACME server know challenge is ready", params: SharedParamsSchema.extend({ - authzId: z.string().uuid() + authzId: z.string().uuid(), + challengeId: z.string().uuid() }), response: { 200: RespondToAcmeChallengeResponseSchema @@ -447,9 +448,13 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { return sendAcmeResponse( res, profileId, - await server.services.pkiAcme.respondToAcmeChallenge({ profileId, authzId: req.params.authzId }) + await server.services.pkiAcme.respondToAcmeChallenge({ + profileId, + accountId, + authzId: req.params.authzId, + challengeId: req.params.challengeId + }) ); - 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 index 77a585efd..d23827a37 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-challenge-dal.ts @@ -1,13 +1,41 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { Knex } from "knex"; export type TPkiAcmeChallengeDALFactory = ReturnType; export const pkiAcmeChallengeDALFactory = (db: TDbClient) => { const pkiAcmeChallengeOrm = ormify(db, TableName.PkiAcmeChallenge); + const findByAccountAuthAndChallengeIdWithToken = async ( + accountId: string, + authId: string, + challengeId: string, + tx?: Knex + ) => { + try { + const challenge = await (tx || db)(TableName.PkiAcmeChallenge) + .join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`) + .select( + selectAllTableCols(TableName.PkiAcmeChallenge), + db.ref("token").withSchema(TableName.PkiAcmeChallenge).as("token") + ) + .where(`${TableName.PkiAcmeChallenge}.id`, challengeId) + .where(`${TableName.PkiAcmeChallenge}.authId`, authId) + .where(`${TableName.PkiAcmeAuth}.accountId`, accountId) + .first(); + if (!challenge) { + return null; + } + return challenge; + } catch (error) { + throw new DatabaseError({ error, name: "Find PKI ACME challenge by account id, auth id and challenge id" }); + } + }; return { - ...pkiAcmeChallengeOrm + ...pkiAcmeChallengeOrm, + findByAccountAuthAndChallengeIdWithToken }; }; 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 bbff4a9c4..c55a46542 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -61,7 +61,7 @@ type TPkiAcmeServiceFactoryDep = { acmeOrderDAL: Pick; acmeAuthDAL: Pick; acmeOrderAuthDAL: Pick; - acmeChallengeDAL: Pick; + acmeChallengeDAL: Pick; }; export const pkiAcmeServiceFactory = ({ @@ -300,9 +300,10 @@ export const pkiAcmeServiceFactory = ({ contact: existingAccount.emails, orders: buildUrl(profile.id, `/accounts/${existingAccount.id}/orders`) }, - headers: { - Location: buildUrl(profile.id, `/accounts/${existingAccount.id}`) - } + headers: [ + ["Location", buildUrl(profile.id, `/accounts/${existingAccount.id}`)], + ["Link", `<${buildUrl(profile.id, "/directory")}>;rel="index"`] + ] }; } @@ -321,9 +322,10 @@ export const pkiAcmeServiceFactory = ({ contact: newAccount.emails, orders: buildUrl(profile.id, `/accounts/${newAccount.id}/orders`) }, - headers: { - Location: buildUrl(profile.id, `/accounts/${newAccount.id}`) - } + headers: [ + ["Location", buildUrl(profile.id, `/accounts/${newAccount.id}`)], + ["Link", `<${buildUrl(profile.id, "/directory")}>;rel="index"`] + ] }; }; @@ -343,9 +345,10 @@ export const pkiAcmeServiceFactory = ({ body: { status: "deactivated" }, - headers: { - Location: buildUrl(profileId, `/accounts/${accountId}`) - } + headers: [ + ["Location", buildUrl(profileId, `/accounts/${accountId}`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -429,9 +432,10 @@ export const pkiAcmeServiceFactory = ({ profileId, order }), - headers: { - Location: buildUrl(profileId, `/orders/${order.id}`) - } + headers: [ + ["Location", buildUrl(profileId, `/orders/${order.id}`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -451,7 +455,10 @@ export const pkiAcmeServiceFactory = ({ return { status: 200, body: buildAcmeOrderResource({ profileId, order }), - headers: { Location: buildUrl(profileId, `/orders/${orderId}`) } + headers: [ + ["Location", buildUrl(profileId, `/orders/${orderId}`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -475,9 +482,10 @@ export const pkiAcmeServiceFactory = ({ return { status: 200, body: buildAcmeOrderResource({ profileId, order }), - headers: { - Location: buildUrl(profileId, `/orders/${orderId}`) - } + headers: [ + ["Location", buildUrl(profileId, `/orders/${orderId}`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -499,9 +507,10 @@ export const pkiAcmeServiceFactory = ({ return { status: 200, body: "FIXME-certificate-pem", - headers: { - Location: buildUrl(profileId, `/orders/${orderId}/certificate`) - } + headers: [ + ["Location", buildUrl(profileId, `/orders/${orderId}/certificate`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -519,9 +528,10 @@ export const pkiAcmeServiceFactory = ({ body: { orders: [] }, - headers: { - Location: buildUrl(profileId, `/accounts/${accountId}/orders`) - } + headers: [ + ["Location", buildUrl(profileId, `/accounts/${accountId}/orders`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; @@ -559,33 +569,42 @@ export const pkiAcmeServiceFactory = ({ }; }) }, - headers: { - Location: buildUrl(profileId, `/authorizations/${authzId}`) - } + headers: [ + ["Location", buildUrl(profileId, `/authorizations/${authzId}`)], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; const respondToAcmeChallenge = async ({ profileId, - authzId + accountId, + authzId, + challengeId }: { profileId: string; + accountId: string; authzId: string; + challengeId: string; }): Promise> => { - const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME challenge response - // Trigger verification process + const challenge = await acmeChallengeDAL.findByAccountAuthAndChallengeIdWithToken(accountId, authzId, challengeId); + if (!challenge) { + throw new NotFoundError({ message: "ACME challenge not found" }); + } + // TODO: Implement ACME challenge response return { status: 200, body: { - type: "http-01", - url: buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`), - status: "pending", - token: "FIXME-challenge-token" + type: challenge.type, + url: buildUrl(profileId, `/authorizations/${authzId}/challenges/${challengeId}`), + status: challenge.status, + token: challenge.token }, - headers: { - Location: buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`) - } + headers: [ + ["Location", buildUrl(profileId, `/authorizations/${authzId}/challenges/http-01`)], + ["Link", `<${buildUrl(profileId, `/authorizations/${authzId}`)}>;rel="up"`], + ["Link", `<${buildUrl(profileId, "/directory")}>;rel="index"`] + ] }; }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-types.ts b/backend/src/ee/services/pki-acme/pki-acme-types.ts index 082078f2a..dbd1ad25f 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -44,7 +44,7 @@ export type TAuthenciatedJwsPayload = TJwsPayload & { }; export type TAcmeResponse = { status: number; - headers: Record; + headers: [string, string][]; body: TPayload; }; @@ -164,9 +164,13 @@ export type TPkiAcmeServiceFactory = { }) => Promise>; respondToAcmeChallenge: ({ profileId, - authzId + accountId, + authzId, + challengeId }: { profileId: string; + accountId: string; authzId: string; + challengeId: string; }) => Promise>; };