mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
fix(pe): enforce certificate name string alphabets (#8494)
* fix(pe): enforce certificate name string alphabets * test(mcp): assert structured absolute-path diagnostics * test(mcp): assert typed selector and stdio diagnostics * fix(pe): bound IA5 name scans before validation
This commit is contained in:
1 parent
32c253ba31
commit
aa48d494e9
3 files changed
+137
-2
No files matched your search
@@ -186,9 +186,19 @@ const decodeDirectoryString = (
|
||||
return decodeUtf32Be(node.content, budget);
|
||||
case 0x1e:
|
||||
return decodeBmpString(node.content, budget);
|
||||
case 0x0c:
|
||||
case 0x13:
|
||||
case 0x13: {
|
||||
const decoded = decodeUtf8(node.content, budget);
|
||||
if (/[^A-Za-z0-9 '()+,\-./:=?]/u.test(decoded))
|
||||
return fail("PrintableString holds a character outside its alphabet.");
|
||||
return decoded;
|
||||
}
|
||||
case 0x16:
|
||||
chargeDecodedName(budget, node.content.length);
|
||||
if (node.content.some((byte) => byte > 0x7f))
|
||||
return fail("IA5String holds a non-ASCII character.");
|
||||
// Validated ASCII needs no UTF-8 round trip or second budget charge.
|
||||
return node.content.toString("ascii");
|
||||
case 0x0c:
|
||||
case 0x17:
|
||||
case 0x18:
|
||||
return decodeUtf8(node.content, budget);
|
||||
|
||||
@@ -829,3 +829,85 @@ it("preserves valid BMPString code units on both sides of the surrogate range",
|
||||
parseCertificate(readAsn1(namedCertificate(name, name))).subject.text,
|
||||
).toBe("commonName=\ud7ff\ue000\ufeff");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"PrintableString",
|
||||
0x13,
|
||||
[
|
||||
"name@host",
|
||||
"under_score",
|
||||
"A\u0000B",
|
||||
"A\tB",
|
||||
"A\n",
|
||||
"A\r",
|
||||
"A\u007f",
|
||||
"é",
|
||||
"中",
|
||||
],
|
||||
],
|
||||
["IA5String", 0x16, ["é", "中"]],
|
||||
] as const)(
|
||||
"rejects characters outside the %s alphabet in certificate names",
|
||||
(_label, tag, values) => {
|
||||
for (const value of values) {
|
||||
const invalid = directoryName(der(tag, Buffer.from(value, "utf8")));
|
||||
const valid = directoryName(der(0x0c, Buffer.from(value, "utf8")));
|
||||
for (const [issuer, subject] of [
|
||||
[invalid, valid],
|
||||
[valid, invalid],
|
||||
] as const) {
|
||||
expect(() =>
|
||||
parseCertificate(readAsn1(namedCertificate(issuer, subject))),
|
||||
).toThrow(PeCertificateAsn1Failure);
|
||||
}
|
||||
const certificate = parseCertificate(
|
||||
readAsn1(namedCertificate(valid, valid)),
|
||||
);
|
||||
expect(certificate.issuer.attributes[0]?.value).toBe(value);
|
||||
expect(certificate.subject.attributes[0]?.value).toBe(value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("bounds IA5String inspection before alphabet validation and charges text once", () => {
|
||||
const ascii = Buffer.alloc(Math.floor(MAX_DECODED_NAME_BYTES / 3), 0x41);
|
||||
const within = directoryName(der(0x16, ascii));
|
||||
const certificate = parseCertificate(
|
||||
readAsn1(namedCertificate(within, within)),
|
||||
);
|
||||
expect(certificate.issuer.attributes[0]?.value).toBe(ascii.toString("ascii"));
|
||||
expect(certificate.subject.attributes[0]?.value).toBe(
|
||||
ascii.toString("ascii"),
|
||||
);
|
||||
for (const ending of [Buffer.from("A"), Buffer.from("é")]) {
|
||||
const oversized = Buffer.concat([
|
||||
Buffer.alloc(MAX_DECODED_NAME_BYTES, 0x41),
|
||||
ending,
|
||||
]);
|
||||
const parseOversized = () =>
|
||||
parseCertificate(
|
||||
readAsn1(namedCertificate(directoryName(der(0x16, oversized)), within)),
|
||||
);
|
||||
expect(parseOversized).toThrow(
|
||||
expect.objectContaining({
|
||||
exhausted: { allowance: "decoded-text", limit: MAX_DECODED_NAME_BYTES },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[0x13, "AZaz09 '()+,-./:=?"],
|
||||
[0x16, "mail_box@example.test\u0000\t\u007f"],
|
||||
] as const)(
|
||||
"preserves valid character boundaries for name tag %i",
|
||||
(tag, value) => {
|
||||
const name = directoryName(der(tag, Buffer.from(value, "ascii")));
|
||||
const certificate = parseCertificate(
|
||||
readAsn1(namedCertificate(name, name)),
|
||||
);
|
||||
expect(certificate.issuer.attributes[0]?.value).toBe(value);
|
||||
expect(certificate.subject.attributes[0]?.value).toBe(value);
|
||||
},
|
||||
);
|
||||
@@ -346,3 +346,46 @@ cliTest.for([
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.for([
|
||||
[0x13, "REA@Fixture"],
|
||||
[0x16, "REAéFixture"],
|
||||
] as const)(
|
||||
"rejects an invalid certificate name alphabet for tag %i through the public CLI",
|
||||
async ([tag, name], { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-pe-cert-name-alphabet-");
|
||||
const path = join(root, "name.exe");
|
||||
const payload = pkcs7SignedDataFixture(name);
|
||||
const content = Buffer.from(name, "utf8");
|
||||
const marker = Buffer.concat([
|
||||
Buffer.from([0x0c, content.length]),
|
||||
content,
|
||||
]);
|
||||
const at = payload.indexOf(marker);
|
||||
expect(at).toBeGreaterThanOrEqual(0);
|
||||
payload[at] = tag;
|
||||
await writeFile(path, peCertificateFixture(false, payload));
|
||||
const invalid = await cli.run({
|
||||
arguments: ["inspect-pe-certificates", path, "--json"],
|
||||
});
|
||||
expect(invalid.exitCode).toBe(1);
|
||||
expect(invalid.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: { operation: "inspect_pe_certificates" },
|
||||
});
|
||||
// Only restore the string tag: the same bytes are valid UTF8String content.
|
||||
payload[at] = 0x0c;
|
||||
await writeFile(path, peCertificateFixture(false, payload));
|
||||
const valid = await cli.run({
|
||||
arguments: ["inspect-pe-certificates", path, "--json"],
|
||||
});
|
||||
expect(valid.exitCode, valid.stderr).toBe(0);
|
||||
const evidence = toolContract("inspect_pe_certificates").outputSchema.parse(
|
||||
valid.json,
|
||||
);
|
||||
expect(
|
||||
evidence.normalized_result.entries[0]?.pkcs7?.certificates[0]?.issuer
|
||||
.attributes[0]?.value,
|
||||
).toBe(name);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user