fix(managed): bind inspection evidence to artifact identity

This commit is contained in:
morluto committed 2026-10-09 05:15:23 +08:00
1 parent dbcf8c3391
commit 73b09f4c5c
8 files changed
+222 -35

No files matched your search

+6
View File
@@ -137,6 +137,12 @@ function implements a specific managed method.
## Evidence record shape
Member comparison, application graph projection, and managed/native verification
authenticate supplied inspection Evidence and check that any subject SHA-256
matches the normalized artifact SHA-256. A valid Evidence ID alone does not
establish that those two identities agree. Evidence without a subject remains
usable, with no subject digest available to cross-check.
Every planned operation returns a provider result and Evidence with four
commitment groups:
+10 -15
View File
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { z } from "zod";
import { evidenceSchema, parseEvidence, type Evidence } from "../evidence.js";
import { evidenceSchema, type Evidence } from "../evidence.js";
import {
createJavaScriptApplicationGraph,
javascriptApplicationGraphSchema,
@@ -32,6 +32,7 @@ import {
} from "./managedApplicationGraphNodes.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
import { parseManagedInspectionEvidence } from "./managedInspectionEvidence.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const textSchema = z.string().min(1);
@@ -194,7 +195,7 @@ const parseManagedInputs = (
artifact:
input.managed_artifact === undefined
? null
: parseManagedEvidence(
: parseManagedInspectionEvidence(
input.managed_artifact,
"inspect_managed_artifact",
managedArtifactInspectionSchema,
@@ -202,7 +203,7 @@ const parseManagedInputs = (
members:
input.managed_members === undefined
? null
: parseManagedEvidence(
: parseManagedInspectionEvidence(
input.managed_members,
"inspect_managed_members",
managedMemberInspectionSchema,
@@ -210,24 +211,13 @@ const parseManagedInputs = (
boundaries:
input.managed_native_boundaries === undefined
? null
: parseManagedEvidence(
: parseManagedInspectionEvidence(
input.managed_native_boundaries,
"inspect_managed_native_boundaries",
managedNativeBoundaryInspectionSchema,
),
});
const parseManagedEvidence = <Result>(
rawEvidence: unknown,
operation: string,
schema: z.ZodType<Result>,
): { readonly evidence: Evidence; readonly result: Result } => {
const evidence = parseEvidence(rawEvidence);
if (evidence.operation !== operation)
throw new TypeError(`Evidence operation is not ${operation}`);
return { evidence, result: schema.parse(evidence.normalized_result) };
};
const chooseArtifact = (
parsed: ParsedManagedGraphInput,
): { readonly sha256: string; readonly path: string } => {
@@ -272,6 +262,11 @@ const projectionLimitations = (
"Managed member Evidence was not supplied; type, method, and field nodes are absent.",
]
: []),
...(parsed.artifact === null
? [
"Managed artifact Evidence was not supplied; assembly identity observations are absent.",
]
: []),
...(parsed.boundaries === null
? [
"Managed native-boundary Evidence was not supplied; P/Invoke and native-implementation nodes are absent.",
@@ -0,0 +1,26 @@
import { parseEvidence, type Evidence } from "../evidence.js";
import type { z } from "zod";
/**
* Parse managed inspection Evidence and bind its artifact to any available subject.
* A null subject remains valid and means there is no digest to cross-check.
*/
export const parseManagedInspectionEvidence = <
Result extends { readonly artifact: { readonly sha256: string } },
>(
rawEvidence: unknown,
operation: string,
schema: z.ZodType<Result>,
): { readonly evidence: Evidence; readonly result: Result } => {
const evidence = parseEvidence(rawEvidence);
if (evidence.operation !== operation)
throw new TypeError(`Evidence operation is not ${operation}`);
const result = schema.parse(evidence.normalized_result);
const artifactSha256 = result.artifact.sha256;
const subjectSha256 = evidence.subject?.digest.sha256;
if (subjectSha256 !== undefined && artifactSha256 !== subjectSha256)
throw new TypeError(
`Managed Evidence ${operation} (${evidence.evidence_id}) subject SHA-256 ${subjectSha256} does not match normalized artifact SHA-256 ${artifactSha256}`,
);
return { evidence, result };
};
@@ -2,11 +2,11 @@ import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { parseEvidence } from "../evidence.js";
import {
managedMemberInspectionSchema,
type ManagedMemberInspection,
} from "./managedArtifact.js";
import { parseManagedInspectionEvidence } from "./managedInspectionEvidence.js";
import type {
ManagedMemberComparisonResult,
ManagedMemberComparisonSide,
@@ -275,11 +275,13 @@ export const parseManagedMemberEvidence = (
readonly evidenceId: string;
readonly result: ManagedMemberInspection;
} => {
const parsed = parseEvidence(evidence);
if (parsed.operation !== "inspect_managed_members")
throw new TypeError("Evidence operation is not inspect_managed_members");
const parsed = parseManagedInspectionEvidence(
evidence,
"inspect_managed_members",
managedMemberInspectionSchema,
);
return {
evidenceId: parsed.evidence_id,
result: managedMemberInspectionSchema.parse(parsed.normalized_result),
evidenceId: parsed.evidence.evidence_id,
result: parsed.result,
};
};
+13 -13
View File
@@ -2,11 +2,12 @@ import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { parseEvidence } from "../evidence.js";
import type { Evidence } from "../evidence.js";
import {
managedNativeBoundaryInspectionSchema,
type ManagedNativeBoundaryInspection,
} from "./managedArtifact.js";
import { parseManagedInspectionEvidence } from "./managedInspectionEvidence.js";
import type { JsonValue } from "../jsonValue.js";
import {
managedNativeVerificationResultSchema,
@@ -33,25 +34,24 @@ const sha256 = (value: JsonValue): string => {
return createHash("sha256").update(serialized).digest("hex");
};
const managedBoundaryFromInput = (
const parseManagedBoundary = (
input: ManagedNativeVerificationInput,
): ManagedNativeBoundaryInspection => {
const managedEvidence = parseEvidence(input.managed_boundaries);
if (managedEvidence.operation !== "inspect_managed_native_boundaries")
throw new TypeError(
"Evidence operation is not inspect_managed_native_boundaries",
);
return managedNativeBoundaryInspectionSchema.parse(
managedEvidence.normalized_result,
): {
readonly evidence: Evidence;
readonly result: ManagedNativeBoundaryInspection;
} =>
parseManagedInspectionEvidence(
input.managed_boundaries,
"inspect_managed_native_boundaries",
managedNativeBoundaryInspectionSchema,
);
};
/** Verify managed P/Invoke declarations against authenticated native Evidence. */
export const verifyManagedNativeBoundaries = (
input: ManagedNativeVerificationInput,
): ManagedNativeVerificationResult => {
const managed = managedBoundaryFromInput(input);
const managedEvidence = parseEvidence(input.managed_boundaries);
const { evidence: managedEvidence, result: managed } =
parseManagedBoundary(input);
const native = collectNativeSymbols(input.native_observations);
const verifiedPinvokes = managed.pinvoke_imports.map((item) =>
verifyPinvoke({
@@ -10,7 +10,10 @@ import {
compareManagedMemberPaths,
} from "../../../../src/application/managed/ManagedMemberComparisonService.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
import { managedMemberComparisonResultSchema } from "../../../../src/domain/managed/managedMemberComparison.js";
import {
managedMemberComparisonResultSchema,
parseManagedMemberEvidence,
} from "../../../../src/domain/managed/managedMemberComparison.js";
import { createEvidence } from "../../../../src/domain/evidence.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { jsonValueSchema } from "../../../../src/domain/jsonValue.js";
@@ -54,6 +57,71 @@ it.each([
},
);
describe("managed member comparison subject identity", () => {
it("rejects authenticated member Evidence whose subject digest differs from its artifact", () => {
const bytes = buildManagedPeFixture();
const target = managedPeFixtureTarget(bytes, "/fixture/mismatch.dll");
const inspection = inspectManagedMembersBytes(bytes, target);
const otherDigest = "b".repeat(64);
const mismatched = createEvidence(
target,
{
id: "partial-fixture",
name: "Partial fixture",
version: "1",
},
{
operation: "inspect_managed_members",
parameters: {},
result: jsonValueSchema.parse({
...inspection,
artifact: { ...inspection.artifact, sha256: otherDigest },
identity_scope: {
...inspection.identity_scope,
requires_artifact_sha256: otherDigest,
},
}),
},
);
const aligned = createEvidence(
target,
{
id: "partial-fixture",
name: "Partial fixture",
version: "1",
},
{
operation: "inspect_managed_members",
parameters: {},
result: inspection,
},
);
const unavailableSubject = createEvidence(
undefined,
{
id: "partial-fixture",
name: "Partial fixture",
version: "1",
},
{
operation: "inspect_managed_members",
parameters: {},
result: inspection,
},
);
expect(() => parseManagedMemberEvidence(mismatched)).toThrow(
`Managed Evidence inspect_managed_members (${mismatched.evidence_id}) subject SHA-256 ${target.sha256} does not match normalized artifact SHA-256 ${otherDigest}`,
);
expect(parseManagedMemberEvidence(aligned).result.artifact.sha256).toBe(
target.sha256,
);
expect(
parseManagedMemberEvidence(unavailableSubject).result.artifact.sha256,
).toBe(target.sha256);
});
});
describe("managed member comparison path workflow", () => {
it("preserves undecoded signatures as unknown through content-addressed partial Evidence", () => {
const observe = (signature: number, path: string) => {
@@ -167,6 +167,48 @@ describe("managed application graph request", () => {
});
describe("managed application graph coverage", () => {
it("rejects managed Evidence digest mismatch and discloses missing artifact observations", () => {
const bytes = buildManagedPeFixture();
const binary = managedPeFixtureTarget(bytes, "/fixture/Mismatch.dll");
const members = inspectManagedMembersBytes(bytes, binary);
const mismatched = createEvidence(binary, MANAGED_STATIC_PROVIDER, {
operation: "inspect_managed_members",
parameters: {},
result: {
...members,
artifact: { ...members.artifact, sha256: "b".repeat(64) },
identity_scope: {
...members.identity_scope,
requires_artifact_sha256: "b".repeat(64),
},
},
});
const rejected = projectManagedApplicationGraphEvidence({
managed_members: mismatched,
});
expect(rejected.ok).toBe(false);
if (rejected.ok) throw new Error("mismatched managed Evidence accepted");
expect(JSON.stringify(rejected.error)).toContain(
`Managed Evidence inspect_managed_members (${mismatched.evidence_id}) subject SHA-256 ${binary.sha256} does not match normalized artifact SHA-256 ${"b".repeat(64)}`,
);
const aligned = createEvidence(binary, MANAGED_STATIC_PROVIDER, {
operation: "inspect_managed_members",
parameters: {},
result: members,
});
const projected = projectManagedApplicationGraphEvidence({
managed_members: aligned,
});
if (!projected.ok) throw projected.error;
const result = managedApplicationGraphResultSchema.parse(
parseEvidence(projected.value).normalized_result,
);
expect(result.limitations).toContain(
"Managed artifact Evidence was not supplied; assembly identity observations are absent.",
);
});
it("preserves partial parser coverage in graph and per-fact coverage", () => {
const bytes = buildManagedPeFixture();
const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe");
@@ -60,6 +60,54 @@ const nativeEvidenceWithExports = (names: readonly string[]) => {
);
};
describe("managed/native boundary Evidence identity", () => {
it("rejects managed boundary Evidence whose subject digest differs from its artifact", () => {
const input = exampleInput();
const managed = input.managed_boundaries;
const result = managedNativeBoundaryInspectionSchema.parse(
managed.normalized_result,
);
const otherDigest = "b".repeat(64);
const mismatched = createEvidence(
{
path: result.artifact.path,
sha256: result.artifact.sha256,
format: "pe",
},
managed.provider,
{
operation: managed.operation,
parameters: managed.parameters,
result: {
...result,
artifact: { ...result.artifact, sha256: otherDigest },
identity_scope: {
...result.identity_scope,
requires_artifact_sha256: otherDigest,
},
},
rawResult: managed.raw_result,
confidence: managed.confidence,
authority: managed.authority,
environment: managed.environment,
limitations: managed.limitations,
locations: managed.locations,
evidenceLinks: managed.evidence_links,
},
);
expect(() =>
verifyManagedNativeBoundaries({
...input,
managed_boundaries: mismatched,
}),
).toThrow(
`Managed Evidence inspect_managed_native_boundaries (${mismatched.evidence_id}) subject SHA-256 ${result.artifact.sha256} does not match normalized artifact SHA-256 ${otherDigest}`,
);
expect(verifyManagedNativeBoundaries(input).summary.verified).toBe(1);
});
});
describe("managed/native boundary verification", () => {
it("verifies a P/Invoke declaration against native export Evidence", () => {
const result = verifyManagedNativeBoundaries(exampleInput());