Add url check

This commit is contained in:
Fang-Pen Lin
2025-10-30 16:46:52 -07:00
parent 4970428f1d
commit 31a70bc781
3 changed files with 73 additions and 31 deletions

View File

@@ -121,7 +121,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req, res) => {
const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload(req.body);
const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload({
url: req.url,
rawJwsPayload: req.body
});
const { alg, jwk } = protectedHeader;
return sendAcmeResponse(
res,
@@ -161,6 +164,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => {
const { payload, profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId,
rawJwsPayload: req.body,
schema: DeactivateAcmeAccountBodySchema,
@@ -202,6 +206,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => {
const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId,
rawJwsPayload: req.body,
schema: CreateAcmeOrderBodySchema
@@ -243,6 +248,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => {
const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId,
rawJwsPayload: req.body
});
@@ -283,6 +289,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => {
const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId,
rawJwsPayload: req.body,
schema: FinalizeAcmeOrderBodySchema
@@ -324,6 +331,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => {
const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId,
rawJwsPayload: req.body,
schema: ListAcmeOrdersPayloadSchema,
@@ -365,6 +373,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => {
const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId,
rawJwsPayload: req.body
});
@@ -401,6 +410,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req, res) => {
const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId,
rawJwsPayload: req.body
});

View File

@@ -19,6 +19,7 @@ import {
AcmeBadPublicKeyError,
AcmeMalformedError,
AcmeServerInternalError,
AcmeUnauthorizedError,
AcmeUnsupportedIdentifierError
} from "./pki-acme-errors";
import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal";
@@ -92,11 +93,17 @@ export const pkiAcmeServiceFactory = ({
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?: TSchema
): Promise<TJwsPayload<T>> => {
>({
url,
rawJwsPayload,
getJWK,
schema
}: {
url: string;
rawJwsPayload: TRawJwsPayload;
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>;
schema?: TSchema;
}): Promise<TJwsPayload<T>> => {
let result: FlattenedVerifyResult;
try {
result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => {
@@ -119,6 +126,9 @@ export const pkiAcmeServiceFactory = ({
const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result;
try {
const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader);
if (protectedHeader.url !== url) {
throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" });
}
// TODO: consume the nonce here
const decoder = new TextDecoder();
const textPayload = decoder.decode(rawPayload);
@@ -136,39 +146,47 @@ export const pkiAcmeServiceFactory = ({
}
};
const validateNewAccountJwsPayload = async (
rawJwsPayload: TRawJwsPayload
): Promise<TJwsPayload<TCreateAcmeAccountPayload>> => {
return await validateJwsPayload(
const validateNewAccountJwsPayload = async ({
url,
rawJwsPayload
}: {
url: string;
rawJwsPayload: TRawJwsPayload;
}): Promise<TJwsPayload<TCreateAcmeAccountPayload>> => {
return await validateJwsPayload({
url,
rawJwsPayload,
async (protectedHeader) => {
getJWK: async (protectedHeader) => {
if (!protectedHeader.jwk) {
throw new AcmeMalformedError({ detail: "JWK is required in the protected header" });
}
return protectedHeader.jwk as unknown as JsonWebKey;
},
CreateAcmeAccountBodySchema
);
schema: CreateAcmeAccountBodySchema
});
};
const validateExistingAccountJwsPayload = async <
TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string
>({
url,
profileId,
rawJwsPayload,
schema,
expectedAccountId
}: {
url: string;
profileId: string;
rawJwsPayload: TRawJwsPayload;
schema?: TSchema;
expectedAccountId?: string;
}): Promise<TAuthenciatedJwsPayload<T>> => {
const profile = await validateAcmeProfile(profileId);
const result = await validateJwsPayload(
const result = await validateJwsPayload({
url,
rawJwsPayload,
async (protectedHeader) => {
getJWK: async (protectedHeader) => {
if (!protectedHeader.kid) {
throw new AcmeMalformedError({ detail: "KID is required in the protected header" });
}
@@ -186,7 +204,7 @@ export const pkiAcmeServiceFactory = ({
return account.publicKey as JsonWebKey;
},
schema
);
});
return {
...result,
accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId),
@@ -194,15 +212,6 @@ export const pkiAcmeServiceFactory = ({
};
};
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
const profile = await validateAcmeProfile(profileId);
return {
newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-nonce`),
newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-account`),
newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-order`)
};
};
const buildAcmeOrderResource = ({
profileId,
order
@@ -233,6 +242,15 @@ export const pkiAcmeServiceFactory = ({
};
};
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
const profile = await validateAcmeProfile(profileId);
return {
newNonce: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-nonce`),
newAccount: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-account`),
newOrder: buildUrl(`/api/v1/pki/acme/profiles/${profile.id}/new-order`)
};
};
const getAcmeNewNonce = async (profileId: string): Promise<string> => {
const profile = await validateAcmeProfile(profileId);
// FIXME: Implement ACME new nonce generation

View File

@@ -52,21 +52,35 @@ export type TPkiAcmeServiceFactory = {
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?: TSchema
) => Promise<TJwsPayload<T>>;
validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>;
>({
url,
rawJwsPayload,
getJWK,
schema
}: {
url: string;
rawJwsPayload: TRawJwsPayload;
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>;
schema?: z.ZodSchema<any>;
}) => Promise<TJwsPayload<any>>;
validateNewAccountJwsPayload: ({
url,
rawJwsPayload
}: {
url: string;
rawJwsPayload: TRawJwsPayload;
}) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>;
validateExistingAccountJwsPayload: <
TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string
>({
url,
profileId,
rawJwsPayload,
schema,
expectedAccountId
}: {
url: string;
profileId: string;
rawJwsPayload: TRawJwsPayload;
schema?: TSchema;