Work on order

This commit is contained in:
Fang-Pen Lin
2025-11-07 09:18:21 -08:00
parent ac82e8071d
commit 2b8a6b801d
5 changed files with 100 additions and 84 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ def step_impl(context: Context, profile_var: str):
# TODO: Fixed value for now, just to make test much easier, # TODO: Fixed value for now, just to make test much easier,
# we should call infisical API to create such profile instead # we should call infisical API to create such profile instead
# in the future # in the future
profile_id = "0e96a01b-017e-4660-8b3d-ff26018fe0ce" profile_id = "dd6e09c8-d5b8-4bfd-b436-4ab5c93d5d7e"
context.vars[profile_var] = AcmeProfile(profile_id) context.vars[profile_var] = AcmeProfile(profile_id)
+19 -20
View File
@@ -1,12 +1,10 @@
/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/no-floating-promises */
import { z } from "zod"; import { z } from "zod";
import { AcmeBadPublicKeyError } from "@app/ee/services/pki-acme/pki-acme-errors";
import { import {
CreateAcmeAccountBodySchema,
CreateAcmeAccountResponseSchema, CreateAcmeAccountResponseSchema,
CreateAcmeOrderBodySchema,
CreateAcmeOrderResponseSchema, CreateAcmeOrderResponseSchema,
CreateAcmeOrderSchema,
DeactivateAcmeAccountResponseSchema, DeactivateAcmeAccountResponseSchema,
DeactivateAcmeAccountSchema, DeactivateAcmeAccountSchema,
DownloadAcmeCertificateSchema, DownloadAcmeCertificateSchema,
@@ -25,7 +23,6 @@ import {
RespondToAcmeChallengeResponseSchema, RespondToAcmeChallengeResponseSchema,
RespondToAcmeChallengeSchema RespondToAcmeChallengeSchema
} from "@app/ee/services/pki-acme/pki-acme-schemas"; } 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 { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -104,29 +101,23 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
hide: false, hide: false,
tags: [ApiDocsTags.PkiAcme], tags: [ApiDocsTags.PkiAcme],
description: "ACME New Account - register a new account or find existing one", description: "ACME New Account - register a new account or find existing one",
...RawJwsPayloadSchema.shape, params: z.object({
profileId: z.string().uuid()
}),
body: RawJwsPayloadSchema,
response: { response: {
201: CreateAcmeAccountResponseSchema 201: CreateAcmeAccountResponseSchema
} }
}, },
handler: async (req, res) => { handler: async (req, res) => {
const { payload, protectedHeader } = await server.services.pkiAcme.validateJwsPayload( const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload(req.body);
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 { alg, jwk } = protectedHeader; const { alg, jwk } = protectedHeader;
const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount( const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount({
req.params.profileId, profileId: req.params.profileId,
alg, alg,
jwk!, jwk: jwk!,
payload payload
); });
// TODO: DRY // TODO: DRY
res.code(status); res.code(status);
for (const [key, value] of Object.entries(headers)) { for (const [key, value] of Object.entries(headers)) {
@@ -153,7 +144,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
hide: false, hide: false,
tags: [ApiDocsTags.PkiAcme], tags: [ApiDocsTags.PkiAcme],
description: "ACME New Order - apply for a new certificate", description: "ACME New Order - apply for a new certificate",
...CreateAcmeOrderSchema.shape, params: z.object({
profileId: z.string().uuid()
}),
body: RawJwsPayloadSchema,
response: { response: {
201: CreateAcmeOrderResponseSchema 201: CreateAcmeOrderResponseSchema
} }
@@ -161,6 +155,11 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// TODO: replace with verify ACME signature here instead // TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), // onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => { 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); const order = await server.services.pkiAcme.createAcmeOrder(req.params.profileId, req.body);
res.code(201); res.code(201);
return order; return order;
@@ -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 { try {
const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).update(data).returning("*"); const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, id }).first();
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();
return account || null; return account || null;
} catch (error) { } 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) => { const findByPublicKey = async (profileId: string, alg: string, publicKey: unknown, tx?: Knex) => {
try { try {
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, alg, publicKey }).first(); 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 { return {
...pkiAcmeAccountOrm, ...pkiAcmeAccountOrm,
create, create,
updateById, updateById,
findById, findById,
findByProfileId, findByProfileId,
findByPublicKey, findByPublicKey
findManyByProfileId,
deleteById
}; };
}; };
@@ -23,6 +23,7 @@ import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal";
import { CreateAcmeAccountBodySchema, ProtectedHeaderSchema } from "./pki-acme-schemas"; import { CreateAcmeAccountBodySchema, ProtectedHeaderSchema } from "./pki-acme-schemas";
import { import {
TAcmeResponse, TAcmeResponse,
TAuthenciatedJwsPayload,
TCreateAcmeAccountPayload, TCreateAcmeAccountPayload,
TCreateAcmeAccountResponse, TCreateAcmeAccountResponse,
TCreateAcmeOrderPayload, TCreateAcmeOrderPayload,
@@ -43,7 +44,7 @@ import {
type TPkiAcmeServiceFactoryDep = { type TPkiAcmeServiceFactoryDep = {
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">; certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findByPublicKey" | "create">; acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById" | "findByPublicKey" | "create">;
acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create">; acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create">;
}; };
@@ -69,6 +70,14 @@ export const pkiAcmeServiceFactory = ({
return `${baseUrl}${path}`; 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 <T>( const validateJwsPayload = async <T>(
rawJwsPayload: TRawJwsPayload, rawJwsPayload: TRawJwsPayload,
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>, getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>,
@@ -120,7 +129,7 @@ export const pkiAcmeServiceFactory = ({
rawJwsPayload, rawJwsPayload,
async (protectedHeader) => { async (protectedHeader) => {
if (!protectedHeader.jwk) { 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; return protectedHeader.jwk as unknown as JsonWebKey;
}, },
@@ -128,6 +137,36 @@ export const pkiAcmeServiceFactory = ({
); );
}; };
const validateExistingAccountJwsPayload = async <T>(
profileId: string,
rawJwsPayload: TRawJwsPayload,
schema: z.ZodSchema<T>
): Promise<TAuthenciatedJwsPayload<T>> => {
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<TGetAcmeDirectoryResponse> => { const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
await validateAcmeProfile(profileId); await validateAcmeProfile(profileId);
return { return {
@@ -195,13 +234,25 @@ export const pkiAcmeServiceFactory = ({
}; };
}; };
const createAcmeOrder = async ( const createAcmeOrder = async ({
profileId: string, profileId,
account: TPkiAcmeAccounts, accountId,
payload: TCreateAcmeOrderPayload payload
): Promise<TAcmeResponse<TCreateAcmeOrderResponse>> => { }: {
const profile = await validateAcmeProfile(profileId); profileId: string;
accountId: string;
payload: TCreateAcmeOrderPayload;
}): Promise<TAcmeResponse<TCreateAcmeOrderResponse>> => {
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 // FIXME: Implement ACME new order creation
const orderId = "FIXME-order-id"; const orderId = "FIXME-order-id";
return { return {
@@ -315,6 +366,7 @@ export const pkiAcmeServiceFactory = ({
return { return {
validateJwsPayload, validateJwsPayload,
validateNewAccountJwsPayload, validateNewAccountJwsPayload,
validateExistingAccountJwsPayload,
getAcmeDirectory, getAcmeDirectory,
getAcmeNewNonce, getAcmeNewNonce,
createAcmeAccount, createAcmeAccount,
@@ -42,6 +42,9 @@ export type TJwsPayload<T> = {
protectedHeader: TProtectedHeader; protectedHeader: TProtectedHeader;
payload: T; payload: T;
}; };
export type TAuthenciatedJwsPayload<T> = TJwsPayload<T> & {
accountId: string;
};
export type TAcmeResponse<TPayload> = { export type TAcmeResponse<TPayload> = {
status: number; status: number;
headers: Record<string, string>; headers: Record<string, string>;
@@ -55,6 +58,11 @@ export type TPkiAcmeServiceFactory = {
schema: z.ZodSchema<T> schema: z.ZodSchema<T>
) => Promise<TJwsPayload<T>>; ) => Promise<TJwsPayload<T>>;
validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>; validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>;
validateExistingAccountJwsPayload: <T>(
profileId: string,
rawJwsPayload: TRawJwsPayload,
schema: z.ZodSchema<T>
) => Promise<TAuthenciatedJwsPayload<T>>;
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>; getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
getAcmeNewNonce: (profileId: string) => Promise<string>; getAcmeNewNonce: (profileId: string) => Promise<string>;
createAcmeAccount: ({ createAcmeAccount: ({
@@ -68,10 +76,15 @@ export type TPkiAcmeServiceFactory = {
jwk: JsonWebKey; jwk: JsonWebKey;
payload: TCreateAcmeAccountPayload; payload: TCreateAcmeAccountPayload;
}) => Promise<TAcmeResponse<TCreateAcmeAccountResponse>>; }) => Promise<TAcmeResponse<TCreateAcmeAccountResponse>>;
createAcmeOrder: ( createAcmeOrder: ({
profileId: string, profileId,
body: TCreateAcmeOrderPayload accountId,
) => Promise<TAcmeResponse<TCreateAcmeOrderResponse>>; payload
}: {
profileId: string;
accountId: string;
payload: TCreateAcmeOrderPayload;
}) => Promise<TAcmeResponse<TCreateAcmeOrderResponse>>;
deactivateAcmeAccount: ( deactivateAcmeAccount: (
profileId: string, profileId: string,
accountId: string, accountId: string,