Implement more stuff

This commit is contained in:
Fang-Pen Lin
2025-11-07 09:18:35 -08:00
parent c6f51cbd10
commit 4707585294
3 changed files with 138 additions and 41 deletions
+103 -30
View File
@@ -10,15 +10,18 @@ import {
DeactivateAcmeAccountBodySchema, DeactivateAcmeAccountBodySchema,
DeactivateAcmeAccountResponseSchema, DeactivateAcmeAccountResponseSchema,
FinalizeAcmeOrderBodySchema, FinalizeAcmeOrderBodySchema,
FinalizeAcmeOrderResponseSchema,
GetAcmeAuthorizationResponseSchema, GetAcmeAuthorizationResponseSchema,
GetAcmeDirectoryResponseSchema, GetAcmeDirectoryResponseSchema,
GetAcmeOrderResponseSchema, GetAcmeOrderResponseSchema,
ListAcmeOrdersPayloadSchema,
ListAcmeOrdersResponseSchema, ListAcmeOrdersResponseSchema,
RawJwsPayloadSchema, RawJwsPayloadSchema,
RespondToAcmeChallengeResponseSchema RespondToAcmeChallengeResponseSchema
} from "@app/ee/services/pki-acme/pki-acme-schemas"; } from "@app/ee/services/pki-acme/pki-acme-schemas";
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";
import { AcmeAccountDoesNotExistError, AcmeMalformedError } from "@app/ee/services/pki-acme/pki-acme-errors";
export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
const sendAcmeResponse = async <T>(res: FastifyReply, profileId: string, response: TAcmeResponse<T>): Promise<T> => { const sendAcmeResponse = async <T>(res: FastifyReply, profileId: string, response: TAcmeResponse<T>): Promise<T> => {
@@ -158,16 +161,16 @@ 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, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload( const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
req.params.profileId, profileId: req.params.profileId,
req.body, rawJwsPayload: req.body,
CreateAcmeOrderBodySchema schema: CreateAcmeOrderBodySchema
); });
return sendAcmeResponse( return sendAcmeResponse(
res, res,
req.params.profileId, profileId,
await server.services.pkiAcme.createAcmeOrder({ await server.services.pkiAcme.createAcmeOrder({
profileId: req.params.profileId, profileId,
accountId, accountId,
payload payload
}) })
@@ -191,7 +194,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
profileId: z.string().uuid(), profileId: z.string().uuid(),
accountId: z.string() accountId: z.string()
}), }),
body: DeactivateAcmeAccountBodySchema, body: RawJwsPayloadSchema,
response: { response: {
200: DeactivateAcmeAccountResponseSchema 200: DeactivateAcmeAccountResponseSchema
} }
@@ -199,13 +202,19 @@ 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, profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
profileId: req.params.profileId,
rawJwsPayload: req.body,
schema: DeactivateAcmeAccountBodySchema,
expectedAccountId: req.params.accountId
});
return sendAcmeResponse( return sendAcmeResponse(
res, res,
req.params.profileId, profileId,
await server.services.pkiAcme.deactivateAcmeAccount({ await server.services.pkiAcme.deactivateAcmeAccount({
profileId: req.params.profileId, profileId,
accountId: req.params.accountId, accountId,
payload: req.body payload
}) })
); );
} }
@@ -227,15 +236,28 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
profileId: z.string().uuid(), profileId: z.string().uuid(),
accountId: z.string() accountId: z.string()
}), }),
body: RawJwsPayloadSchema,
response: { response: {
200: ListAcmeOrdersResponseSchema 200: ListAcmeOrdersResponseSchema
} }
}, },
// 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) => { handler: async (req, res) => {
const orders = await server.services.pkiAcme.listAcmeOrders(req.params.profileId, req.params.accountId); const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
return orders; profileId: req.params.profileId,
rawJwsPayload: req.body,
schema: ListAcmeOrdersPayloadSchema,
expectedAccountId: req.params.accountId
});
return sendAcmeResponse(
res,
profileId,
await server.services.pkiAcme.listAcmeOrders({
profileId,
accountId
})
);
} }
}); });
@@ -255,15 +277,27 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
profileId: z.string().uuid(), profileId: z.string().uuid(),
orderId: z.string().uuid() orderId: z.string().uuid()
}), }),
body: RawJwsPayloadSchema,
response: { response: {
200: GetAcmeOrderResponseSchema 200: GetAcmeOrderResponseSchema
} }
}, },
// 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) => { handler: async (req, res) => {
const order = await server.services.pkiAcme.getAcmeOrder(req.params.profileId, req.params.orderId); const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
return order; profileId: req.params.profileId,
rawJwsPayload: req.body
});
return sendAcmeResponse(
res,
profileId,
await server.services.pkiAcme.getAcmeOrder({
profileId,
accountId,
orderId: req.params.orderId
})
);
} }
}); });
@@ -283,13 +317,29 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
profileId: z.string().uuid(), profileId: z.string().uuid(),
orderId: z.string().uuid() orderId: z.string().uuid()
}), }),
body: FinalizeAcmeOrderBodySchema body: RawJwsPayloadSchema,
response: {
200: FinalizeAcmeOrderResponseSchema
}
}, },
// 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) => { handler: async (req, res) => {
const order = await server.services.pkiAcme.finalizeAcmeOrder(req.params.profileId, req.params.orderId, req.body); const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
return order; profileId: req.params.profileId,
rawJwsPayload: req.body
schema: FinalizeAcmeOrderBodySchema,
});
return sendAcmeResponse(
res,
profileId,
await server.services.pkiAcme.finalizeAcmeOrder({
profileId,
accountId,
orderId: req.params.orderId,
payload
})
);
} }
}); });
@@ -309,6 +359,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
profileId: z.string().uuid(), profileId: z.string().uuid(),
orderId: z.string().uuid() orderId: z.string().uuid()
}), }),
body: RawJwsPayloadSchema,
response: { response: {
200: z.string() 200: z.string()
} }
@@ -316,12 +367,19 @@ 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 certificate = await server.services.pkiAcme.downloadAcmeCertificate( const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
req.params.profileId, profileId: req.params.profileId,
req.params.orderId rawJwsPayload: req.body
});
return sendAcmeResponse(
res,
profileId,
await server.services.pkiAcme.downloadAcmeCertificate({
profileId,
accountId,
orderId: req.params.orderId
})
); );
res.header("Content-Type", "application/pem-certificate-chain");
return certificate;
} }
}); });
@@ -341,15 +399,30 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
profileId: z.string().uuid(), profileId: z.string().uuid(),
authzId: z.string().uuid() authzId: z.string().uuid()
}), }),
body: RawJwsPayloadSchema,
response: { response: {
200: GetAcmeAuthorizationResponseSchema 200: GetAcmeAuthorizationResponseSchema
} }
}, },
// 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) => { handler: async (req, res) => {
const authz = await server.services.pkiAcme.getAcmeAuthorization(req.params.profileId, req.params.authzId); const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
return authz; profileId: req.params.profileId,
rawJwsPayload: req.body
});
if (payload !== "") {
throw new AcmeMalformedError({ detail: "Payload should be empty" });
}
return sendAcmeResponse(
res,
profileId,
await server.services.pkiAcme.getAcmeAuthorization({
profileId,
accountId,
authzId: req.params.authzId
})
);
} }
}); });
@@ -93,10 +93,13 @@ export const pkiAcmeServiceFactory = ({
return kid.slice(kidPrefix.length); return kid.slice(kidPrefix.length);
}; };
const validateJwsPayload = async <T>( const validateJwsPayload = async <
TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string
>(
rawJwsPayload: TRawJwsPayload, rawJwsPayload: TRawJwsPayload,
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>, getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>,
schema: z.ZodSchema<T> schema?: TSchema
): Promise<TJwsPayload<T>> => { ): Promise<TJwsPayload<T>> => {
let result: FlattenedVerifyResult; let result: FlattenedVerifyResult;
try { try {
@@ -122,8 +125,8 @@ export const pkiAcmeServiceFactory = ({
const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader);
// TODO: consume the nonce here // TODO: consume the nonce here
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const jsonPayload = JSON.parse(decoder.decode(rawPayload)); const textPayload = decoder.decode(rawPayload);
const payload = schema.parse(jsonPayload); const payload = schema ? schema.parse(JSON.parse(textPayload)) : textPayload;
return { return {
protectedHeader, protectedHeader,
payload payload
@@ -152,7 +155,10 @@ export const pkiAcmeServiceFactory = ({
); );
}; };
const validateExistingAccountJwsPayload = async <T>({ const validateExistingAccountJwsPayload = async <
TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string
>({
profileId, profileId,
rawJwsPayload, rawJwsPayload,
schema, schema,
@@ -160,7 +166,7 @@ export const pkiAcmeServiceFactory = ({
}: { }: {
profileId: string; profileId: string;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
schema: z.ZodSchema<T>; schema?: TSchema;
expectedAccountId?: string; expectedAccountId?: string;
}): Promise<TAuthenciatedJwsPayload<T>> => { }): Promise<TAuthenciatedJwsPayload<T>> => {
const profile = await validateAcmeProfile(profileId); const profile = await validateAcmeProfile(profileId);
@@ -187,7 +193,8 @@ export const pkiAcmeServiceFactory = ({
); );
return { return {
...result, ...result,
accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId) accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId),
profileId
}; };
}; };
@@ -406,10 +413,12 @@ export const pkiAcmeServiceFactory = ({
const finalizeAcmeOrder = async ({ const finalizeAcmeOrder = async ({
profileId, profileId,
accountId,
orderId, orderId,
payload payload
}: { }: {
profileId: string; profileId: string;
accountId: string;
orderId: string; orderId: string;
payload: TFinalizeAcmeOrderPayload; payload: TFinalizeAcmeOrderPayload;
}): Promise<TAcmeResponse<TFinalizeAcmeOrderResponse>> => { }): Promise<TAcmeResponse<TFinalizeAcmeOrderResponse>> => {
@@ -453,12 +462,18 @@ export const pkiAcmeServiceFactory = ({
const getAcmeAuthorization = async ({ const getAcmeAuthorization = async ({
profileId, profileId,
accountId,
authzId authzId
}: { }: {
profileId: string; profileId: string;
accountId: string;
authzId: string; authzId: string;
}): Promise<TAcmeResponse<TGetAcmeAuthorizationResponse>> => { }): Promise<TAcmeResponse<TGetAcmeAuthorizationResponse>> => {
const profile = await validateAcmeProfile(profileId); const profile = await validateAcmeProfile(profileId);
const order = await acmeOrderDAL.findByIdWithAuthorizations(orderId);
if (!order || order.accountId !== accountId) {
throw new NotFoundError({ message: "ACME order not found" });
}
// FIXME: Implement ACME authorization retrieval // FIXME: Implement ACME authorization retrieval
return { return {
status: 200, status: 200,
@@ -43,6 +43,7 @@ export type TJwsPayload<T> = {
payload: T; payload: T;
}; };
export type TAuthenciatedJwsPayload<T> = TJwsPayload<T> & { export type TAuthenciatedJwsPayload<T> = TJwsPayload<T> & {
profileId: string;
accountId: string; accountId: string;
}; };
export type TAcmeResponse<TPayload> = { export type TAcmeResponse<TPayload> = {
@@ -52,13 +53,19 @@ export type TAcmeResponse<TPayload> = {
}; };
export type TPkiAcmeServiceFactory = { export type TPkiAcmeServiceFactory = {
validateJwsPayload: <T>( validateJwsPayload: <
TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string
>(
rawJwsPayload: TRawJwsPayload, rawJwsPayload: TRawJwsPayload,
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>, getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>,
schema: z.ZodSchema<T> schema?: TSchema
) => Promise<TJwsPayload<T>>; ) => Promise<TJwsPayload<T>>;
validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>; validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>;
validateExistingAccountJwsPayload: <T>({ validateExistingAccountJwsPayload: <
TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string
>({
profileId, profileId,
rawJwsPayload, rawJwsPayload,
schema, schema,
@@ -66,7 +73,7 @@ export type TPkiAcmeServiceFactory = {
}: { }: {
profileId: string; profileId: string;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
schema: z.ZodSchema<T>; schema?: TSchema;
expectedAccountId?: string; expectedAccountId?: string;
}) => Promise<TAuthenciatedJwsPayload<T>>; }) => Promise<TAuthenciatedJwsPayload<T>>;
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>; getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
@@ -118,10 +125,12 @@ export type TPkiAcmeServiceFactory = {
}) => Promise<TAcmeResponse<TGetAcmeOrderResponse>>; }) => Promise<TAcmeResponse<TGetAcmeOrderResponse>>;
finalizeAcmeOrder: ({ finalizeAcmeOrder: ({
profileId, profileId,
accountId,
orderId, orderId,
payload payload
}: { }: {
profileId: string; profileId: string;
accountId: string;
orderId: string; orderId: string;
payload: TFinalizeAcmeOrderPayload; payload: TFinalizeAcmeOrderPayload;
}) => Promise<TAcmeResponse<TFinalizeAcmeOrderResponse>>; }) => Promise<TAcmeResponse<TFinalizeAcmeOrderResponse>>;