mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 17:30:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eb48add14 |
No files matched your search
@@ -66,6 +66,28 @@ const report: PythonPyc = {
|
||||
],
|
||||
};
|
||||
|
||||
it.each([
|
||||
{
|
||||
flags: 1,
|
||||
hash_based: true,
|
||||
source_hash_hex: "",
|
||||
source_mtime: null,
|
||||
source_size: null,
|
||||
},
|
||||
{ flags: 1 },
|
||||
])("refuses inconsistent provider header facts %j", async (header) => {
|
||||
const result = await new PythonPycService({
|
||||
close: async () => {},
|
||||
identity,
|
||||
inspect: async () =>
|
||||
ok({ ...report, header: { ...report.header, ...header } }),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisOutputError" },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a relative path and extra arguments before reading", async () => {
|
||||
let calls = 0;
|
||||
const service = new PythonPycService({
|
||||
|
||||
@@ -202,9 +202,8 @@ describe("v1 signature block (PKCS#7 SignedData)", () => {
|
||||
let nested: Uint8Array = Uint8Array.from([0x05, 0x00]);
|
||||
for (let i = 0; i < 64; i += 1) nested = indefinite(0x30, nested);
|
||||
expect(errorOf(nested)).toEqual({
|
||||
kind: "budget-exceeded",
|
||||
resource: "asn1-depth",
|
||||
limit: 32,
|
||||
kind: "malformed-pkcs7",
|
||||
detail: "ASN.1 nesting exceeds 32 levels.",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -412,42 +411,3 @@ it.each([0, 1])(
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a valid constructed CMS content depth refusal distinct from malformed input", () => {
|
||||
let content = tlv(0x04, Uint8Array.of(1));
|
||||
const block = () =>
|
||||
signedDataBlock({
|
||||
certificates: [LEAF_CERTIFICATE],
|
||||
signerInfos: [signerInfo({ names: LEAF_CERTIFICATE })],
|
||||
encapsulatedContent: content,
|
||||
});
|
||||
expect(decodeV1SignatureBlock(block()).ok).toBe(true);
|
||||
for (let depth = 0; depth < 40; depth += 1)
|
||||
content = indefinite(0x24, content);
|
||||
expect(errorOf(block())).toEqual({
|
||||
kind: "budget-exceeded",
|
||||
resource: "asn1-depth",
|
||||
limit: 32,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the canonical OID byte allowance for a valid unknown digest algorithm", () => {
|
||||
const algorithm = tlv(
|
||||
0x06,
|
||||
Uint8Array.from([0x2a, ...Array<number>(64).fill(0)]),
|
||||
);
|
||||
expect(
|
||||
errorOf(
|
||||
signedDataBlock({
|
||||
certificates: [LEAF_CERTIFICATE],
|
||||
signerInfos: [
|
||||
signerInfo({
|
||||
names: LEAF_CERTIFICATE,
|
||||
digestAlgorithmOid: algorithm,
|
||||
}),
|
||||
],
|
||||
digestAlgorithmOid: algorithm,
|
||||
}),
|
||||
),
|
||||
).toEqual({ kind: "budget-exceeded", resource: "oid-bytes", limit: 64 });
|
||||
});
|
||||
@@ -27,10 +27,7 @@ export type ApkV1BlockError =
|
||||
readonly resource:
|
||||
| "v1-block-certificates"
|
||||
| "v1-signer-infos"
|
||||
| "asn1-elements"
|
||||
| "asn1-depth"
|
||||
| "oid-bytes"
|
||||
| "oid-arc-bytes";
|
||||
| "asn1-elements";
|
||||
readonly limit: number;
|
||||
};
|
||||
|
||||
@@ -101,14 +98,11 @@ class ApkBerReader {
|
||||
return this.#reader.read(at, limit, depth);
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof Asn1BerFailure)) throw cause;
|
||||
if (cause.constraint !== null)
|
||||
if (cause.kind === "budget-exceeded")
|
||||
throw new BlockFailure({
|
||||
kind: "budget-exceeded",
|
||||
resource:
|
||||
cause.constraint.resource === "nodes"
|
||||
? "asn1-elements"
|
||||
: "asn1-depth",
|
||||
limit: cause.constraint.limit,
|
||||
resource: "asn1-elements",
|
||||
limit: MAX_ASN1_ELEMENTS,
|
||||
});
|
||||
throw malformed(cause.message);
|
||||
}
|
||||
@@ -161,13 +155,6 @@ const expectOctetString = (
|
||||
const oidText = (content: Uint8Array): string => {
|
||||
const decoded = decodeAsn1Oid(content, { bytes: 64, arcBytes: 64 });
|
||||
if (decoded.ok) return decoded.value;
|
||||
if (decoded.error.kind === "byte-limit" || decoded.error.kind === "arc-limit")
|
||||
throw new BlockFailure({
|
||||
kind: "budget-exceeded",
|
||||
resource:
|
||||
decoded.error.kind === "byte-limit" ? "oid-bytes" : "oid-arc-bytes",
|
||||
limit: decoded.error.limit,
|
||||
});
|
||||
if (decoded.error.kind === "truncated")
|
||||
throw malformed("object identifier is truncated");
|
||||
throw malformed(`object identifier of ${String(content.length)} bytes`);
|
||||
@@ -201,13 +188,6 @@ const integerIdentityContents = (bytes: Uint8Array): Uint8Array => {
|
||||
return start === 0 ? bytes : bytes.subarray(start);
|
||||
};
|
||||
|
||||
/** Optional identity interpretation may remain unknown while raw certificate identity survives. */
|
||||
const unavailableIdentityInterpretation = (cause: unknown): boolean =>
|
||||
cause instanceof BlockFailure &&
|
||||
(cause.error.kind === "malformed-pkcs7" ||
|
||||
cause.error.resource === "oid-bytes" ||
|
||||
cause.error.resource === "oid-arc-bytes");
|
||||
|
||||
/**
|
||||
* RFC 5280 §7.1: RDN order matters; attributes in one SET do not. This
|
||||
* comparison profile admits ASCII PrintableString/UTF8String for common
|
||||
@@ -257,7 +237,8 @@ const directoryNameKey = (name: Asn1BerNode): string | null => {
|
||||
}
|
||||
return JSON.stringify(rdns);
|
||||
} catch (cause) {
|
||||
if (unavailableIdentityInterpretation(cause)) return null;
|
||||
if (cause instanceof BlockFailure && cause.error.kind === "malformed-pkcs7")
|
||||
return null;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
@@ -317,7 +298,8 @@ const certificateIdentity = (
|
||||
subjectKeyIdentifier,
|
||||
};
|
||||
} catch (cause: unknown) {
|
||||
if (unavailableIdentityInterpretation(cause)) return null;
|
||||
if (cause instanceof BlockFailure && cause.error.kind === "malformed-pkcs7")
|
||||
return null;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
+8
-14
@@ -11,16 +11,11 @@ export interface Asn1BerNode {
|
||||
readonly children: readonly Asn1BerNode[];
|
||||
}
|
||||
|
||||
/** The fixed parser allowance that prevented examining more BER structure. */
|
||||
export interface Asn1BerConstraint {
|
||||
readonly resource: "nodes" | "depth";
|
||||
readonly limit: number;
|
||||
}
|
||||
|
||||
export class Asn1BerFailure extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly constraint: Asn1BerConstraint | null = null,
|
||||
readonly kind: "malformed" | "budget-exceeded" = "malformed",
|
||||
readonly limit: number | null = null,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "Asn1BerFailure";
|
||||
@@ -33,9 +28,10 @@ interface Budget {
|
||||
|
||||
const fail = (
|
||||
message: string,
|
||||
constraint: Asn1BerConstraint | null = null,
|
||||
kind: Asn1BerFailure["kind"] = "malformed",
|
||||
limit: number | null = null,
|
||||
): never => {
|
||||
throw new Asn1BerFailure(message, constraint);
|
||||
throw new Asn1BerFailure(message, kind, limit);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -55,14 +51,12 @@ export class Asn1BerReader {
|
||||
|
||||
read(offset = 0, end = this.bytes.length, depth = 0): Asn1BerNode {
|
||||
if (depth > this.maxDepth)
|
||||
return fail(`ASN.1 nesting exceeds ${String(this.maxDepth)} levels.`, {
|
||||
resource: "depth",
|
||||
limit: this.maxDepth,
|
||||
});
|
||||
return fail(`ASN.1 nesting exceeds ${String(this.maxDepth)} levels.`);
|
||||
if (this.#budget.remaining <= 0)
|
||||
return fail(
|
||||
`ASN.1 value count exceeds the ${String(this.maxNodes)}-node budget.`,
|
||||
{ resource: "nodes", limit: this.maxNodes },
|
||||
"budget-exceeded",
|
||||
this.maxNodes,
|
||||
);
|
||||
this.#budget.remaining -= 1;
|
||||
if (offset >= end || offset + 2 > end)
|
||||
|
||||
@@ -3,9 +3,49 @@ import {
|
||||
inspectPythonPycInputSchema,
|
||||
pythonPycMarshalProfile,
|
||||
PYTHON_PYC_MARSHAL_PROFILES,
|
||||
pythonPycHeaderSchema,
|
||||
} from "./pythonPyc.js";
|
||||
import { pythonBytecodeIdentity } from "./pythonBytecodeIdentity.js";
|
||||
|
||||
const hashHeader = {
|
||||
magic: 3531,
|
||||
python_version: "3.12",
|
||||
code_layout: "py311",
|
||||
flags: 3,
|
||||
hash_based: true,
|
||||
check_source: true,
|
||||
source_hash_hex: "0123456789abcdef",
|
||||
source_mtime: null,
|
||||
source_size: null,
|
||||
};
|
||||
|
||||
it.each(["", "ab", "g".repeat(16)])(
|
||||
"rejects an invalid source hash %j",
|
||||
(source_hash_hex) => {
|
||||
expect(
|
||||
pythonPycHeaderSchema.safeParse({ ...hashHeader, source_hash_hex })
|
||||
.success,
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ hash_based: false },
|
||||
{ check_source: false },
|
||||
{ source_hash_hex: null },
|
||||
{ source_mtime: 0 },
|
||||
{ source_size: 0 },
|
||||
])("keeps header facts consistent with the recorded flags: %j", (changed) => {
|
||||
expect(
|
||||
pythonPycHeaderSchema.safeParse({ ...hashHeader, ...changed }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves unknown raw header flags and valid hash bytes", () => {
|
||||
const header = { ...hashHeader, flags: 0x80000003 };
|
||||
expect(pythonPycHeaderSchema.parse(header)).toEqual(header);
|
||||
});
|
||||
|
||||
it("keeps pinned CPython identity separate from standalone marshal coverage", () => {
|
||||
expect(pythonPycMarshalProfile(3394)).toEqual({
|
||||
code_layout: "py37",
|
||||
|
||||
@@ -205,17 +205,53 @@ const pythonCodeObjectSchema = z.strictObject({
|
||||
});
|
||||
export type PythonPycCodeObject = z.infer<typeof pythonCodeObjectSchema>;
|
||||
|
||||
export const pythonPycHeaderSchema = z.strictObject({
|
||||
magic: z.number().int().min(0).max(0xffff),
|
||||
python_version: z.string().min(1),
|
||||
code_layout: z.enum(PYTHON_PYC_CODE_LAYOUTS),
|
||||
flags: uint,
|
||||
hash_based: z.boolean(),
|
||||
check_source: z.boolean(),
|
||||
source_hash_hex: z.string().nullable(),
|
||||
source_mtime: uint.nullable(),
|
||||
source_size: uint.nullable(),
|
||||
});
|
||||
export const pythonPycHeaderSchema = z
|
||||
.strictObject({
|
||||
magic: z.number().int().min(0).max(0xffff),
|
||||
python_version: z.string().min(1),
|
||||
code_layout: z.enum(PYTHON_PYC_CODE_LAYOUTS),
|
||||
flags: uint,
|
||||
hash_based: z.boolean(),
|
||||
check_source: z.boolean(),
|
||||
source_hash_hex: z
|
||||
.string()
|
||||
.regex(/^[0-9a-f]{16}$/u)
|
||||
.nullable(),
|
||||
source_mtime: uint.nullable(),
|
||||
source_size: uint.nullable(),
|
||||
})
|
||||
.superRefine((header, context) => {
|
||||
for (const [field, flag] of [
|
||||
["hash_based", PYTHON_PYC_HASH_BASED],
|
||||
["check_source", PYTHON_PYC_CHECK_SOURCE],
|
||||
] as const)
|
||||
if (header[field] !== ((header.flags & flag) !== 0))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: [field],
|
||||
message: `${field} disagrees with its recorded header flag.`,
|
||||
});
|
||||
if (header.hash_based === (header.source_hash_hex === null))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["source_hash_hex"],
|
||||
message:
|
||||
"Hash-based pycs require an 8-byte source hash; others must omit it.",
|
||||
});
|
||||
if (header.hash_based === (header.source_mtime !== null))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["source_mtime"],
|
||||
message: "Timestamp pycs require mtime; hash-based pycs must omit it.",
|
||||
});
|
||||
if (header.hash_based === (header.source_size !== null))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["source_size"],
|
||||
message:
|
||||
"Timestamp pycs require source_size; hash-based pycs must omit it.",
|
||||
});
|
||||
});
|
||||
|
||||
/** Bounded CPython 3.7–3.13 pyc observation; marshal is walked, bytecode is not decompiled. */
|
||||
export const pythonPycSchema = z
|
||||
@@ -253,29 +289,6 @@ export const pythonPycSchema = z
|
||||
message: "Marshal byte count disagrees with the header-stripped file.",
|
||||
});
|
||||
}
|
||||
if (report.header.hash_based === (report.header.source_hash_hex === null)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["header", "source_hash_hex"],
|
||||
message:
|
||||
"Hash-based pycs require an 8-byte source hash; others must omit it.",
|
||||
});
|
||||
}
|
||||
if (report.header.hash_based === (report.header.source_mtime !== null)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["header", "source_mtime"],
|
||||
message: "Timestamp pycs require mtime; hash-based pycs must omit it.",
|
||||
});
|
||||
}
|
||||
if (report.header.hash_based === (report.header.source_size !== null)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["header", "source_size"],
|
||||
message:
|
||||
"Timestamp pycs require source_size; hash-based pycs must omit it.",
|
||||
});
|
||||
}
|
||||
if (report.coverage.code_objects !== report.code_objects.length) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
|
||||
@@ -28,12 +28,7 @@ const OID_NAMES: Readonly<Record<string, string>> = {
|
||||
|
||||
/** A fixed per-payload decoder allowance and its limit. */
|
||||
export interface PeCertificateDecodeAllowance {
|
||||
readonly allowance:
|
||||
| "asn1-nodes"
|
||||
| "asn1-depth"
|
||||
| "decoded-text"
|
||||
| "oid-bytes"
|
||||
| "oid-arc-bytes";
|
||||
readonly allowance: "asn1-nodes" | "decoded-text";
|
||||
readonly limit: number;
|
||||
}
|
||||
|
||||
@@ -67,15 +62,9 @@ export const readAsn1 = (bytes: Buffer): Asn1BerNode => {
|
||||
if (cause instanceof Asn1BerFailure)
|
||||
throw new PeCertificateAsn1Failure(
|
||||
cause.message,
|
||||
cause.constraint === null
|
||||
? null
|
||||
: {
|
||||
allowance:
|
||||
cause.constraint.resource === "nodes"
|
||||
? "asn1-nodes"
|
||||
: "asn1-depth",
|
||||
limit: cause.constraint.limit,
|
||||
},
|
||||
cause.kind === "budget-exceeded"
|
||||
? { allowance: "asn1-nodes", limit: cause.limit ?? ASN1_NODE_BUDGET }
|
||||
: null,
|
||||
);
|
||||
throw cause;
|
||||
}
|
||||
@@ -123,14 +112,12 @@ export const decodeOid = (content: Buffer): string => {
|
||||
case "empty":
|
||||
return fail("Empty OBJECT IDENTIFIER.");
|
||||
case "byte-limit":
|
||||
throw new PeCertificateAsn1Failure(
|
||||
return fail(
|
||||
`OBJECT IDENTIFIER exceeds ${String(decoded.error.limit)} bytes.`,
|
||||
{ allowance: "oid-bytes", limit: decoded.error.limit },
|
||||
);
|
||||
case "arc-limit":
|
||||
throw new PeCertificateAsn1Failure(
|
||||
return fail(
|
||||
`OBJECT IDENTIFIER arc exceeds ${String(decoded.error.limit)} bytes.`,
|
||||
{ allowance: "oid-arc-bytes", limit: decoded.error.limit },
|
||||
);
|
||||
case "non-minimal":
|
||||
return fail("Non-minimal OBJECT IDENTIFIER component.");
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { repeatedCertificateSignedData } from "../../../tests/fixtures/peCertificateAllowance.js";
|
||||
import {
|
||||
indefinite,
|
||||
tlv,
|
||||
signedDataBlock,
|
||||
signerInfo,
|
||||
LEAF_CERTIFICATE,
|
||||
} from "../../../tests/fixtures/apkSignature/v1SignatureBlock.js";
|
||||
import { inspectPeCertificatesInputSchema } from "../../domain/native/peCertificates.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { peCertificateFixture } from "./PeCertificates.fixture.js";
|
||||
import { PeCertificatesProvider } from "./PeCertificatesProvider.js";
|
||||
|
||||
const encapsulated = (depth: number): Buffer => {
|
||||
let content = tlv(0x04, Uint8Array.of(1));
|
||||
for (let level = 0; level < depth; level += 1)
|
||||
content = indefinite(0x24, content);
|
||||
return Buffer.from(
|
||||
signedDataBlock({
|
||||
certificates: [LEAF_CERTIFICATE],
|
||||
signerInfos: [signerInfo({ names: LEAF_CERTIFICATE })],
|
||||
encapsulatedContent: content,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const inspect = async (payload: Buffer) => {
|
||||
const path = join(
|
||||
await createTestTempDirectory("rea-pe-decoder-allowance-"),
|
||||
"inert.exe",
|
||||
);
|
||||
await writeFile(path, peCertificateFixture(false, payload));
|
||||
const provider = new PeCertificatesProvider();
|
||||
try {
|
||||
return await provider.inspect(
|
||||
inspectPeCertificatesInputSchema.parse({ path }),
|
||||
);
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
};
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "depth",
|
||||
payload: () => encapsulated(40),
|
||||
limits: { max_asn1_depth: 32 },
|
||||
description: "32 nesting levels",
|
||||
},
|
||||
{
|
||||
name: "nodes",
|
||||
payload: () => repeatedCertificateSignedData(600),
|
||||
limits: { max_asn1_values: 16_384 },
|
||||
description: "16384 values",
|
||||
},
|
||||
{
|
||||
name: "text",
|
||||
payload: () => repeatedCertificateSignedData(350),
|
||||
limits: { max_decoded_text_bytes: 65_536 },
|
||||
description: "65536 bytes",
|
||||
},
|
||||
])(
|
||||
"reports the fixed $name allowance with its own resource and recovery",
|
||||
async ({ payload, limits, description }) => {
|
||||
const result = await inspect(payload());
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected decoder refusal");
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
if (!(result.error instanceof AnalysisResourceConstraintError))
|
||||
throw result.error;
|
||||
expect(result.error.reportedLimits).toEqual(limits);
|
||||
expect(result.error.retryable).toBe(false);
|
||||
expect(result.error.fixedAllowance?.name).toContain(description);
|
||||
expect(result.error.fixedAllowance?.name).toContain("no runtime override");
|
||||
},
|
||||
);
|
||||
|
||||
it("accepts shallower valid BER and keeps malformed BER an input failure", async () => {
|
||||
expect((await inspect(encapsulated(1))).ok).toBe(true);
|
||||
const malformed = await inspect(Buffer.from([0x30, 0x03, 0x05]));
|
||||
expect(malformed.ok).toBe(false);
|
||||
if (malformed.ok) throw new Error("Expected malformed input refusal");
|
||||
expect(malformed.error).toBeInstanceOf(AnalysisInputError);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "bytes",
|
||||
content: Buffer.from([0x2a, ...Array<number>(256).fill(0)]),
|
||||
limits: { max_oid_bytes: 256 },
|
||||
},
|
||||
{
|
||||
name: "arc bytes",
|
||||
content: Buffer.from([0x2a, 0x81, ...Array<number>(31).fill(0x80), 0]),
|
||||
limits: { max_oid_arc_bytes: 32 },
|
||||
},
|
||||
])(
|
||||
"preserves the canonical OID $name allowance in a valid CMS algorithm",
|
||||
async ({ content, limits }) => {
|
||||
const algorithm = tlv(0x06, content);
|
||||
const result = await inspect(
|
||||
Buffer.from(
|
||||
signedDataBlock({
|
||||
certificates: [LEAF_CERTIFICATE],
|
||||
signerInfos: [
|
||||
signerInfo({
|
||||
names: LEAF_CERTIFICATE,
|
||||
digestAlgorithmOid: algorithm,
|
||||
}),
|
||||
],
|
||||
digestAlgorithmOid: algorithm,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected OID allowance refusal");
|
||||
expect(result.error).toBeInstanceOf(AnalysisResourceConstraintError);
|
||||
if (!(result.error instanceof AnalysisResourceConstraintError))
|
||||
throw result.error;
|
||||
expect(result.error.reportedLimits).toEqual(limits);
|
||||
expect(result.error.retryable).toBe(false);
|
||||
expect(result.error.fixedAllowance?.name).toContain("OBJECT IDENTIFIER");
|
||||
expect(result.error.fixedAllowance?.name).toContain("no runtime override");
|
||||
},
|
||||
);
|
||||
@@ -105,40 +105,24 @@ const inputBudgetRemediation = (
|
||||
const decoderAllowanceFailure = (
|
||||
cause: PeCertificateAsn1Failure,
|
||||
{ allowance, limit }: PeCertificateDecodeAllowance,
|
||||
) => {
|
||||
const selected = {
|
||||
"asn1-nodes": {
|
||||
limits: { max_asn1_values: limit },
|
||||
description: `The PE certificate ASN.1 allowance of ${String(limit)} values per PKCS#7 payload (this release has no runtime override)`,
|
||||
},
|
||||
"asn1-depth": {
|
||||
limits: { max_asn1_depth: limit },
|
||||
description: `The PE certificate ASN.1 allowance of ${String(limit)} nesting levels per PKCS#7 payload (this release has no runtime override)`,
|
||||
},
|
||||
"decoded-text": {
|
||||
limits: { max_decoded_text_bytes: limit },
|
||||
description: `The PE certificate decoded-text allowance of ${String(limit)} bytes per PKCS#7 payload (this release has no runtime override)`,
|
||||
},
|
||||
"oid-bytes": {
|
||||
limits: { max_oid_bytes: limit },
|
||||
description: `The PE certificate OBJECT IDENTIFIER allowance of ${String(limit)} bytes per OID (this release has no runtime override)`,
|
||||
},
|
||||
"oid-arc-bytes": {
|
||||
limits: { max_oid_arc_bytes: limit },
|
||||
description: `The PE certificate OBJECT IDENTIFIER allowance of ${String(limit)} bytes per base-128 arc (this release has no runtime override)`,
|
||||
},
|
||||
}[allowance];
|
||||
return new AnalysisResourceConstraintError(
|
||||
) =>
|
||||
new AnalysisResourceConstraintError(
|
||||
OPERATION,
|
||||
"memory",
|
||||
`PKCS#7 signedData exceeds a fixed decoder allowance: ${cause.message}`,
|
||||
selected.limits,
|
||||
allowance === "asn1-nodes"
|
||||
? { max_asn1_values: limit }
|
||||
: { max_decoded_text_bytes: limit },
|
||||
{
|
||||
cause,
|
||||
...fixedAllowanceOptions(selected.description, "parser"),
|
||||
...fixedAllowanceOptions(
|
||||
allowance === "asn1-nodes"
|
||||
? `The PE certificate ASN.1 allowance of ${String(limit)} values per PKCS#7 payload (this release has no runtime override)`
|
||||
: `The PE certificate decoded-text allowance of ${String(limit)} bytes per PKCS#7 payload (this release has no runtime override)`,
|
||||
"parser",
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const peCertificatesFailurePrimary = (
|
||||
facts: ArtifactFilePrimaryFacts,
|
||||
|
||||
+3
-14
@@ -230,15 +230,12 @@ export interface SignerInfoSpec {
|
||||
* from the certificate named by `names`.
|
||||
*/
|
||||
readonly serial?: Uint8Array;
|
||||
/** Complete digest algorithm OID TLV, including bounded-parser fixtures. */
|
||||
readonly digestAlgorithmOid?: Uint8Array;
|
||||
}
|
||||
|
||||
export const signerInfo = ({
|
||||
names,
|
||||
signature,
|
||||
serial,
|
||||
digestAlgorithmOid,
|
||||
}: SignerInfoSpec): Uint8Array =>
|
||||
tlv(
|
||||
0x30,
|
||||
@@ -247,7 +244,7 @@ export const signerInfo = ({
|
||||
names,
|
||||
serial === undefined ? undefined : tlv(0x02, serial),
|
||||
),
|
||||
tlv(0x30, digestAlgorithmOid ?? oid(OID_SHA256)),
|
||||
algorithm(OID_SHA256),
|
||||
algorithm(OID_ECDSA_SHA256),
|
||||
signature ?? tlv(0x04, Uint8Array.from([1, 2, 3])),
|
||||
);
|
||||
@@ -257,8 +254,6 @@ export const signedDataBlock = (options: {
|
||||
readonly certificates: readonly Uint8Array[];
|
||||
readonly signerInfos: readonly Uint8Array[];
|
||||
readonly contentType?: string;
|
||||
readonly encapsulatedContent?: Uint8Array;
|
||||
readonly digestAlgorithmOid?: Uint8Array;
|
||||
}): Uint8Array =>
|
||||
tlv(
|
||||
0x30,
|
||||
@@ -268,14 +263,8 @@ export const signedDataBlock = (options: {
|
||||
tlv(
|
||||
0x30,
|
||||
integer(1),
|
||||
tlv(0x31, tlv(0x30, options.digestAlgorithmOid ?? oid(OID_SHA256))),
|
||||
tlv(
|
||||
0x30,
|
||||
oid("1.2.840.113549.1.7.1"),
|
||||
...(options.encapsulatedContent === undefined
|
||||
? []
|
||||
: [tlv(0xa0, options.encapsulatedContent)]),
|
||||
),
|
||||
tlv(0x31, algorithm(OID_SHA256)),
|
||||
tlv(0x30, oid("1.2.840.113549.1.7.1")),
|
||||
tlv(0xa0, ...options.certificates),
|
||||
tlv(0x31, ...options.signerInfos),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user