More for validate jws payload

This commit is contained in:
Fang-Pen Lin
2025-11-07 09:18:06 -08:00
parent efabf9ee05
commit f06f6108f4
2 changed files with 28 additions and 19 deletions
@@ -1,12 +1,17 @@
import { z } from "zod"; import { z } from "zod";
export const ProtectedHeaderSchema = z.object({ export const ProtectedHeaderSchema = z
alg: z.string(), .object({
nonce: z.string(), alg: z.string(),
url: z.string(), nonce: z.string(),
kid: z.string().optional(), url: z.string(),
jwk: z.record(z.string(), z.string()).optional() kid: z.string().optional(),
}); jwk: z.record(z.string(), z.string()).optional()
})
.refine((data) => data.kid || data.jwk, {
message: "Either kid or jwk must be provided",
path: ["kid", "jwk"]
});
// Raw JWS payload schema before parsing and verification // Raw JWS payload schema before parsing and verification
export const RawJwsPayloadSchema = z.object({ export const RawJwsPayloadSchema = z.object({
@@ -1,14 +1,16 @@
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
import { AcmeMalformedError, AcmeBadPublicKeyError } from "./pki-acme-errors"; import { AcmeBadPublicKeyError, AcmeMalformedError } from "./pki-acme-errors";
import { import {
EnrollmentType, EnrollmentType,
TCertificateProfileWithConfigs TCertificateProfileWithConfigs
} from "@app/services/certificate-profile/certificate-profile-types"; } from "@app/services/certificate-profile/certificate-profile-types";
import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose";
import { ProtectedHeaderSchema } from "./pki-acme-schemas";
import { import {
TCreateAcmeAccountPayload, TCreateAcmeAccountPayload,
TCreateAcmeAccountResponse, TCreateAcmeAccountResponse,
@@ -21,14 +23,12 @@ import {
TGetAcmeAuthorizationResponse, TGetAcmeAuthorizationResponse,
TGetAcmeDirectoryResponse, TGetAcmeDirectoryResponse,
TGetAcmeOrderResponse, TGetAcmeOrderResponse,
TRawJwsPayload, TJwsPayload,
TListAcmeOrdersResponse, TListAcmeOrdersResponse,
TPkiAcmeServiceFactory, TPkiAcmeServiceFactory,
TRespondToAcmeChallengeResponse, TRawJwsPayload,
TJwsPayload, TRespondToAcmeChallengeResponse
TProtectedHeader
} from "./pki-acme-types"; } from "./pki-acme-types";
import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose";
type TPkiAcmeServiceFactoryDep = { type TPkiAcmeServiceFactoryDep = {
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">; certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
@@ -52,9 +52,9 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
return `${baseUrl}${path}`; return `${baseUrl}${path}`;
}; };
const validateCreateAcmeAccountJwsPayload = async (rawPayload: TRawJwsPayload): Promise<TJwsPayload> => { const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise<TJwsPayload> => {
const { payload, protectedHeader } = await flattenedVerify( const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify(
rawPayload, rawJwsPayload,
async (protectedHeader: JWSHeaderParameters | undefined) => { async (protectedHeader: JWSHeaderParameters | undefined) => {
if (protectedHeader === undefined) { if (protectedHeader === undefined) {
throw new AcmeMalformedError({ detail: "Protected header is required" }); throw new AcmeMalformedError({ detail: "Protected header is required" });
@@ -68,10 +68,14 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
return imported; return imported;
} }
); );
const { success, data: protectedHeader } = ProtectedHeaderSchema.safeParse(rawProtectedHeader);
if (!success) {
throw new AcmeMalformedError({ detail: "Invalid protected header" });
}
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const parsedPayload = JSON.parse(decoder.decode(payload)) as TCreateAcmeAccountPayload; const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload;
// TODO: also consume the nonce here // TODO: also consume the nonce here
return { payload: parsedPayload, protectedHeader: protectedHeader as TProtectedHeader }; return { payload, protectedHeader };
}; };
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => { const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {