From 31a70bc781985228280eb721c642aefd28abf8b7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 16:46:52 -0700 Subject: [PATCH] Add url check --- backend/src/ee/routes/v1/pki-acme-router.ts | 12 +++- .../ee/services/pki-acme/pki-acme-service.ts | 66 ++++++++++++------- .../ee/services/pki-acme/pki-acme-types.ts | 26 ++++++-- 3 files changed, 73 insertions(+), 31 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 4794a125a..a35e54cc1 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -121,7 +121,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }, handler: async (req, res) => { - const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload(req.body); + const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload({ + url: req.url, + rawJwsPayload: req.body + }); const { alg, jwk } = protectedHeader; return sendAcmeResponse( res, @@ -161,6 +164,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { payload, profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body, schema: DeactivateAcmeAccountBodySchema, @@ -202,6 +206,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body, schema: CreateAcmeOrderBodySchema @@ -243,6 +248,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body }); @@ -283,6 +289,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body, schema: FinalizeAcmeOrderBodySchema @@ -324,6 +331,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body, schema: ListAcmeOrdersPayloadSchema, @@ -365,6 +373,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body }); @@ -401,6 +410,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + url: req.url, profileId: req.params.profileId, rawJwsPayload: req.body }); 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 fe823dceb..97e08d757 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -19,6 +19,7 @@ import { AcmeBadPublicKeyError, AcmeMalformedError, AcmeServerInternalError, + AcmeUnauthorizedError, AcmeUnsupportedIdentifierError } from "./pki-acme-errors"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; @@ -92,11 +93,17 @@ export const pkiAcmeServiceFactory = ({ const validateJwsPayload = async < TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string - >( - rawJwsPayload: TRawJwsPayload, - getJWK: (protectedHeader: JWSHeaderParameters) => Promise, - schema?: TSchema - ): Promise> => { + >({ + url, + rawJwsPayload, + getJWK, + schema + }: { + url: string; + rawJwsPayload: TRawJwsPayload; + getJWK: (protectedHeader: JWSHeaderParameters) => Promise; + schema?: TSchema; + }): Promise> => { let result: FlattenedVerifyResult; try { result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => { @@ -119,6 +126,9 @@ export const pkiAcmeServiceFactory = ({ const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; try { const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); + if (protectedHeader.url !== url) { + throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" }); + } // TODO: consume the nonce here const decoder = new TextDecoder(); const textPayload = decoder.decode(rawPayload); @@ -136,39 +146,47 @@ export const pkiAcmeServiceFactory = ({ } }; - const validateNewAccountJwsPayload = async ( - rawJwsPayload: TRawJwsPayload - ): Promise> => { - return await validateJwsPayload( + const validateNewAccountJwsPayload = async ({ + url, + rawJwsPayload + }: { + url: string; + rawJwsPayload: TRawJwsPayload; + }): Promise> => { + return await validateJwsPayload({ + url, rawJwsPayload, - async (protectedHeader) => { + getJWK: async (protectedHeader) => { if (!protectedHeader.jwk) { throw new AcmeMalformedError({ detail: "JWK is required in the protected header" }); } return protectedHeader.jwk as unknown as JsonWebKey; }, - CreateAcmeAccountBodySchema - ); + schema: CreateAcmeAccountBodySchema + }); }; const validateExistingAccountJwsPayload = async < TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string >({ + url, profileId, rawJwsPayload, schema, expectedAccountId }: { + url: string; profileId: string; rawJwsPayload: TRawJwsPayload; schema?: TSchema; expectedAccountId?: string; }): Promise> => { const profile = await validateAcmeProfile(profileId); - const result = await validateJwsPayload( + const result = await validateJwsPayload({ + url, rawJwsPayload, - async (protectedHeader) => { + getJWK: async (protectedHeader) => { if (!protectedHeader.kid) { throw new AcmeMalformedError({ detail: "KID is required in the protected header" }); } @@ -186,7 +204,7 @@ export const pkiAcmeServiceFactory = ({ return account.publicKey as JsonWebKey; }, schema - ); + }); return { ...result, accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId), @@ -194,15 +212,6 @@ export const pkiAcmeServiceFactory = ({ }; }; - const getAcmeDirectory = async (profileId: string): Promise => { - const profile = await validateAcmeProfile(profileId); - return { - newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-nonce`), - newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-account`), - newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-order`) - }; - }; - const buildAcmeOrderResource = ({ profileId, order @@ -233,6 +242,15 @@ export const pkiAcmeServiceFactory = ({ }; }; + const getAcmeDirectory = async (profileId: string): Promise => { + const profile = await validateAcmeProfile(profileId); + return { + newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-nonce`), + newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-account`), + newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-order`) + }; + }; + const getAcmeNewNonce = async (profileId: string): Promise => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME new nonce generation 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 8fded06d4..1f962a220 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -52,21 +52,35 @@ export type TPkiAcmeServiceFactory = { validateJwsPayload: < TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string - >( - rawJwsPayload: TRawJwsPayload, - getJWK: (protectedHeader: JWSHeaderParameters) => Promise, - schema?: TSchema - ) => Promise>; - validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise>; + >({ + url, + rawJwsPayload, + getJWK, + schema + }: { + url: string; + rawJwsPayload: TRawJwsPayload; + getJWK: (protectedHeader: JWSHeaderParameters) => Promise; + schema?: z.ZodSchema; + }) => Promise>; + validateNewAccountJwsPayload: ({ + url, + rawJwsPayload + }: { + url: string; + rawJwsPayload: TRawJwsPayload; + }) => Promise>; validateExistingAccountJwsPayload: < TSchema extends z.ZodSchema | undefined = undefined, T = TSchema extends z.ZodSchema ? R : string >({ + url, profileId, rawJwsPayload, schema, expectedAccountId }: { + url: string; profileId: string; rawJwsPayload: TRawJwsPayload; schema?: TSchema;