Implement more stuff

This commit is contained in:
Fang-Pen Lin
2025-10-29 20:57:18 -07:00
parent c6f51cbd10
commit 4707585294
3 changed files with 138 additions and 41 deletions

View File

@@ -10,15 +10,18 @@ import {
DeactivateAcmeAccountBodySchema,
DeactivateAcmeAccountResponseSchema,
FinalizeAcmeOrderBodySchema,
FinalizeAcmeOrderResponseSchema,
GetAcmeAuthorizationResponseSchema,
GetAcmeDirectoryResponseSchema,
GetAcmeOrderResponseSchema,
ListAcmeOrdersPayloadSchema,
ListAcmeOrdersResponseSchema,
RawJwsPayloadSchema,
RespondToAcmeChallengeResponseSchema
} from "@app/ee/services/pki-acme/pki-acme-schemas";
import { ApiDocsTags } from "@app/lib/api-docs";
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) => {
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
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => {
const { payload, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload(
req.params.profileId,
req.body,
CreateAcmeOrderBodySchema
);
const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
profileId: req.params.profileId,
rawJwsPayload: req.body,
schema: CreateAcmeOrderBodySchema
});
return sendAcmeResponse(
res,
req.params.profileId,
profileId,
await server.services.pkiAcme.createAcmeOrder({
profileId: req.params.profileId,
profileId,
accountId,
payload
})
@@ -191,7 +194,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
profileId: z.string().uuid(),
accountId: z.string()
}),
body: DeactivateAcmeAccountBodySchema,
body: RawJwsPayloadSchema,
response: {
200: DeactivateAcmeAccountResponseSchema
}
@@ -199,13 +202,19 @@ 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, profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
profileId: req.params.profileId,
rawJwsPayload: req.body,
schema: DeactivateAcmeAccountBodySchema,
expectedAccountId: req.params.accountId
});
return sendAcmeResponse(
res,
req.params.profileId,
profileId,
await server.services.pkiAcme.deactivateAcmeAccount({
profileId: req.params.profileId,
accountId: req.params.accountId,
payload: req.body
profileId,
accountId,
payload
})
);
}
@@ -227,15 +236,28 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
profileId: z.string().uuid(),
accountId: z.string()
}),
body: RawJwsPayloadSchema,
response: {
200: ListAcmeOrdersResponseSchema
}
},
// TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const orders = await server.services.pkiAcme.listAcmeOrders(req.params.profileId, req.params.accountId);
return orders;
handler: async (req, res) => {
const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
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(),
orderId: z.string().uuid()
}),
body: RawJwsPayloadSchema,
response: {
200: GetAcmeOrderResponseSchema
}
},
// TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const order = await server.services.pkiAcme.getAcmeOrder(req.params.profileId, req.params.orderId);
return order;
handler: async (req, res) => {
const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
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(),
orderId: z.string().uuid()
}),
body: FinalizeAcmeOrderBodySchema
body: RawJwsPayloadSchema,
response: {
200: FinalizeAcmeOrderResponseSchema
}
},
// TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const order = await server.services.pkiAcme.finalizeAcmeOrder(req.params.profileId, req.params.orderId, req.body);
return order;
handler: async (req, res) => {
const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
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(),
orderId: z.string().uuid()
}),
body: RawJwsPayloadSchema,
response: {
200: z.string()
}
@@ -316,12 +367,19 @@ 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 certificate = await server.services.pkiAcme.downloadAcmeCertificate(
req.params.profileId,
req.params.orderId
const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
profileId: req.params.profileId,
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(),
authzId: z.string().uuid()
}),
body: RawJwsPayloadSchema,
response: {
200: GetAcmeAuthorizationResponseSchema
}
},
// TODO: replace with verify ACME signature here instead
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const authz = await server.services.pkiAcme.getAcmeAuthorization(req.params.profileId, req.params.authzId);
return authz;
handler: async (req, res) => {
const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
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
})
);
}
});

View File

@@ -93,10 +93,13 @@ export const pkiAcmeServiceFactory = ({
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,
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>,
schema: z.ZodSchema<T>
schema?: TSchema
): Promise<TJwsPayload<T>> => {
let result: FlattenedVerifyResult;
try {
@@ -122,8 +125,8 @@ export const pkiAcmeServiceFactory = ({
const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader);
// TODO: consume the nonce here
const decoder = new TextDecoder();
const jsonPayload = JSON.parse(decoder.decode(rawPayload));
const payload = schema.parse(jsonPayload);
const textPayload = decoder.decode(rawPayload);
const payload = schema ? schema.parse(JSON.parse(textPayload)) : textPayload;
return {
protectedHeader,
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,
rawJwsPayload,
schema,
@@ -160,7 +166,7 @@ export const pkiAcmeServiceFactory = ({
}: {
profileId: string;
rawJwsPayload: TRawJwsPayload;
schema: z.ZodSchema<T>;
schema?: TSchema;
expectedAccountId?: string;
}): Promise<TAuthenciatedJwsPayload<T>> => {
const profile = await validateAcmeProfile(profileId);
@@ -187,7 +193,8 @@ export const pkiAcmeServiceFactory = ({
);
return {
...result,
accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId)
accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId),
profileId
};
};
@@ -406,10 +413,12 @@ export const pkiAcmeServiceFactory = ({
const finalizeAcmeOrder = async ({
profileId,
accountId,
orderId,
payload
}: {
profileId: string;
accountId: string;
orderId: string;
payload: TFinalizeAcmeOrderPayload;
}): Promise<TAcmeResponse<TFinalizeAcmeOrderResponse>> => {
@@ -453,12 +462,18 @@ export const pkiAcmeServiceFactory = ({
const getAcmeAuthorization = async ({
profileId,
accountId,
authzId
}: {
profileId: string;
accountId: string;
authzId: string;
}): Promise<TAcmeResponse<TGetAcmeAuthorizationResponse>> => {
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
return {
status: 200,

View File

@@ -43,6 +43,7 @@ export type TJwsPayload<T> = {
payload: T;
};
export type TAuthenciatedJwsPayload<T> = TJwsPayload<T> & {
profileId: string;
accountId: string;
};
export type TAcmeResponse<TPayload> = {
@@ -52,13 +53,19 @@ export type TAcmeResponse<TPayload> = {
};
export type TPkiAcmeServiceFactory = {
validateJwsPayload: <T>(
validateJwsPayload: <
TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string
>(
rawJwsPayload: TRawJwsPayload,
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>,
schema: z.ZodSchema<T>
schema?: TSchema
) => Promise<TJwsPayload<T>>;
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,
rawJwsPayload,
schema,
@@ -66,7 +73,7 @@ export type TPkiAcmeServiceFactory = {
}: {
profileId: string;
rawJwsPayload: TRawJwsPayload;
schema: z.ZodSchema<T>;
schema?: TSchema;
expectedAccountId?: string;
}) => Promise<TAuthenciatedJwsPayload<T>>;
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
@@ -118,10 +125,12 @@ export type TPkiAcmeServiceFactory = {
}) => Promise<TAcmeResponse<TGetAcmeOrderResponse>>;
finalizeAcmeOrder: ({
profileId,
accountId,
orderId,
payload
}: {
profileId: string;
accountId: string;
orderId: string;
payload: TFinalizeAcmeOrderPayload;
}) => Promise<TAcmeResponse<TFinalizeAcmeOrderResponse>>;