mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(managed): bind inspection evidence to artifact identity
This commit is contained in:
1 parent
dbcf8c3391
commit
73b09f4c5c
8 files changed
+222
-35
No files matched your search
@@ -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:
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user