From 03077d54afc49613db4e38f70c506bc60f5223aa Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:29:37 +0800 Subject: [PATCH] fix(pe): separate certificate content from counted alignment --- docs/pe-certificates.md | 5 +++ src/native/pe/PeCertificateAsn1.ts | 4 +-- src/native/pe/PeCertificateParser.test.ts | 41 +++++++++++++++++++++++ src/native/pe/PeCertificateParser.ts | 12 ++++++- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/pe-certificates.md b/docs/pe-certificates.md index 9875e19f91..0987cee27d 100644 --- a/docs/pe-certificates.md +++ b/docs/pe-certificates.md @@ -28,6 +28,11 @@ The inline Evidence result contains the artifact path, SHA-256, and byte length. absent or zero. Each `entries[]` item reports `WIN_CERTIFICATE` length, revision, type, and the payload SHA-256. +Type-2 entries may count their zero alignment padding in `dwLength`. The PE +entry owner accepts only the exact gap to the next eight-byte boundary after +the parsed ContentInfo; extra data or nonzero counted padding fails. Entry and +payload ranges and hashes retain all declared bytes, including counted padding. + For `WIN_CERTIFICATE` type 2 (PKCS#7 signedData) `pkcs7` contains digest algorithm OIDs, embedded X.509 serial/issuer/subject/validity fields, and signer identifier facts. A signer identified by subject key identifier reports diff --git a/src/native/pe/PeCertificateAsn1.ts b/src/native/pe/PeCertificateAsn1.ts index b104ccc7b4..4b3125e769 100644 --- a/src/native/pe/PeCertificateAsn1.ts +++ b/src/native/pe/PeCertificateAsn1.ts @@ -569,9 +569,7 @@ const parseSignedData = ( }; /** Decode ContentInfo wrapping SignedData; other content types remain unsupported. */ -export const parsePkcs7SignedData = (bytes: Buffer): ParsedSignedData => { - const root = readAsn1(bytes); - if (root.end !== bytes.length) return fail("ContentInfo has trailing data."); +export const parsePkcs7SignedData = (root: Asn1Node): ParsedSignedData => { const children = sequence(root, "ContentInfo"); const type = children[0]; const content = children[1]; diff --git a/src/native/pe/PeCertificateParser.test.ts b/src/native/pe/PeCertificateParser.test.ts index 4f95700723..4e94bf88e0 100644 --- a/src/native/pe/PeCertificateParser.test.ts +++ b/src/native/pe/PeCertificateParser.test.ts @@ -72,6 +72,47 @@ it("reads PE32 and PE32+ PKCS#7 certificate tables", () => { } }); +it.each([false, true])( + "preserves PE certificate bytes when dwLength counts alignment padding (PE32+ = %s)", + (plus) => { + // Choose a fixture with nonzero alignment padding so the control distinguishes + // an ASN.1 terminator from the end of the WIN_CERTIFICATE payload. + const payload = ["Padding", "Paddingx"] + .map((name) => pkcs7SignedDataFixture(name)) + .find((value) => (value.length + 8) % 8 !== 0)!; + const bytes = peCertificateFixture(plus, payload); + const control = parse(bytes); + const entry = control.entries[0]!; + const aligned = Math.ceil(entry.length / 8) * 8; + expect(aligned).toBeGreaterThan(entry.length); + bytes.writeUInt32LE(aligned, entry.location.offset); + const report = peCertificatesSchema.parse(parse(bytes)); + expect(report.entries[0]?.pkcs7).toEqual(entry.pkcs7); + expect(report.entries[0]?.length).toBe(aligned); + expect(report.entries[0]?.payload.sha256).toBe( + createHash("sha256") + .update( + bytes.subarray( + entry.payload.location.offset, + entry.location.offset + aligned, + ), + ) + .digest("hex"), + ); + bytes[entry.location.offset + aligned - 1] = 1; + expect(() => parse(bytes)).toThrow(/ContentInfo has trailing data/); + }, +); + +it("refuses surplus zero bytes beyond the PE entry alignment gap", () => { + const payload = pkcs7SignedDataFixture(); + expect(() => + parse( + peCertificateFixture(false, Buffer.concat([payload, Buffer.alloc(8)])), + ), + ).toThrow(/ContentInfo has trailing data/); +}); + it("reads BER indefinite constructed ContentInfo and preserves certificate bytes", () => { const payload = pkcs7SignedDataIndefiniteEnvelopeFixture(); const root = readAsn1(payload); diff --git a/src/native/pe/PeCertificateParser.ts b/src/native/pe/PeCertificateParser.ts index 5f8cbb1e21..9866a9ee76 100644 --- a/src/native/pe/PeCertificateParser.ts +++ b/src/native/pe/PeCertificateParser.ts @@ -9,6 +9,7 @@ import { oidName, parsePkcs7SignedData, PeCertificateAsn1Failure, + readAsn1, } from "./PeCertificateAsn1.js"; import { readPeHeaders } from "./PeResourceLayout.js"; @@ -115,7 +116,16 @@ export const parsePeCertificates = ( let pkcs7: PeCertificates["entries"][number]["pkcs7"] = null; if (peCertificateTypeName(certificateType) === "pkcs7") { try { - const parsed = parsePkcs7SignedData(payload); + const content = readAsn1(payload); + // The PE entry owns alignment outside the ASN.1 ContentInfo. Accept + // either uncounted padding or the exact counted zero-padding extent. + if ( + content.end !== payload.length && + (payload.length !== align8(8 + content.end) - 8 || + !payload.subarray(content.end).every((byte) => byte === 0)) + ) + throw new PeCertificateAsn1Failure("ContentInfo has trailing data."); + const parsed = parsePkcs7SignedData(content); teletexStringCompatibilityUsed ||= parsed.teletexStringCompatibilityUsed; pkcs7 = {