diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 7dcd63c0e..8a19670f0 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -722,6 +722,17 @@ declare module "knex/types/tables" { TPkiAcmeAccountsInsert, TPkiAcmeAccountsUpdate >; + [TableName.PkiAcmeOrder]: KnexOriginal.CompositeTableType< + TPkiAcmeOrders, + TPkiAcmeOrdersInsert, + TPkiAcmeOrdersUpdate + >; + [TableName.PkiAcmeAuth]: KnexOriginal.CompositeTableType; + [TableName.PkiAcmeChallenge]: KnexOriginal.CompositeTableType< + TPkiAcmeChallenges, + TPkiAcmeChallengesInsert, + TPkiAcmeChallengesUpdate + >; [TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType< TCertificateTemplateEstConfigs, TCertificateTemplateEstConfigsInsert, diff --git a/backend/src/db/migrations/20251027234547_add-pki-acme.ts b/backend/src/db/migrations/20251027234547_add-pki-acme.ts index 781fe040e..285676c8f 100644 --- a/backend/src/db/migrations/20251027234547_add-pki-acme.ts +++ b/backend/src/db/migrations/20251027234547_add-pki-acme.ts @@ -56,6 +56,7 @@ export async function up(knex: Knex): Promise { // Multi-value emails array t.specificType("emails", "text[]").notNullable(); + // TODO: make public key a string instead of jsonb to make indexing much easier // Public key (JWK format) t.jsonb("publicKey").notNullable(); // The JWS algorithm used to sign the public key when creating the account, e.g. "RS256", "ES256", "PS256", etc. diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index e0762b7f3..8361fd330 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { + CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, CreateAcmeOrderResponseSchema, CreateAcmeOrderSchema, @@ -27,6 +28,7 @@ import { TCreateAcmeAccountPayload, TRawJwsPayload } from "@app/ee/services/pki- import { ApiDocsTags } from "@app/lib/api-docs"; import { getConfig } from "@app/lib/config/env"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { AcmeBadPublicKeyError } from "@app/ee/services/pki-acme/pki-acme-errors"; export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); @@ -111,13 +113,24 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { } }, handler: async (req, res) => { - const { payload, jwk } = await server.services.pkiAcme.validateCreateAcmeAccountJwsPayload( - req.body as TRawJwsPayload + const { payload, protectedHeader, jwk } = 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 ); + if (!jwk) { + throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" }); + } const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount( req.params.profileId, + protectedHeader.alg, jwk, - payload as TCreateAcmeAccountPayload + payload ); // TODO: DRY res.code(status); 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 599203dae..7e50b2108 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -20,11 +20,6 @@ export const RawJwsPayloadSchema = z.object({ signature: z.string() }); -export const JwsPayloadSchema = z.object({ - protectedHeader: ProtectedHeaderSchema, - payload: z.unknown() -}); - // Directory endpoint export const GetAcmeDirectorySchema = z.object({ params: z.object({ 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 57aa504b3..b75a76979 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -81,7 +81,8 @@ export const pkiAcmeServiceFactory = ({ if (protectedHeader === undefined) { throw new AcmeMalformedError({ detail: "Protected header is required" }); } - ProtectedHeaderSchema.parse(protectedHeader); + const parsedHeader = ProtectedHeaderSchema.parse(protectedHeader); + // TODO: consume the nonce here const jwk = await getJWK(protectedHeader); return await importJWK(jwk, protectedHeader.alg); }); 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 4bf7411c4..35571b0cb 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-types.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-types.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { JWSHeaderParameters } from "jose"; import { CreateAcmeAccountBodySchema, CreateAcmeAccountResponseSchema, @@ -12,7 +13,6 @@ import { GetAcmeAuthorizationResponseSchema, GetAcmeDirectoryResponseSchema, GetAcmeOrderResponseSchema, - JwsPayloadSchema, ListAcmeOrdersResponseSchema, ProtectedHeaderSchema, RawJwsPayloadSchema, @@ -32,16 +32,16 @@ export type TRespondToAcmeChallengeResponse = z.infer; -export type TJwsPayload = z.infer; export type TProtectedHeader = z.infer; export type TCreateAcmeAccountPayload = z.infer; export type TCreateAcmeOrderPayload = z.infer; export type TDeactivateAcmeAccountPayload = z.infer; export type TFinalizeAcmeOrderPayload = z.infer; -export type TJwsPayloadWithJwk = TJwsPayload & { - jwk: JsonWebKey; - alg: string; +export type TJwsPayload = { + protectedHeader: TProtectedHeader; + jwk?: JsonWebKey; + payload: T; }; export type TAcmeResponse = { status: number; @@ -50,7 +50,11 @@ export type TAcmeResponse = { }; export type TPkiAcmeServiceFactory = { - validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise; + validateJwsPayload: ( + rawJwsPayload: TRawJwsPayload, + getJWK: (protectedHeader: JWSHeaderParameters) => Promise, + schema: z.ZodSchema + ) => Promise>; getAcmeDirectory: (profileId: string) => Promise; getAcmeNewNonce: (profileId: string) => Promise; createAcmeAccount: (