fix: close release provenance and privacy gaps

This commit is contained in:
2026-09-02 11:25:52 +03:00
parent 7da2b72933
commit 79e5d04395
17 changed files with 2271 additions and 704 deletions

View File

@@ -1,49 +1,60 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
const request = JSON.parse(await readFile("/input/request.json", "utf8"));
const [a, b] = request.reports ?? [];
if (
!a ||
!b ||
a.workerId !== "worker-a" ||
b.workerId !== "worker-b" ||
a.imageDigest === b.imageDigest ||
a.toolchain === b.toolchain ||
a.inputDigest !== b.inputDigest ||
a.checkId !== b.checkId ||
a.normalizedOutputDigest !== b.normalizedOutputDigest ||
JSON.stringify(a.normalizedMembers) !== JSON.stringify(b.normalizedMembers) ||
a.verdict !== "pass" ||
b.verdict !== "pass"
)
throw new Error("Independent worker evidence does not agree.");
const rules = request.rules;
if (!Array.isArray(rules) || rules.length === 0)
throw new Error("Matcher rules are required.");
const appliedRuleIds = rules
.map((rule) => rule.id)
.sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
if (
new Set(appliedRuleIds).size !== appliedRuleIds.length ||
rules.some((rule) => !Array.isArray(rule.deniedMemberDigests))
)
throw new Error("Matcher rules are invalid.");
const denied = new Set(rules.flatMap((rule) => rule.deniedMemberDigests));
if (a.normalizedMembers.some((member) => denied.has(member)))
throw new Error("Privacy rule matched normalized evidence.");
const result = {
schema: "portfolio-privacy-matcher-result/v1",
inputDigest: a.inputDigest,
checkId: a.checkId,
normalizedOutputDigest: a.normalizedOutputDigest,
normalizedMembers: a.normalizedMembers,
ruleSetDigest: request.ruleSetDigest,
appliedRuleIds,
verdict: "pass",
workerImageDigests: [a.imageDigest, b.imageDigest],
};
await writeFile("/output/result.json", JSON.stringify(result), {
flag: "wx",
mode: 0o400,
});
export function matchReports(request) {
const [a, b] = request.reports ?? [];
if (
!a ||
!b ||
a.workerId !== "worker-a" ||
b.workerId !== "worker-b" ||
a.imageDigest === b.imageDigest ||
a.toolchain === b.toolchain ||
a.inputDigest !== b.inputDigest ||
a.checkId !== b.checkId ||
a.normalizedOutputDigest !== b.normalizedOutputDigest ||
JSON.stringify(a.normalizedMembers) !==
JSON.stringify(b.normalizedMembers) ||
a.verdict !== "pass" ||
b.verdict !== "pass"
)
throw new Error("Independent worker evidence does not agree and pass.");
const rules = request.rules;
if (!Array.isArray(rules) || rules.length === 0)
throw new Error("Matcher rules are required.");
const appliedRuleIds = rules
.map((rule) => rule.id)
.sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
if (
new Set(appliedRuleIds).size !== appliedRuleIds.length ||
rules.some((rule) => !Array.isArray(rule.deniedMemberDigests))
)
throw new Error("Matcher rules are invalid.");
const denied = new Set(rules.flatMap((rule) => rule.deniedMemberDigests));
if (a.normalizedMembers.some((member) => denied.has(member)))
throw new Error("Privacy rule matched normalized evidence.");
return {
schema: "portfolio-privacy-matcher-result/v1",
inputDigest: a.inputDigest,
checkId: a.checkId,
normalizedOutputDigest: a.normalizedOutputDigest,
normalizedMembers: a.normalizedMembers,
ruleSetDigest: request.ruleSetDigest,
appliedRuleIds,
verdict: "pass",
workerImageDigests: [a.imageDigest, b.imageDigest],
};
}
async function main() {
const result = matchReports(
JSON.parse(await readFile("/input/request.json", "utf8")),
);
await writeFile("/output/result.json", JSON.stringify(result), {
flag: "wx",
mode: 0o400,
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
await main();

View File

@@ -1,33 +1,45 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
const request = JSON.parse(await readFile("/input/request.json", "utf8"));
if (
request.schema !== "portfolio-privacy-worker-request/v1" ||
request.workerId !== "worker-a" ||
!/^[a-f0-9]{64}$/u.test(request.inputDigest)
)
throw new Error("Invalid worker request.");
const bytes = await readFile("/input/object");
const actual = createHash("sha256").update(bytes).digest("hex");
if (actual !== request.inputDigest) throw new Error("Input digest mismatch.");
const normalizedMembers = [`bytes:${actual}`];
const normalizedOutputDigest = createHash("sha256")
.update(Buffer.from(JSON.stringify(normalizedMembers)))
.digest("hex");
const output = {
schema: "portfolio-privacy-worker-report/v1",
workerId: "worker-a",
imageDigest: request.imageDigest,
toolchain: "bytes-js/v1",
inputDigest: actual,
checkId: request.checkId,
normalizedOutputDigest,
normalizedMembers,
verdict: "pass",
findingsDigest: "0".repeat(64),
};
await writeFile("/output/report.json", JSON.stringify(output), {
flag: "wx",
mode: 0o400,
});
export function inspectObject(request, bytes) {
if (
request?.schema !== "portfolio-privacy-worker-request/v1" ||
request.workerId !== "worker-a" ||
!/^[a-f0-9]{64}$/u.test(request.inputDigest) ||
!/^[a-f0-9]{64}$/u.test(request.imageDigest) ||
!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u.test(request.checkId)
)
throw new Error("Invalid worker request.");
const actual = createHash("sha256").update(bytes).digest("hex");
if (actual !== request.inputDigest) throw new Error("Input digest mismatch.");
const normalizedMembers = [`unsupported:${actual}`];
return {
schema: "portfolio-privacy-worker-report/v1",
workerId: "worker-a",
imageDigest: request.imageDigest,
toolchain: "scanner-unavailable-js/v1",
inputDigest: actual,
checkId: request.checkId,
normalizedOutputDigest: createHash("sha256")
.update(Buffer.from(JSON.stringify(normalizedMembers)))
.digest("hex"),
normalizedMembers,
verdict: "unsupported",
findingsDigest: createHash("sha256")
.update("scanner-unavailable")
.digest("hex"),
};
}
async function main() {
const request = JSON.parse(await readFile("/input/request.json", "utf8"));
const output = inspectObject(request, await readFile("/input/object"));
await writeFile("/output/report.json", JSON.stringify(output), {
flag: "wx",
mode: 0o400,
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
await main();

View File

@@ -1,20 +1,23 @@
import hashlib
import json
with open('/input/request.json', 'r', encoding='utf-8') as handle:
request = json.load(handle)
if request.get('schema') != 'portfolio-privacy-worker-request/v1' or request.get('workerId') != 'worker-b':
raise ValueError('Invalid worker request.')
with open('/input/object', 'rb') as handle:
payload = handle.read()
actual = hashlib.sha256(payload).hexdigest()
if actual != request.get('inputDigest'):
raise ValueError('Input digest mismatch.')
members = ['bytes:' + actual]
normalized = json.dumps(members, separators=(',', ':')).encode('utf-8')
output = {'schema': 'portfolio-privacy-worker-report/v1', 'workerId': 'worker-b', 'imageDigest': request['imageDigest'],
'toolchain': 'bytes-python/v1', 'inputDigest': actual, 'checkId': request['checkId'],
'normalizedOutputDigest': hashlib.sha256(normalized).hexdigest(), 'normalizedMembers': members,
'verdict': 'pass', 'findingsDigest': '0' * 64}
with open('/output/report.json', 'x', encoding='utf-8') as handle:
json.dump(output, handle, separators=(',', ':'))
def inspect_object(request, payload):
if request.get('schema') != 'portfolio-privacy-worker-request/v1' or request.get('workerId') != 'worker-b':
raise ValueError('Invalid worker request.')
actual = hashlib.sha256(payload).hexdigest()
if actual != request.get('inputDigest'):
raise ValueError('Input digest mismatch.')
members = ['unsupported:' + actual]
normalized = json.dumps(members, separators=(',', ':')).encode('utf-8')
return {'schema': 'portfolio-privacy-worker-report/v1', 'workerId': 'worker-b', 'imageDigest': request['imageDigest'],
'toolchain': 'scanner-unavailable-python/v1', 'inputDigest': actual, 'checkId': request['checkId'],
'normalizedOutputDigest': hashlib.sha256(normalized).hexdigest(), 'normalizedMembers': members,
'verdict': 'unsupported', 'findingsDigest': hashlib.sha256(b'scanner-unavailable').hexdigest()}
if __name__ == '__main__':
with open('/input/request.json', 'r', encoding='utf-8') as handle:
request = json.load(handle)
with open('/input/object', 'rb') as handle:
payload = handle.read()
with open('/output/report.json', 'x', encoding='utf-8') as handle:
json.dump(inspect_object(request, payload), handle, separators=(',', ':'))