Add url check

This commit is contained in:
Fang-Pen Lin
2025-11-07 09:18:58 -08:00
parent 4970428f1d
commit 31a70bc781
3 changed files with 73 additions and 31 deletions
+11 -1
View File
@@ -121,7 +121,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req, res) => { 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; const { alg, jwk } = protectedHeader;
return sendAcmeResponse( return sendAcmeResponse(
res, res,
@@ -161,6 +164,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// 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({ const { payload, profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId, profileId: req.params.profileId,
rawJwsPayload: req.body, rawJwsPayload: req.body,
schema: DeactivateAcmeAccountBodySchema, schema: DeactivateAcmeAccountBodySchema,
@@ -202,6 +206,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// 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 { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId, profileId: req.params.profileId,
rawJwsPayload: req.body, rawJwsPayload: req.body,
schema: CreateAcmeOrderBodySchema schema: CreateAcmeOrderBodySchema
@@ -243,6 +248,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// 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 { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId, profileId: req.params.profileId,
rawJwsPayload: req.body rawJwsPayload: req.body
}); });
@@ -283,6 +289,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// 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 { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId, profileId: req.params.profileId,
rawJwsPayload: req.body, rawJwsPayload: req.body,
schema: FinalizeAcmeOrderBodySchema schema: FinalizeAcmeOrderBodySchema
@@ -324,6 +331,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// 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 { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId, profileId: req.params.profileId,
rawJwsPayload: req.body, rawJwsPayload: req.body,
schema: ListAcmeOrdersPayloadSchema, schema: ListAcmeOrdersPayloadSchema,
@@ -365,6 +373,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// 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 { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId, profileId: req.params.profileId,
rawJwsPayload: req.body rawJwsPayload: req.body
}); });
@@ -401,6 +410,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
// 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 { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({ const { profileId, accountId, payload } = await server.services.pkiAcme.validateExistingAccountJwsPayload({
url: req.url,
profileId: req.params.profileId, profileId: req.params.profileId,
rawJwsPayload: req.body rawJwsPayload: req.body
}); });
@@ -19,6 +19,7 @@ import {
AcmeBadPublicKeyError, AcmeBadPublicKeyError,
AcmeMalformedError, AcmeMalformedError,
AcmeServerInternalError, AcmeServerInternalError,
AcmeUnauthorizedError,
AcmeUnsupportedIdentifierError AcmeUnsupportedIdentifierError
} from "./pki-acme-errors"; } from "./pki-acme-errors";
import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal";
@@ -92,11 +93,17 @@ export const pkiAcmeServiceFactory = ({
const validateJwsPayload = async < const validateJwsPayload = async <
TSchema extends z.ZodSchema<any> | undefined = undefined, TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string T = TSchema extends z.ZodSchema<infer R> ? R : string
>( >({
rawJwsPayload: TRawJwsPayload, url,
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>, rawJwsPayload,
schema?: TSchema getJWK,
): Promise<TJwsPayload<T>> => { schema
}: {
url: string;
rawJwsPayload: TRawJwsPayload;
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>;
schema?: TSchema;
}): Promise<TJwsPayload<T>> => {
let result: FlattenedVerifyResult; let result: FlattenedVerifyResult;
try { try {
result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => { result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => {
@@ -119,6 +126,9 @@ export const pkiAcmeServiceFactory = ({
const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result;
try { try {
const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader);
if (protectedHeader.url !== url) {
throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" });
}
// TODO: consume the nonce here // TODO: consume the nonce here
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const textPayload = decoder.decode(rawPayload); const textPayload = decoder.decode(rawPayload);
@@ -136,39 +146,47 @@ export const pkiAcmeServiceFactory = ({
} }
}; };
const validateNewAccountJwsPayload = async ( const validateNewAccountJwsPayload = async ({
rawJwsPayload: TRawJwsPayload url,
): Promise<TJwsPayload<TCreateAcmeAccountPayload>> => { rawJwsPayload
return await validateJwsPayload( }: {
url: string;
rawJwsPayload: TRawJwsPayload;
}): Promise<TJwsPayload<TCreateAcmeAccountPayload>> => {
return await validateJwsPayload({
url,
rawJwsPayload, rawJwsPayload,
async (protectedHeader) => { getJWK: async (protectedHeader) => {
if (!protectedHeader.jwk) { if (!protectedHeader.jwk) {
throw new AcmeMalformedError({ 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;
}, },
CreateAcmeAccountBodySchema schema: CreateAcmeAccountBodySchema
); });
}; };
const validateExistingAccountJwsPayload = async < const validateExistingAccountJwsPayload = async <
TSchema extends z.ZodSchema<any> | undefined = undefined, TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string T = TSchema extends z.ZodSchema<infer R> ? R : string
>({ >({
url,
profileId, profileId,
rawJwsPayload, rawJwsPayload,
schema, schema,
expectedAccountId expectedAccountId
}: { }: {
url: string;
profileId: string; profileId: string;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
schema?: TSchema; schema?: TSchema;
expectedAccountId?: string; expectedAccountId?: string;
}): Promise<TAuthenciatedJwsPayload<T>> => { }): Promise<TAuthenciatedJwsPayload<T>> => {
const profile = await validateAcmeProfile(profileId); const profile = await validateAcmeProfile(profileId);
const result = await validateJwsPayload( const result = await validateJwsPayload({
url,
rawJwsPayload, rawJwsPayload,
async (protectedHeader) => { getJWK: async (protectedHeader) => {
if (!protectedHeader.kid) { if (!protectedHeader.kid) {
throw new AcmeMalformedError({ detail: "KID is required in the protected header" }); throw new AcmeMalformedError({ detail: "KID is required in the protected header" });
} }
@@ -186,7 +204,7 @@ export const pkiAcmeServiceFactory = ({
return account.publicKey as JsonWebKey; return account.publicKey as JsonWebKey;
}, },
schema schema
); });
return { return {
...result, ...result,
accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId), 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 = ({ const buildAcmeOrderResource = ({
profileId, profileId,
order 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 getAcmeNewNonce = async (profileId: string): Promise<string> => {
const profile = await validateAcmeProfile(profileId); const profile = await validateAcmeProfile(profileId);
// FIXME: Implement ACME new nonce generation // FIXME: Implement ACME new nonce generation
@@ -52,21 +52,35 @@ export type TPkiAcmeServiceFactory = {
validateJwsPayload: < validateJwsPayload: <
TSchema extends z.ZodSchema<any> | undefined = undefined, TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string T = TSchema extends z.ZodSchema<infer R> ? R : string
>( >({
rawJwsPayload: TRawJwsPayload, url,
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>, rawJwsPayload,
schema?: TSchema getJWK,
) => Promise<TJwsPayload<T>>; schema
validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>; }: {
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: < validateExistingAccountJwsPayload: <
TSchema extends z.ZodSchema<any> | undefined = undefined, TSchema extends z.ZodSchema<any> | undefined = undefined,
T = TSchema extends z.ZodSchema<infer R> ? R : string T = TSchema extends z.ZodSchema<infer R> ? R : string
>({ >({
url,
profileId, profileId,
rawJwsPayload, rawJwsPayload,
schema, schema,
expectedAccountId expectedAccountId
}: { }: {
url: string;
profileId: string; profileId: string;
rawJwsPayload: TRawJwsPayload; rawJwsPayload: TRawJwsPayload;
schema?: TSchema; schema?: TSchema;