diff --git a/src/native/pe/PeCertificateAsn1.ts b/src/native/pe/PeCertificateAsn1.ts index 0533569c05..3c15a9058e 100644 --- a/src/native/pe/PeCertificateAsn1.ts +++ b/src/native/pe/PeCertificateAsn1.ts @@ -230,11 +230,7 @@ const decodeDirectoryString = ( // X.690 8.3.1 and 8.3.2 hold for BER as well as DER: an INTEGER has at least // one content octet, and its first nine bits are never all zeros or all ones. // Padded encodings would otherwise collapse onto the minimal serial's hex. -const integerHex = ( - content: Buffer, - budget: DecodedNameBudget, - label: string, -): string => { +const requireIntegerContent = (content: Buffer, label: string): void => { if (content.length === 0) return fail(`${label} INTEGER is empty.`); const second = content[1]; if ( @@ -243,6 +239,14 @@ const integerHex = ( (content[0] === 0xff && second >= 0x80)) ) return fail(`${label} INTEGER is not minimally encoded.`); +}; + +const integerHex = ( + content: Buffer, + budget: DecodedNameBudget, + label: string, +): string => { + requireIntegerContent(content, label); let start = 0; while (start < content.length - 1 && content[start] === 0) start += 1; chargeDecodedName(budget, (content.length - start) * 2); @@ -716,6 +720,7 @@ const parseSignedData = ( const [version, digestSet, encapContentInfo] = children; if (version === undefined || version.tag !== 0x02) return fail("SignedData is missing version."); + requireIntegerContent(version.content, "SignedData version"); if (digestSet === undefined || digestSet.tag !== 0x31) return fail("SignedData is missing digestAlgorithms."); if ( diff --git a/src/native/pe/PeCertificateParser.test.ts b/src/native/pe/PeCertificateParser.test.ts index 32771427cb..2884365c16 100644 --- a/src/native/pe/PeCertificateParser.test.ts +++ b/src/native/pe/PeCertificateParser.test.ts @@ -1274,3 +1274,49 @@ it("rejects malformed and repeated certificate extensions", () => { ).subject.text, ).toBe("commonName=Fixture"); }); + +it.each([[], [0x00, 0x01], [0xff, 0xff]].map((version) => ({ version })))( + "rejects malformed SignedData version INTEGER content $version", + ({ version }) => { + const original = pkcs7SignedDataFixture(); + const signed = readAsn1(original).children[1]!.children[0]!; + const payload = contentInfo( + derSeq( + der(0x02, Buffer.from(version)), + ...signed.children + .slice(1) + .map((node) => original.subarray(node.start, node.end)), + ), + ); + expect(() => parse(peCertificateFixture(false, payload))).toThrow( + /SignedData version INTEGER/u, + ); + }, +); + +it.each([ + { name: "short form", version: Buffer.from([0x02, 0x01, 0x01]) }, + { name: "BER long form", version: Buffer.from([0x02, 0x81, 0x01, 0x01]) }, +])( + "retains a valid SignedData version with $name length encoding", + ({ version }) => { + const original = pkcs7SignedDataFixture(); + const signed = readAsn1(original).children[1]!.children[0]!; + const payload = contentInfo( + derSeq( + version, + ...signed.children + .slice(1) + .map((node) => original.subarray(node.start, node.end)), + ), + ); + const report = parse(peCertificateFixture(false, payload)); + expect(report.coverage).toMatchObject({ + status: "complete", + certificates: 1, + }); + expect(report.entries[0]?.pkcs7?.signers[0]).toMatchObject({ + serial_hex: "11223344", + }); + }, +);