From 3a2074b633868b28518d4d989f0f4fefcaa2cb41 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 18:46:54 +0800 Subject: [PATCH] fix(asn1): decode four-byte BER lengths unsigned Accumulate length octets arithmetically so a four-octet length with bit 31 set remains a positive unsigned value and is rejected by the containing-range check rather than becoming a negative length. --- .../apkSignature/apkV1SignatureBlock.test.ts | 21 +++++++++++++++++++ src/domain/asn1Ber.ts | 3 ++- src/native/pe/PeCertificateParser.test.ts | 17 +++++++++++++++ src/native/pe/PeCertificates.fixture.ts | 3 ++- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/domain/apkSignature/apkV1SignatureBlock.test.ts b/src/domain/apkSignature/apkV1SignatureBlock.test.ts index 14d091845e..2917190731 100644 --- a/src/domain/apkSignature/apkV1SignatureBlock.test.ts +++ b/src/domain/apkSignature/apkV1SignatureBlock.test.ts @@ -244,3 +244,24 @@ it("rejects non-OCTET children inside a constructed signature", () => { ).kind, ).toBe("malformed-pkcs7"); }); + +it.each([ + ["primitive", 0x04], + ["constructed", 0x24], +] as const)( + "rejects a high-bit four-octet BER length on a %s signature field", + (_kind, tag) => { + const signature = Uint8Array.of(tag, 0x84, 0x80, 0, 0, 0); + expect( + errorOf( + signedDataBlock({ + certificates: [LEAF_CERTIFICATE], + signerInfos: [signerInfo({ names: LEAF_CERTIFICATE, signature })], + }), + ), + ).toEqual({ + kind: "malformed-pkcs7", + detail: "ASN.1 value exceeds its parent.", + }); + }, +); diff --git a/src/domain/asn1Ber.ts b/src/domain/asn1Ber.ts index 764865f754..fa66c64b98 100644 --- a/src/domain/asn1Ber.ts +++ b/src/domain/asn1Ber.ts @@ -90,7 +90,8 @@ export class Asn1BerReader { for (let index = 0; index < width; index += 1) { const byte = this.bytes[cursor]; if (byte === undefined) return fail("Truncated ASN.1 length."); - length = (length << 8) | byte; + // Bitwise shifts sign-extend a four-octet length with bit 31 set. + length = length * 256 + byte; cursor += 1; } } diff --git a/src/native/pe/PeCertificateParser.test.ts b/src/native/pe/PeCertificateParser.test.ts index 13e5fd94e0..fe401dfac6 100644 --- a/src/native/pe/PeCertificateParser.test.ts +++ b/src/native/pe/PeCertificateParser.test.ts @@ -94,6 +94,23 @@ it("reads BER indefinite constructed ContentInfo and preserves certificate bytes ); }); +it.each([ + ["primitive", 0x04], + ["constructed", 0x24], +] as const)( + "rejects a high-bit four-octet BER length on a %s SignerInfo signature", + (_kind, tag) => { + const payload = pkcs7SignedDataFixture( + "REA Fixture", + "REA Tests", + Buffer.from([tag, 0x84, 0x80, 0, 0, 0]), + ); + expect(() => parse(peCertificateFixture(false, payload))).toThrow( + /PKCS#7 signedData is malformed: ASN\.1 value exceeds its parent\./u, + ); + }, +); + it("matches nested indefinite constructed values and rejects invalid EOC forms", () => { const nested = readAsn1( Buffer.from([0x30, 0x80, 0x30, 0x80, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00]), diff --git a/src/native/pe/PeCertificates.fixture.ts b/src/native/pe/PeCertificates.fixture.ts index 4e526435b3..0960d675ea 100644 --- a/src/native/pe/PeCertificates.fixture.ts +++ b/src/native/pe/PeCertificates.fixture.ts @@ -66,6 +66,7 @@ const algorithm = (identifier: Buffer): Buffer => seq(identifier, nul()); export const pkcs7SignedDataFixture = ( commonName = "REA Fixture", organization = "REA Tests", + signature = octet(Buffer.from("sig")), ): Buffer => { const serial = Buffer.from([0x11, 0x22, 0x33, 0x44]); const subject = name(commonName, organization); @@ -92,7 +93,7 @@ export const pkcs7SignedDataFixture = ( seq(subject, integer(serial)), algorithm(SHA256), algorithm(SHA256_RSA), - octet(Buffer.from("sig")), + signature, ); const signedData = seq( integer(1),