Work on order

This commit is contained in:
Fang-Pen Lin
2025-10-28 20:13:38 -07:00
parent ac82e8071d
commit 2b8a6b801d
5 changed files with 100 additions and 84 deletions

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,
# 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)

View File

@@ -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;

View File

@@ -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
};
};

View File

@@ -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<TCertificateProfileDALFactory, "findById">;
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findByPublicKey" | "create">;
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById" | "findByPublicKey" | "create">;
acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create">;
};
@@ -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 <T>(
rawJwsPayload: TRawJwsPayload,
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>,
@@ -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 <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> => {
await validateAcmeProfile(profileId);
return {
@@ -195,13 +234,25 @@ export const pkiAcmeServiceFactory = ({
};
};
const createAcmeOrder = async (
profileId: string,
account: TPkiAcmeAccounts,
payload: TCreateAcmeOrderPayload
): Promise<TAcmeResponse<TCreateAcmeOrderResponse>> => {
const profile = await validateAcmeProfile(profileId);
const createAcmeOrder = async ({
profileId,
accountId,
payload
}: {
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
const orderId = "FIXME-order-id";
return {
@@ -315,6 +366,7 @@ export const pkiAcmeServiceFactory = ({
return {
validateJwsPayload,
validateNewAccountJwsPayload,
validateExistingAccountJwsPayload,
getAcmeDirectory,
getAcmeNewNonce,
createAcmeAccount,

View File

@@ -42,6 +42,9 @@ export type TJwsPayload<T> = {
protectedHeader: TProtectedHeader;
payload: T;
};
export type TAuthenciatedJwsPayload<T> = TJwsPayload<T> & {
accountId: string;
};
export type TAcmeResponse<TPayload> = {
status: number;
headers: Record<string, string>;
@@ -55,6 +58,11 @@ export type TPkiAcmeServiceFactory = {
schema: z.ZodSchema<T>
) => Promise<TJwsPayload<T>>;
validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>;
validateExistingAccountJwsPayload: <T>(
profileId: string,
rawJwsPayload: TRawJwsPayload,
schema: z.ZodSchema<T>
) => Promise<TAuthenciatedJwsPayload<T>>;
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
getAcmeNewNonce: (profileId: string) => Promise<string>;
createAcmeAccount: ({
@@ -68,10 +76,15 @@ export type TPkiAcmeServiceFactory = {
jwk: JsonWebKey;
payload: TCreateAcmeAccountPayload;
}) => Promise<TAcmeResponse<TCreateAcmeAccountResponse>>;
createAcmeOrder: (
profileId: string,
body: TCreateAcmeOrderPayload
) => Promise<TAcmeResponse<TCreateAcmeOrderResponse>>;
createAcmeOrder: ({
profileId,
accountId,
payload
}: {
profileId: string;
accountId: string;
payload: TCreateAcmeOrderPayload;
}) => Promise<TAcmeResponse<TCreateAcmeOrderResponse>>;
deactivateAcmeAccount: (
profileId: string,
accountId: string,