fix(pe): reject empty and non-minimal certificate serial INTEGERs (#8836)

This commit is contained in:
RRFZ authored and GitHub committed 2026-11-04 16:45:48 +08:00
1 parent 1998e65705
commit 772c1bc401
2 files changed
+112 -3

No files matched your search

+26 -3
View File
@@ -214,7 +214,22 @@ const decodeDirectoryString = (
}
};
const integerHex = (content: Buffer, budget: DecodedNameBudget): string => {
// 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 => {
if (content.length === 0) return fail(`${label} INTEGER is empty.`);
const second = content[1];
if (
second !== undefined &&
((content[0] === 0x00 && second < 0x80) ||
(content[0] === 0xff && second >= 0x80))
)
return fail(`${label} INTEGER is not minimally encoded.`);
let start = 0;
while (start < content.length - 1 && content[start] === 0) start += 1;
chargeDecodedName(budget, (content.length - start) * 2);
@@ -444,7 +459,11 @@ export const parseCertificate = (
return {
start: node.start,
end: node.end,
serial_hex: integerHex(serial.content, names),
serial_hex: integerHex(
serial.content,
names,
"TBSCertificate serialNumber",
),
issuer: decodeName(issuer, names),
subject: decodeName(subject, names),
not_before: validityTime(notBefore, names),
@@ -484,7 +503,11 @@ const signerIdentifier = (
return fail("SignerInfo issuerAndSerialNumber is truncated.");
return {
issuer: decodeName(name, names),
serial_hex: integerHex(serial.content, names),
serial_hex: integerHex(
serial.content,
names,
"SignerInfo issuerAndSerialNumber serialNumber",
),
subject_key_identifier_hex: null,
};
}
+86
View File
@@ -515,6 +515,92 @@ it("keeps subjectKeyIdentifier bytes on the signer", () => {
).toThrow(/decoded budget/u);
});
// X.690 8.3.1 and 8.3.2 apply under BER as well as DER.
const malformedSerials = [
[],
[0x00, 0x00],
[0x00, 0x7f],
[0x00, 0x00, 0x80],
[0xff, 0x80],
[0xff, 0xff],
] as const;
const minimalSerials = [
[[0x00], "00"],
[[0x7f], "7f"],
[[0x00, 0x80], "80"],
[[0xff], "ff"],
[[0xff, 0x7f], "ff7f"],
[[0x11, 0x22, 0x33, 0x44], "11223344"],
] as const;
const serialName = derSeq(
derSet(
derSeq(
der(0x06, Buffer.from([0x55, 0x04, 0x03])),
der(0x0c, Buffer.from("Fixture")),
),
),
);
it("rejects empty and non-minimal certificate serialNumber INTEGERs", () => {
const certificate = (serial: readonly number[]) =>
readAsn1(
derSeq(
derSeq(
der(0x02, Buffer.from(serial)),
SHA256_ALG,
serialName,
derSeq(
der(0x17, Buffer.from("250101000000Z")),
der(0x17, Buffer.from("260101000000Z")),
),
serialName,
derSeq(SHA256_ALG, der(0x03, Buffer.from([0]))),
),
SHA256_ALG,
der(0x03, Buffer.from([0])),
),
);
for (const serial of malformedSerials)
expect(
() => parseCertificate(certificate(serial)),
JSON.stringify(serial),
).toThrow(/TBSCertificate serialNumber INTEGER is (empty|not minimally)/u);
for (const [serial, hex] of minimalSerials)
expect(parseCertificate(certificate(serial)).serial_hex).toBe(hex);
});
it("rejects empty and non-minimal SignerInfo serialNumber INTEGERs", () => {
const payload = (serial: readonly number[]) =>
contentInfo(
derSeq(
derInt(1),
derSet(SHA256_ALG),
derSeq(DATA_OID),
derSet(
derSeq(
derInt(1),
derSeq(serialName, der(0x02, Buffer.from(serial))),
SHA256_ALG,
SHA256_ALG,
der(0x04, Buffer.from([1])),
),
),
),
);
for (const serial of malformedSerials)
expect(
() => parse(peCertificateFixture(false, payload(serial))),
JSON.stringify(serial),
).toThrow(
/SignerInfo issuerAndSerialNumber serialNumber INTEGER is (empty|not minimally)/u,
);
for (const [serial, hex] of minimalSerials)
expect(
parse(peCertificateFixture(false, payload(serial))).entries[0]?.pkcs7
?.signers[0]?.serial_hex,
).toBe(hex);
});
it("fails a truncated issuerAndSerialNumber instead of dropping the signer", () => {
const signer = derSeq(
derInt(1),