diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index e576eb306..362c8b38e 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -10,15 +10,18 @@ import { DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountResponseSchema, FinalizeAcmeOrderBodySchema, + FinalizeAcmeOrderResponseSchema, GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, GetAcmeOrderResponseSchema, + ListAcmeOrdersPayloadSchema, ListAcmeOrdersResponseSchema, RawJwsPayloadSchema, RespondToAcmeChallengeResponseSchema } from "@app/ee/services/pki-acme/pki-acme-schemas"; import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { AcmeAccountDoesNotExistError, AcmeMalformedError } from "@app/ee/services/pki-acme/pki-acme-errors"; export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { const sendAcmeResponse = async (res: FastifyReply, profileId: string, response: TAcmeResponse): Promise => { @@ -158,16 +161,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const { payload, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload( - req.params.profileId, - req.body, - CreateAcmeOrderBodySchema - ); + const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body, + schema: CreateAcmeOrderBodySchema + }); return sendAcmeResponse( res, - req.params.profileId, + profileId, await server.services.pkiAcme.createAcmeOrder({ - profileId: req.params.profileId, + profileId, accountId, payload }) @@ -191,7 +194,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), accountId: z.string() }), - body: DeactivateAcmeAccountBodySchema, + body: RawJwsPayloadSchema, response: { 200: DeactivateAcmeAccountResponseSchema } @@ -199,13 +202,19 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // 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 server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body, + schema: DeactivateAcmeAccountBodySchema, + expectedAccountId: req.params.accountId + }); return sendAcmeResponse( res, - req.params.profileId, + profileId, await server.services.pkiAcme.deactivateAcmeAccount({ - profileId: req.params.profileId, - accountId: req.params.accountId, - payload: req.body + profileId, + accountId, + payload }) ); } @@ -227,15 +236,28 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), accountId: z.string() }), + body: RawJwsPayloadSchema, response: { 200: ListAcmeOrdersResponseSchema } }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const orders = await server.services.pkiAcme.listAcmeOrders(req.params.profileId, req.params.accountId); - return orders; + handler: async (req, res) => { + const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body, + schema: ListAcmeOrdersPayloadSchema, + expectedAccountId: req.params.accountId + }); + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.listAcmeOrders({ + profileId, + accountId + }) + ); } }); @@ -255,15 +277,27 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), orderId: z.string().uuid() }), + body: RawJwsPayloadSchema, response: { 200: GetAcmeOrderResponseSchema } }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const order = await server.services.pkiAcme.getAcmeOrder(req.params.profileId, req.params.orderId); - return order; + handler: async (req, res) => { + const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body + }); + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.getAcmeOrder({ + profileId, + accountId, + orderId: req.params.orderId + }) + ); } }); @@ -283,13 +317,29 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), orderId: z.string().uuid() }), - body: FinalizeAcmeOrderBodySchema + body: RawJwsPayloadSchema, + response: { + 200: FinalizeAcmeOrderResponseSchema + } }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const order = await server.services.pkiAcme.finalizeAcmeOrder(req.params.profileId, req.params.orderId, req.body); - return order; + handler: async (req, res) => { + const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body + schema: FinalizeAcmeOrderBodySchema, + }); + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.finalizeAcmeOrder({ + profileId, + accountId, + orderId: req.params.orderId, + payload + }) + ); } }); @@ -309,6 +359,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), orderId: z.string().uuid() }), + body: RawJwsPayloadSchema, response: { 200: z.string() } @@ -316,12 +367,19 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req, res) => { - const certificate = await server.services.pkiAcme.downloadAcmeCertificate( - req.params.profileId, - req.params.orderId + const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body + }); + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.downloadAcmeCertificate({ + profileId, + accountId, + orderId: req.params.orderId + }) ); - res.header("Content-Type", "application/pem-certificate-chain"); - return certificate; } }); @@ -341,15 +399,30 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { profileId: z.string().uuid(), authzId: z.string().uuid() }), + body: RawJwsPayloadSchema, response: { 200: GetAcmeAuthorizationResponseSchema } }, // TODO: replace with verify ACME signature here instead // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), - handler: async (req) => { - const authz = await server.services.pkiAcme.getAcmeAuthorization(req.params.profileId, req.params.authzId); - return authz; + handler: async (req, res) => { + const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + profileId: req.params.profileId, + rawJwsPayload: req.body + }); + if (payload !== "") { + throw new AcmeMalformedError({ detail: "Payload should be empty" }); + } + return sendAcmeResponse( + res, + profileId, + await server.services.pkiAcme.getAcmeAuthorization({ + profileId, + accountId, + authzId: req.params.authzId + }) + ); } }); 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 9b6f9d4e5..4d51e31dd 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -93,10 +93,13 @@ export const pkiAcmeServiceFactory = ({ return kid.slice(kidPrefix.length); }; - const validateJwsPayload = async ( + const validateJwsPayload = async < + TSchema extends z.ZodSchema | undefined = undefined, + T = TSchema extends z.ZodSchema ? R : string + >( rawJwsPayload: TRawJwsPayload, getJWK: (protectedHeader: JWSHeaderParameters) => Promise, - schema: z.ZodSchema + schema?: TSchema ): Promise> => { let result: FlattenedVerifyResult; try { @@ -122,8 +125,8 @@ export const pkiAcmeServiceFactory = ({ const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); // TODO: consume the nonce here const decoder = new TextDecoder(); - const jsonPayload = JSON.parse(decoder.decode(rawPayload)); - const payload = schema.parse(jsonPayload); + const textPayload = decoder.decode(rawPayload); + const payload = schema ? schema.parse(JSON.parse(textPayload)) : textPayload; return { protectedHeader, payload @@ -152,7 +155,10 @@ export const pkiAcmeServiceFactory = ({ ); }; - const validateExistingAccountJwsPayload = async ({ + const validateExistingAccountJwsPayload = async < + TSchema extends z.ZodSchema | undefined = undefined, + T = TSchema extends z.ZodSchema ? R : string + >({ profileId, rawJwsPayload, schema, @@ -160,7 +166,7 @@ export const pkiAcmeServiceFactory = ({ }: { profileId: string; rawJwsPayload: TRawJwsPayload; - schema: z.ZodSchema; + schema?: TSchema; expectedAccountId?: string; }): Promise> => { const profile = await validateAcmeProfile(profileId); @@ -187,7 +193,8 @@ export const pkiAcmeServiceFactory = ({ ); return { ...result, - accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId) + accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId), + profileId }; }; @@ -406,10 +413,12 @@ export const pkiAcmeServiceFactory = ({ const finalizeAcmeOrder = async ({ profileId, + accountId, orderId, payload }: { profileId: string; + accountId: string; orderId: string; payload: TFinalizeAcmeOrderPayload; }): Promise> => { @@ -453,12 +462,18 @@ export const pkiAcmeServiceFactory = ({ const getAcmeAuthorization = async ({ profileId, + accountId, authzId }: { profileId: string; + accountId: string; authzId: string; }): Promise> => { const profile = await validateAcmeProfile(profileId); + const order = await acmeOrderDAL.findByIdWithAuthorizations(orderId); + if (!order || order.accountId !== accountId) { + throw new NotFoundError({ message: "ACME order not found" }); + } // FIXME: Implement ACME authorization retrieval return { status: 200, 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 e9fa4ffa0..a1f65d8f8 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -43,6 +43,7 @@ export type TJwsPayload = { payload: T; }; export type TAuthenciatedJwsPayload = TJwsPayload & { + profileId: string; accountId: string; }; export type TAcmeResponse = { @@ -52,13 +53,19 @@ export type TAcmeResponse = { }; export type TPkiAcmeServiceFactory = { - validateJwsPayload: ( + validateJwsPayload: < + TSchema extends z.ZodSchema | undefined = undefined, + T = TSchema extends z.ZodSchema ? R : string + >( rawJwsPayload: TRawJwsPayload, getJWK: (protectedHeader: JWSHeaderParameters) => Promise, - schema: z.ZodSchema + schema?: TSchema ) => Promise>; validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise>; - validateExistingAccountJwsPayload: ({ + validateExistingAccountJwsPayload: < + TSchema extends z.ZodSchema | undefined = undefined, + T = TSchema extends z.ZodSchema ? R : string + >({ profileId, rawJwsPayload, schema, @@ -66,7 +73,7 @@ export type TPkiAcmeServiceFactory = { }: { profileId: string; rawJwsPayload: TRawJwsPayload; - schema: z.ZodSchema; + schema?: TSchema; expectedAccountId?: string; }) => Promise>; getAcmeDirectory: (profileId: string) => Promise; @@ -118,10 +125,12 @@ export type TPkiAcmeServiceFactory = { }) => Promise>; finalizeAcmeOrder: ({ profileId, + accountId, orderId, payload }: { profileId: string; + accountId: string; orderId: string; payload: TFinalizeAcmeOrderPayload; }) => Promise>;