fix: close release provenance and privacy gaps
This commit is contained in:
@@ -140,85 +140,68 @@ async function writeOnce(root, digest, bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
export function reconcileScan({
|
||||
expectedChecks,
|
||||
reports,
|
||||
matcherResults,
|
||||
candidate,
|
||||
}) {
|
||||
if (
|
||||
!Array.isArray(expectedChecks) ||
|
||||
!Array.isArray(reports) ||
|
||||
!Array.isArray(matcherResults) ||
|
||||
expectedChecks.length === 0
|
||||
)
|
||||
function closureDigest(value, schema, label) {
|
||||
const closure = object(value, label);
|
||||
exact(closure, ["schema", "members"], label);
|
||||
if (closure.schema !== schema || !Array.isArray(closure.members))
|
||||
throw new Error(`${label} is invalid.`);
|
||||
return canonicalJsonDigest(schema, closure);
|
||||
}
|
||||
|
||||
export function reconcileScan(input) {
|
||||
const request = object(input, "reconciliation request");
|
||||
exact(
|
||||
request,
|
||||
[
|
||||
"sourceClosure",
|
||||
"publicObjectClosure",
|
||||
"reports",
|
||||
"matcherResults",
|
||||
"candidate",
|
||||
],
|
||||
"reconciliation request",
|
||||
);
|
||||
const {
|
||||
sourceClosure,
|
||||
publicObjectClosure,
|
||||
reports,
|
||||
matcherResults,
|
||||
candidate,
|
||||
} = request;
|
||||
if (!Array.isArray(reports) || !Array.isArray(matcherResults))
|
||||
throw new Error("Reconciliation input is invalid.");
|
||||
const ordered = [...expectedChecks].sort((a, b) =>
|
||||
Buffer.from(`${a.inputDigest}\0${a.checkId}`).compare(
|
||||
Buffer.from(`${b.inputDigest}\0${b.checkId}`),
|
||||
),
|
||||
const sourceClosureDigest = closureDigest(
|
||||
sourceClosure,
|
||||
"portfolio-publication-closure/v1",
|
||||
"source closure",
|
||||
);
|
||||
const publicObjectClosureDigest = closureDigest(
|
||||
publicObjectClosure,
|
||||
"portfolio-public-object-closure/v1",
|
||||
"public object closure",
|
||||
);
|
||||
if (
|
||||
!stringsEqual(expectedChecks, ordered) ||
|
||||
new Set(expectedChecks.map((v) => `${v.inputDigest}\0${v.checkId}`))
|
||||
.size !== expectedChecks.length
|
||||
)
|
||||
throw new Error("Expected check closure must be unique and byte sorted.");
|
||||
if (
|
||||
reports.length !== expectedChecks.length * 2 ||
|
||||
matcherResults.length !== expectedChecks.length
|
||||
)
|
||||
throw new Error("Missing or extra privacy evidence.");
|
||||
candidate?.sourceClosureDigest !== sourceClosureDigest ||
|
||||
candidate?.publicObjectClosureDigest !== publicObjectClosureDigest
|
||||
) {
|
||||
throw new Error(
|
||||
"Candidate does not bind the exact canonical source/public closure.",
|
||||
);
|
||||
}
|
||||
if (publicObjectClosure.members.length !== 0) {
|
||||
throw new Error(
|
||||
"Verified privacy scanners are unavailable; every non-empty public object closure fails closed.",
|
||||
);
|
||||
}
|
||||
if (reports.length !== 0 || matcherResults.length !== 0)
|
||||
throw new Error(
|
||||
"Empty public object closure cannot carry fabricated scan evidence.",
|
||||
);
|
||||
const checks = [];
|
||||
expectedChecks.forEach((expected, index) => {
|
||||
if (!isSha256Hex(expected.inputDigest) || !safeToken.test(expected.checkId))
|
||||
throw new Error("Expected check is invalid.");
|
||||
const a = validateWorker(reports[index * 2], "worker-a");
|
||||
const b = validateWorker(reports[index * 2 + 1], "worker-b");
|
||||
const match = validateMatcher(matcherResults[index]);
|
||||
for (const item of [a, b, match])
|
||||
if (
|
||||
item.inputDigest !== expected.inputDigest ||
|
||||
item.checkId !== expected.checkId
|
||||
)
|
||||
throw new Error(
|
||||
"Privacy evidence is missing, reordered, or bound to another check.",
|
||||
);
|
||||
if (a.imageDigest === b.imageDigest || a.toolchain === b.toolchain)
|
||||
throw new Error("Privacy workers are not heterogeneous.");
|
||||
if (
|
||||
a.verdict !== "pass" ||
|
||||
b.verdict !== "pass" ||
|
||||
match.verdict !== "pass"
|
||||
)
|
||||
throw new Error("Every worker and matcher must independently pass.");
|
||||
if (
|
||||
a.normalizedOutputDigest !== b.normalizedOutputDigest ||
|
||||
a.normalizedOutputDigest !== match.normalizedOutputDigest ||
|
||||
!stringsEqual(a.normalizedMembers, b.normalizedMembers) ||
|
||||
!stringsEqual(a.normalizedMembers, match.normalizedMembers) ||
|
||||
!stringsEqual(match.workerImageDigests, [a.imageDigest, b.imageDigest])
|
||||
)
|
||||
throw new Error("Independent normalized evidence disagrees.");
|
||||
checks.push({
|
||||
inputDigest: expected.inputDigest,
|
||||
checkId: expected.checkId,
|
||||
normalizedOutputDigest: a.normalizedOutputDigest,
|
||||
normalizedMembers: a.normalizedMembers,
|
||||
workerReports: [
|
||||
canonicalJsonDigest("portfolio-privacy-worker-report/v1", a),
|
||||
canonicalJsonDigest("portfolio-privacy-worker-report/v1", b),
|
||||
],
|
||||
matcherResult: canonicalJsonDigest(
|
||||
"portfolio-privacy-matcher-result/v1",
|
||||
match,
|
||||
),
|
||||
ruleSetDigest: match.ruleSetDigest,
|
||||
appliedRuleIds: match.appliedRuleIds,
|
||||
});
|
||||
});
|
||||
const evidence = {
|
||||
schema: "portfolio-scan-evidence/v1",
|
||||
sourceClosureDigest,
|
||||
publicObjectClosureDigest,
|
||||
coverageCount: checks.length,
|
||||
passedCount: checks.length,
|
||||
checks,
|
||||
|
||||
Reference in New Issue
Block a user