From 6b85ab54a4f12b57ed3bcf121805b55ed7484470 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 30 Oct 2025 09:45:15 -0700 Subject: [PATCH] Refactor code --- backend/src/db/schemas/pki-acme-orders.ts | 4 +- backend/src/ee/routes/v1/pki-acme-router.ts | 119 ++++++----- .../ee/services/pki-acme/pki-acme-schemas.ts | 36 +--- .../ee/services/pki-acme/pki-acme-service.ts | 188 ++++++++++-------- .../ee/services/pki-acme/pki-acme-types.ts | 38 ++-- 5 files changed, 188 insertions(+), 197 deletions(-) diff --git a/backend/src/db/schemas/pki-acme-orders.ts b/backend/src/db/schemas/pki-acme-orders.ts index d52c30662..5f18a3b8c 100644 --- a/backend/src/db/schemas/pki-acme-orders.ts +++ b/backend/src/db/schemas/pki-acme-orders.ts @@ -12,7 +12,9 @@ export const PkiAcmeOrdersSchema = z.object({ accountId: z.string().uuid(), status: z.string(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + notBefore: z.date().nullable().optional(), + notAfter: z.date().nullable().optional() }); export type TPkiAcmeOrders = z.infer; diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 362c8b38e..0cb39657d 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -4,16 +4,14 @@ import { FastifyReply } from "fastify"; import { z } from "zod"; import { + AcmeOrderResourceSchema, CreateAcmeAccountResponseSchema, CreateAcmeOrderBodySchema, - CreateAcmeOrderResponseSchema, DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountResponseSchema, FinalizeAcmeOrderBodySchema, - FinalizeAcmeOrderResponseSchema, GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, - GetAcmeOrderResponseSchema, ListAcmeOrdersPayloadSchema, ListAcmeOrdersResponseSchema, RawJwsPayloadSchema, @@ -138,46 +136,6 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//new-order - // New Certificate Order (RFC 8555 Section 7.4) - server.route({ - method: "POST", - url: "/profiles/:profileId/new-order", - config: { - rateLimit: writeLimit - }, - schema: { - hide: false, - tags: [ApiDocsTags.PkiAcme], - description: "ACME New Order - apply for a new certificate", - params: z.object({ - profileId: z.string().uuid() - }), - body: RawJwsPayloadSchema, - response: { - 201: CreateAcmeOrderResponseSchema - } - }, - // 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 server.services.pkiAcme.validateExistingAccountJwsPayload({ - profileId: req.params.profileId, - rawJwsPayload: req.body, - schema: CreateAcmeOrderBodySchema - }); - return sendAcmeResponse( - res, - profileId, - await server.services.pkiAcme.createAcmeOrder({ - profileId, - accountId, - payload - }) - ); - } - }); - // POST /api/v1/pki/acme/profiles//accounts/ // Account Deactivation (RFC 8555 Section 7.3.6) server.route({ @@ -220,42 +178,41 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }); - // POST /api/v1/pki/acme/profiles//accounts//orders - // List Orders (RFC 8555 Section 7.1.2.1) + // POST /api/v1/pki/acme/profiles//new-order + // New Certificate Order (RFC 8555 Section 7.4) server.route({ method: "POST", - url: "/profiles/:profileId/accounts/:accountId/orders", + url: "/profiles/:profileId/new-order", config: { - rateLimit: readLimit + rateLimit: writeLimit }, schema: { hide: false, tags: [ApiDocsTags.PkiAcme], - description: "ACME List Orders - get existing orders from current account", + description: "ACME New Order - apply for a new certificate", params: z.object({ - profileId: z.string().uuid(), - accountId: z.string() + profileId: z.string().uuid() }), body: RawJwsPayloadSchema, response: { - 200: ListAcmeOrdersResponseSchema + 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 } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ + const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ profileId: req.params.profileId, rawJwsPayload: req.body, - schema: ListAcmeOrdersPayloadSchema, - expectedAccountId: req.params.accountId + schema: CreateAcmeOrderBodySchema }); return sendAcmeResponse( res, profileId, - await server.services.pkiAcme.listAcmeOrders({ + await server.services.pkiAcme.createAcmeOrder({ profileId, - accountId + accountId, + payload }) ); } @@ -279,7 +236,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { }), body: RawJwsPayloadSchema, response: { - 200: GetAcmeOrderResponseSchema + 200: AcmeOrderResourceSchema } }, // TODO: replace with verify ACME signature here instead @@ -319,16 +276,16 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { }), body: RawJwsPayloadSchema, response: { - 200: FinalizeAcmeOrderResponseSchema + 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 server.services.pkiAcme.validateExistingAccountJwsPayload({ + const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ profileId: req.params.profileId, - rawJwsPayload: req.body - schema: FinalizeAcmeOrderBodySchema, + rawJwsPayload: req.body, + schema: FinalizeAcmeOrderBodySchema }); return sendAcmeResponse( res, @@ -342,6 +299,46 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { ); } }); + // POST /api/v1/pki/acme/profiles//accounts//orders + // List Orders (RFC 8555 Section 7.1.2.1) + server.route({ + method: "POST", + url: "/profiles/:profileId/accounts/:accountId/orders", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + tags: [ApiDocsTags.PkiAcme], + description: "ACME List Orders - get existing orders from current account", + params: z.object({ + 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, 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 + }) + ); + } + }); // POST /api/v1/pki/acme/profiles//orders//certificate // Download Certificate (RFC 8555 Section 7.4.2) 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 a10476ca4..ad40ea17b 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -90,9 +90,11 @@ export const CreateAcmeOrderBodySchema = z.object({ notAfter: z.string().optional() }); -export const CreateAcmeOrderResponseSchema = z.object({ - status: z.string(), - expires: z.string(), +export const AcmeOrderResourceSchema = z.object({ + status: z.enum(Object.values(AcmeOrderStatus) as [string, ...string[]]), + expires: z.string().optional(), + notBefore: z.string().optional(), + notAfter: z.string().optional(), identifiers: z.array( z.object({ type: z.string(), @@ -120,39 +122,11 @@ export const ListAcmeOrdersResponseSchema = z.object({ orders: z.array(z.string()) }); -export const GetAcmeOrderResponseSchema = z.object({ - status: z.enum(Object.values(AcmeOrderStatus) as [string, ...string[]]), - expires: z.string().optional(), - identifiers: z.array( - z.object({ - type: z.string(), - value: z.string() - }) - ), - authorizations: z.array(z.string()), - finalize: z.string(), - certificate: z.string().optional() -}); - // Finalize Order payload schema export const FinalizeAcmeOrderBodySchema = z.object({ csr: z.string() }); -export const FinalizeAcmeOrderResponseSchema = z.object({ - status: z.enum(Object.values(AcmeOrderStatus) as [string, ...string[]]), - expires: z.string().optional(), - identifiers: z.array( - z.object({ - type: z.string(), - value: z.string() - }) - ), - authorizations: z.array(z.string()), - finalize: z.string(), - certificate: z.string().optional() -}); - export const GetAcmeAuthorizationResponseSchema = z.object({ status: z.enum(Object.values(AcmeAuthStatus) as [string, ...string[]]), expires: z.string().optional(), 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 d15992c99..49dfb72ef 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -37,16 +37,14 @@ import { TCreateAcmeAccountPayload, TCreateAcmeAccountResponse, TCreateAcmeOrderPayload, - TCreateAcmeOrderResponse, TDeactivateAcmeAccountPayload, TDeactivateAcmeAccountResponse, TFinalizeAcmeOrderPayload, - TFinalizeAcmeOrderResponse, TGetAcmeAuthorizationResponse, TGetAcmeDirectoryResponse, - TGetAcmeOrderResponse, TJwsPayload, TListAcmeOrdersResponse, + TAcmeOrderResource, TPkiAcmeServiceFactory, TRawJwsPayload, TRespondToAcmeChallengeResponse @@ -206,6 +204,36 @@ export const pkiAcmeServiceFactory = ({ }; }; + const buildAcmeOrderResource = ({ + profileId, + order + }: { + order: { + id: string; + status: string; + expiresAt: Date; + notBefore?: Date | null; + notAfter?: Date | null; + authorizations: TPkiAcmeAuths[]; + }; + profileId: string; + }) => { + return { + status: order.status, + expires: order.expiresAt.toISOString(), + notBefore: order.notBefore?.toISOString(), + notAfter: order.notAfter?.toISOString(), + identifiers: order.authorizations.map((auth: TPkiAcmeAuths) => ({ + type: auth.identifierType, + value: auth.identifierValue + })), + authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => + buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${auth.id}`) + ), + finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${order.id}/finalize`) + }; + }; + const getAcmeNewNonce = async (profileId: string): Promise => { const profile = await validateAcmeProfile(profileId); // FIXME: Implement ACME new nonce generation @@ -213,6 +241,9 @@ export const pkiAcmeServiceFactory = ({ return "FIXME-generate-nonce"; }; + /** -------------------------------------------------------------- + * ACME Account + * -------------------------------------------------------------- */ const createAcmeAccount = async ({ profileId, alg, @@ -251,6 +282,7 @@ export const pkiAcmeServiceFactory = ({ publicKey: jwk, emails: contact ?? [] }); + // TODO: create audit log here // TODO: check EAB authentication here return { status: 201, @@ -265,6 +297,31 @@ export const pkiAcmeServiceFactory = ({ }; }; + const deactivateAcmeAccount = async ({ + profileId, + accountId, + payload: { status } = { status: "deactivated" } + }: { + profileId: string; + accountId: string; + payload?: TDeactivateAcmeAccountPayload; + }): Promise> => { + const profile = await validateAcmeProfile(profileId); + // FIXME: Implement ACME account deactivation + return { + status: 200, + body: { + status: "deactivated" + }, + headers: { + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`) + } + }; + }; + + /** -------------------------------------------------------------- + * ACME Order + * -------------------------------------------------------------- */ const createAcmeOrder = async ({ profileId, accountId, @@ -273,7 +330,7 @@ export const pkiAcmeServiceFactory = ({ profileId: string; accountId: string; payload: TCreateAcmeOrderPayload; - }): Promise> => { + }): Promise> => { // TODO: check and see if we have existing orders for this account that meet the criteria // if we do, return the existing order @@ -314,47 +371,68 @@ export const pkiAcmeServiceFactory = ({ })), tx ); + // TODO: create audit log here return { ...createdOrder, authorizations, account }; }); return { status: 201, - body: { - status: order.status, - expires: order.expiresAt.toISOString(), - identifiers: order.authorizations.map((auth: TPkiAcmeAuths) => ({ - type: auth.identifierType, - value: auth.identifierValue - })), - authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => - buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/authorizations/${auth.id}`) - ), - finalize: buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/orders/${order.id}/finalize`) - }, + body: buildAcmeOrderResource({ + profileId, + order + }), headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${order.account.profileId}/orders/${order.id}`) } }; }; - const deactivateAcmeAccount = async ({ + const getAcmeOrder = async ({ profileId, accountId, - payload: { status } = { status: "deactivated" } + orderId }: { profileId: string; accountId: string; - payload?: TDeactivateAcmeAccountPayload; - }): Promise> => { + orderId: string; + }): Promise> => { + const order = await acmeOrderDAL.findByIdWithAuthorizations(orderId); + if (!order || order.accountId !== accountId) { + throw new NotFoundError({ message: "ACME order not found" }); + } + return { + status: 200, + body: buildAcmeOrderResource({ profileId, order }), + headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) } + }; + }; + + const finalizeAcmeOrder = async ({ + profileId, + accountId, + orderId, + payload + }: { + profileId: string; + accountId: string; + orderId: string; + payload: TFinalizeAcmeOrderPayload; + }): Promise> => { const profile = await validateAcmeProfile(profileId); - // FIXME: Implement ACME account deactivation + const { csr } = payload; + // FIXME: Implement ACME finalize order return { status: 200, body: { - status: "deactivated" + status: "processing", + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + identifiers: [], + authorizations: [], + finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`), + certificate: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) }, headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`) + Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) } }; }; @@ -379,67 +457,6 @@ export const pkiAcmeServiceFactory = ({ }; }; - const getAcmeOrder = async ({ - profileId, - accountId, - orderId - }: { - profileId: string; - accountId: string; - orderId: string; - }): Promise> => { - const order = await acmeOrderDAL.findByIdWithAuthorizations(orderId); - if (!order || order.accountId !== accountId) { - throw new NotFoundError({ message: "ACME order not found" }); - } - return { - status: 200, - body: { - status: order.status, - expires: order.expiresAt.toISOString(), - identifiers: order.authorizations.map((auth: TPkiAcmeAuths) => ({ - type: auth.identifierType, - value: auth.identifierValue - })), - authorizations: order.authorizations.map((auth: TPkiAcmeAuths) => - buildUrl(`/api/v1/pki/acme/profiles/${profileId}/authorizations/${auth.id}`) - ), - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`) - }, - headers: { Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) } - }; - }; - - const finalizeAcmeOrder = async ({ - profileId, - accountId, - orderId, - payload - }: { - profileId: string; - accountId: string; - orderId: string; - payload: TFinalizeAcmeOrderPayload; - }): Promise> => { - const profile = await validateAcmeProfile(profileId); - const { csr } = payload; - // FIXME: Implement ACME finalize order - return { - status: 200, - body: { - status: "processing", - expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - identifiers: [], - authorizations: [], - finalize: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/finalize`), - certificate: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}/certificate`) - }, - headers: { - Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/orders/${orderId}`) - } - }; - }; - const downloadAcmeCertificate = async ({ profileId, orderId @@ -459,6 +476,9 @@ export const pkiAcmeServiceFactory = ({ }; }; + /** -------------------------------------------------------------- + * ACME Authorization + * -------------------------------------------------------------- */ const getAcmeAuthorization = async ({ profileId, accountId, 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 93f14de17..672300f18 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -2,17 +2,15 @@ import { z } from "zod"; import { JWSHeaderParameters } from "jose"; import { + AcmeOrderResourceSchema, CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, CreateAcmeOrderBodySchema, - CreateAcmeOrderResponseSchema, DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountResponseSchema, FinalizeAcmeOrderBodySchema, - FinalizeAcmeOrderResponseSchema, GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, - GetAcmeOrderResponseSchema, ListAcmeOrdersResponseSchema, ProtectedHeaderSchema, RawJwsPayloadSchema, @@ -21,11 +19,9 @@ import { export type TGetAcmeDirectoryResponse = z.infer; export type TCreateAcmeAccountResponse = z.infer; -export type TCreateAcmeOrderResponse = z.infer; +export type TAcmeOrderResource = z.infer; export type TDeactivateAcmeAccountResponse = z.infer; export type TListAcmeOrdersResponse = z.infer; -export type TGetAcmeOrderResponse = z.infer; -export type TFinalizeAcmeOrderResponse = z.infer; export type TDownloadAcmeCertificateDTO = string; export type TGetAcmeAuthorizationResponse = z.infer; export type TRespondToAcmeChallengeResponse = z.infer; @@ -89,15 +85,6 @@ export type TPkiAcmeServiceFactory = { jwk: JsonWebKey; payload: TCreateAcmeAccountPayload; }) => Promise>; - createAcmeOrder: ({ - profileId, - accountId, - payload - }: { - profileId: string; - accountId: string; - payload: TCreateAcmeOrderPayload; - }) => Promise>; deactivateAcmeAccount: ({ profileId, accountId, @@ -107,13 +94,15 @@ export type TPkiAcmeServiceFactory = { accountId: string; payload?: TDeactivateAcmeAccountPayload; }) => Promise>; - listAcmeOrders: ({ + createAcmeOrder: ({ profileId, - accountId + accountId, + payload }: { profileId: string; accountId: string; - }) => Promise>; + payload: TCreateAcmeOrderPayload; + }) => Promise>; getAcmeOrder: ({ profileId, accountId, @@ -122,7 +111,7 @@ export type TPkiAcmeServiceFactory = { profileId: string; accountId: string; orderId: string; - }) => Promise>; + }) => Promise>; finalizeAcmeOrder: ({ profileId, accountId, @@ -133,12 +122,21 @@ export type TPkiAcmeServiceFactory = { accountId: string; orderId: string; payload: TFinalizeAcmeOrderPayload; - }) => Promise>; + }) => Promise>; + listAcmeOrders: ({ + profileId, + accountId + }: { + profileId: string; + accountId: string; + }) => Promise>; downloadAcmeCertificate: ({ profileId, + accountId, orderId }: { profileId: string; + accountId: string; orderId: string; }) => Promise>; getAcmeAuthorization: ({