From 2b8a6b801deea31fc67ff3a074c345e16337da63 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Tue, 28 Oct 2025 20:13:38 -0700 Subject: [PATCH] Work on order --- backend/bdd/features/steps/pki_acme.py | 2 +- backend/src/ee/routes/v1/pki-acme-router.ts | 39 ++++++----- .../services/pki-acme/pki-acme-account-dal.ts | 54 +-------------- .../ee/services/pki-acme/pki-acme-service.ts | 68 ++++++++++++++++--- .../ee/services/pki-acme/pki-acme-types.ts | 21 ++++-- 5 files changed, 100 insertions(+), 84 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index eb8a04315..8546cb217 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -36,7 +36,7 @@ def step_impl(context: Context, profile_var: str): # TODO: Fixed value for now, just to make test much easier, # we should call infisical API to create such profile instead # in the future - profile_id = "0e96a01b-017e-4660-8b3d-ff26018fe0ce" + profile_id = "dd6e09c8-d5b8-4bfd-b436-4ab5c93d5d7e" context.vars[profile_var] = AcmeProfile(profile_id) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index b60b13dde..13bad0a6a 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -1,12 +1,10 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ import { z } from "zod"; -import { AcmeBadPublicKeyError } from "@app/ee/services/pki-acme/pki-acme-errors"; import { - CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, + CreateAcmeOrderBodySchema, CreateAcmeOrderResponseSchema, - CreateAcmeOrderSchema, DeactivateAcmeAccountResponseSchema, DeactivateAcmeAccountSchema, DownloadAcmeCertificateSchema, @@ -25,7 +23,6 @@ import { RespondToAcmeChallengeResponseSchema, RespondToAcmeChallengeSchema } from "@app/ee/services/pki-acme/pki-acme-schemas"; -import { TRawJwsPayload } from "@app/ee/services/pki-acme/pki-acme-types"; import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -104,29 +101,23 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME New Account - register a new account or find existing one", - ...RawJwsPayloadSchema.shape, + params: z.object({ + profileId: z.string().uuid() + }), + body: RawJwsPayloadSchema, response: { 201: CreateAcmeAccountResponseSchema } }, handler: async (req, res) => { - const { payload, protectedHeader } = await server.services.pkiAcme.validateJwsPayload( - req.body as TRawJwsPayload, - async (protectedHeader) => { - if (!protectedHeader.jwk) { - throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); - } - return protectedHeader.jwk as unknown as JsonWebKey; - }, - CreateAcmeAccountBodySchema - ); + const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload(req.body); const { alg, jwk } = protectedHeader; - const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount( - req.params.profileId, + const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount({ + profileId: req.params.profileId, alg, - jwk!, + jwk: jwk!, payload - ); + }); // TODO: DRY res.code(status); for (const [key, value] of Object.entries(headers)) { @@ -153,7 +144,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { hide: false, tags: [ApiDocsTags.PkiAcme], description: "ACME New Order - apply for a new certificate", - ...CreateAcmeOrderSchema.shape, + params: z.object({ + profileId: z.string().uuid() + }), + body: RawJwsPayloadSchema, response: { 201: CreateAcmeOrderResponseSchema } @@ -161,6 +155,11 @@ 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, protectedHeader, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload( + req.params.profileId, + req.body, + CreateAcmeOrderBodySchema + ); const order = await server.services.pkiAcme.createAcmeOrder(req.params.profileId, req.body); res.code(201); return order; diff --git a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts index 15c07b0e1..713cb4350 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-account-dal.ts @@ -26,24 +26,9 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { } }; - const updateById = async (id: string, data: TPkiAcmeAccountsUpdate, tx?: Knex) => { + const findById = async (profileId: string, id: string, tx?: Knex) => { try { - const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).update(data).returning("*"); - const [account] = result; - - if (!account) { - return null; - } - - return account; - } catch (error) { - throw new DatabaseError({ error, name: "Update PKI ACME account" }); - } - }; - - const findById = async (id: string, tx?: Knex) => { - try { - const account = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).first(); + const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, id }).first(); return account || null; } catch (error) { @@ -51,16 +36,6 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { } }; - const findByProfileId = async (profileId: string, tx?: Knex) => { - try { - const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId }).first(); - - return account || null; - } catch (error) { - throw new DatabaseError({ error, name: "Find PKI ACME account by profile id" }); - } - }; - const findByPublicKey = async (profileId: string, alg: string, publicKey: unknown, tx?: Knex) => { try { const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, alg, publicKey }).first(); @@ -71,35 +46,12 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => { } }; - const findManyByProfileId = async (profileId: string, tx?: Knex) => { - try { - const accounts = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId }); - - return accounts; - } catch (error) { - throw new DatabaseError({ error, name: "Find many PKI ACME accounts by profile id" }); - } - }; - - const deleteById = async (id: string, tx?: Knex) => { - try { - const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).delete().returning("*"); - const [account] = result; - - return account || null; - } catch (error) { - throw new DatabaseError({ error, name: "Delete PKI ACME account by id" }); - } - }; - return { ...pkiAcmeAccountOrm, create, updateById, findById, findByProfileId, - findByPublicKey, - findManyByProfileId, - deleteById + findByPublicKey }; }; 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 46f67ac17..a6d4b63ac 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -23,6 +23,7 @@ import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { CreateAcmeAccountBodySchema, ProtectedHeaderSchema } from "./pki-acme-schemas"; import { TAcmeResponse, + TAuthenciatedJwsPayload, TCreateAcmeAccountPayload, TCreateAcmeAccountResponse, TCreateAcmeOrderPayload, @@ -43,7 +44,7 @@ import { type TPkiAcmeServiceFactoryDep = { certificateProfileDAL: Pick; - acmeAccountDAL: Pick; + acmeAccountDAL: Pick; acmeOrderDAL: Pick; }; @@ -69,6 +70,14 @@ export const pkiAcmeServiceFactory = ({ return `${baseUrl}${path}`; }; + const extractAccountIdFromKid = (kid: string, profileId: string): string => { + const kidPrefix = buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/`); + if (!kid.startsWith(kidPrefix)) { + throw new AcmeMalformedError({ detail: "KID must start with the profile account URL" }); + } + return kid.slice(kidPrefix.length); + }; + const validateJwsPayload = async ( rawJwsPayload: TRawJwsPayload, getJWK: (protectedHeader: JWSHeaderParameters) => Promise, @@ -120,7 +129,7 @@ export const pkiAcmeServiceFactory = ({ rawJwsPayload, async (protectedHeader) => { if (!protectedHeader.jwk) { - throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); + throw new AcmeMalformedError({ detail: "JWK is required in the protected header" }); } return protectedHeader.jwk as unknown as JsonWebKey; }, @@ -128,6 +137,36 @@ export const pkiAcmeServiceFactory = ({ ); }; + const validateExistingAccountJwsPayload = async ( + profileId: string, + rawJwsPayload: TRawJwsPayload, + schema: z.ZodSchema + ): Promise> => { + const profile = await validateAcmeProfile(profileId); + const result = await validateJwsPayload( + rawJwsPayload, + async (protectedHeader) => { + if (!protectedHeader.kid) { + throw new AcmeMalformedError({ detail: "KID is required in the protected header" }); + } + const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId); + const account = await acmeAccountDAL.findById(profile.id, accountId); + if (!account) { + throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); + } + if (account.alg !== protectedHeader.alg) { + throw new AcmeMalformedError({ detail: "ACME account algorithm mismatch" }); + } + return account.publicKey as JsonWebKey; + }, + schema + ); + return { + ...result, + accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId) + }; + }; + const getAcmeDirectory = async (profileId: string): Promise => { await validateAcmeProfile(profileId); return { @@ -195,13 +234,25 @@ export const pkiAcmeServiceFactory = ({ }; }; - const createAcmeOrder = async ( - profileId: string, - account: TPkiAcmeAccounts, - payload: TCreateAcmeOrderPayload - ): Promise> => { - const profile = await validateAcmeProfile(profileId); + const createAcmeOrder = async ({ + profileId, + accountId, + payload + }: { + profileId: string; + accountId: string; + payload: TCreateAcmeOrderPayload; + }): Promise> => { + const account = await acmeAccountDAL.findById(profileId, accountId)!; + // TODO: check and see if we have existing orders for this account that meet the criteria + // if we do, return the existing order + + orders = await acmeOrderDAL.create({ + profileId, + accountId, + status: "pending" + }); // FIXME: Implement ACME new order creation const orderId = "FIXME-order-id"; return { @@ -315,6 +366,7 @@ export const pkiAcmeServiceFactory = ({ return { validateJwsPayload, validateNewAccountJwsPayload, + validateExistingAccountJwsPayload, getAcmeDirectory, getAcmeNewNonce, createAcmeAccount, 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 3bb03f6a6..7051222fc 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -42,6 +42,9 @@ export type TJwsPayload = { protectedHeader: TProtectedHeader; payload: T; }; +export type TAuthenciatedJwsPayload = TJwsPayload & { + accountId: string; +}; export type TAcmeResponse = { status: number; headers: Record; @@ -55,6 +58,11 @@ export type TPkiAcmeServiceFactory = { schema: z.ZodSchema ) => Promise>; validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise>; + validateExistingAccountJwsPayload: ( + profileId: string, + rawJwsPayload: TRawJwsPayload, + schema: z.ZodSchema + ) => Promise>; getAcmeDirectory: (profileId: string) => Promise; getAcmeNewNonce: (profileId: string) => Promise; createAcmeAccount: ({ @@ -68,10 +76,15 @@ export type TPkiAcmeServiceFactory = { jwk: JsonWebKey; payload: TCreateAcmeAccountPayload; }) => Promise>; - createAcmeOrder: ( - profileId: string, - body: TCreateAcmeOrderPayload - ) => Promise>; + createAcmeOrder: ({ + profileId, + accountId, + payload + }: { + profileId: string; + accountId: string; + payload: TCreateAcmeOrderPayload; + }) => Promise>; deactivateAcmeAccount: ( profileId: string, accountId: string,