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

@@ -9,19 +9,22 @@ import {
canonicalValueBytes,
} from "../../src/security/canonical-digest.ts";
import { computeProductionInputRoot } from "../../src/security/production-input-root.ts";
import { APPROVAL_PROJECTION_ATTESTATION_SCOPE } from "../../src/security/approval-closure.ts";
// @ts-expect-error executable release modules intentionally have no browser declarations
import * as releaseSigner from "../../scripts/release/sign-release.mjs";
const {
signReleaseCandidate,
signReleaseCandidateForTest,
validateTrustPolicy,
verifySignedReleaseEnvelope,
verifySignedReleaseEnvelopeForTest,
} = releaseSigner;
const h = (c: string) => c.repeat(64);
const roles = [
["approval", "portfolio-approval-decision/v1"],
["projection", "portfolio-approval-projection/v1"],
["projection", APPROVAL_PROJECTION_ATTESTATION_SCOPE],
["privacyWorkload", "portfolio-privacy-workload/v1"],
["policy", "portfolio-policy-attestation/v1"],
["toolImage", "portfolio-tool-image-provenance/v1"],
@@ -91,6 +94,9 @@ function envelope(
...payload,
};
return {
...(role === "projection"
? { schema: "portfolio-approval-projection-attestation-envelope/v1" }
: {}),
payload: body,
signature: {
algorithm: "Ed25519",
@@ -142,12 +148,27 @@ function fixture() {
expiresAt: "2026-09-02T12:00:00Z",
minimumBoundExpiry: "2026-09-02T12:00:00Z",
};
const sourceClosure = {
schema: "portfolio-publication-closure/v1",
members: [],
};
const publicObjectClosure = {
schema: "portfolio-public-object-closure/v1",
members: [],
};
const candidate = {
schema: "portfolio-candidate-release-manifest/v1",
releaseManifest,
latestLedgerHeadHash: h("1"),
latestLedgerRevision: 7,
sourceClosureDigest: h("4"),
sourceClosureDigest: canonicalJsonDigest(
"portfolio-publication-closure/v1",
sourceClosure,
),
publicObjectClosureDigest: canonicalJsonDigest(
"portfolio-public-object-closure/v1",
publicObjectClosure,
),
publicRefMap: {},
lfsOids: [],
releaseArchives: [],
@@ -157,40 +178,40 @@ function fixture() {
};
const evidence = {
schema: "portfolio-scan-evidence/v1",
coverageCount: 1,
passedCount: 1,
checks: [
{
inputDigest: h("5"),
checkId: "bytes",
normalizedOutputDigest: h("6"),
normalizedMembers: [`bytes:${h("7")}`],
workerReports: [h("8"), h("9")],
matcherResult: h("a"),
ruleSetDigest: h("b"),
appliedRuleIds: ["denylist-v1"],
},
],
sourceClosureDigest: candidate.sourceClosureDigest,
publicObjectClosureDigest: candidate.publicObjectClosureDigest,
coverageCount: 0,
passedCount: 0,
checks: [],
};
const evidenceDigest = canonicalJsonDigest(
"portfolio-scan-evidence/v1",
evidence,
);
candidate.releaseManifest.scanEvidenceDigest = evidenceDigest;
const projection = envelope("projection", roles[1][1], keyPairs, {
approvedRecordRoot: h("9"),
approvedAssetRoot: h("e"),
approvalProofSetDigest: h("f"),
inputDigests,
productionInputRoot: releaseManifest.productionInputRoot,
liveLedgerHeadHash: h("1"),
liveLedgerRevision: 7,
policyVersion: "2026-09-02",
expiresAt: "2026-09-02T12:00:00Z",
earliestApprovalExpiry: "2026-09-02T12:00:00Z",
});
candidate.releaseManifest.approvalProjectionAttestationDigest =
canonicalJsonDigest(
"portfolio-approval-projection-attestation-envelope/v1",
projection,
);
const candidateDigest = canonicalJsonDigest(
"portfolio-candidate-release-manifest/v1",
candidate,
);
const attestations = {
projection: envelope("projection", roles[1][1], keyPairs, {
digest: h("2"),
productionInputRoot: releaseManifest.productionInputRoot,
approvedRecordRoot: h("9"),
approvedAssetRoot: h("e"),
approvalProofSetDigest: h("f"),
latestLedgerHeadHash: h("1"),
latestLedgerRevision: 7,
}),
projection,
privacyWorkload: envelope("privacyWorkload", roles[2][1], keyPairs, {
digest: h("2"),
candidateDigest,
@@ -207,6 +228,8 @@ function fixture() {
};
return {
keyPairs,
sourceClosure,
publicObjectClosure,
candidate,
evidence,
candidateDigest,
@@ -217,6 +240,54 @@ function fixture() {
}
describe("external scoped release signing", () => {
test("production signer/verifier reject caller-selected time and signer reconstructs exact source/public closure", async () => {
const f = fixture();
const dir = await mkdtemp(join(tmpdir(), "portfolio-release-key-"));
const keyPath = join(dir, "release.pem");
await writeFile(
keyPath,
f.keyPairs.release!.privateKey.export({ type: "pkcs8", format: "pem" }),
{ mode: 0o400 },
);
await expect(
signReleaseCandidate({
...f,
releaseKeyPath: keyPath,
now: "2000-01-01T00:00:00Z",
protectedState: {
latestLedgerHeadHash: h("1"),
latestLedgerRevision: 7,
currentManifestDigest: null,
publisherFence: 4,
},
}),
).rejects.toThrow(/now|unknown|caller/i);
const nonEmptyPublic = {
schema: "portfolio-public-object-closure/v1",
members: [
{
objectId: "private.txt",
inputDigest: h("d"),
requiredChecks: ["secret"],
},
],
};
await expect(
signReleaseCandidateForTest({
...f,
publicObjectClosure: nonEmptyPublic,
releaseKeyPath: keyPath,
now: "2026-09-02T01:00:00Z",
protectedState: {
latestLedgerHeadHash: h("1"),
latestLedgerRevision: 7,
currentManifestDigest: null,
publisherFence: 4,
},
}),
).rejects.toThrow(/closure|digest|non-empty/i);
});
test("signs exact canonical candidate bytes with an external Ed25519 key", async () => {
const f = fixture();
const dir = await mkdtemp(join(tmpdir(), "portfolio-release-key-"));
@@ -226,7 +297,7 @@ describe("external scoped release signing", () => {
f.keyPairs.release!.privateKey.export({ type: "pkcs8", format: "pem" }),
{ mode: 0o400 },
);
const result = await signReleaseCandidate({
const result = await signReleaseCandidateForTest({
...f,
releaseKeyPath: keyPath,
now: "2026-09-02T01:00:00Z",
@@ -247,7 +318,7 @@ describe("external scoped release signing", () => {
),
).toBe(true);
expect(
verifySignedReleaseEnvelope({
verifySignedReleaseEnvelopeForTest({
signedRelease: result,
trustPolicy: f.trustPolicy,
now: "2026-09-02T01:00:00Z",
@@ -285,14 +356,14 @@ describe("external scoped release signing", () => {
currentManifestDigest: null,
publisherFence: 4,
};
const signedRelease = await signReleaseCandidate({
const signedRelease = await signReleaseCandidateForTest({
...f,
releaseKeyPath: keyPath,
now: "2026-09-02T01:00:00Z",
protectedState: state,
});
expect(() =>
verifySignedReleaseEnvelope({
verifySignedReleaseEnvelopeForTest({
signedRelease,
trustPolicy: f.trustPolicy,
now: "2026-09-02T01:00:00Z",
@@ -300,7 +371,7 @@ describe("external scoped release signing", () => {
}),
).toThrow(/stale/i);
expect(() =>
verifySignedReleaseEnvelope({
verifySignedReleaseEnvelopeForTest({
signedRelease: {
...signedRelease,
payload: { ...signedRelease.payload, latestLedgerRevision: 8 },
@@ -315,7 +386,7 @@ describe("external scoped release signing", () => {
test("refuses repository-resident keys and non-external key material", async () => {
const f = fixture();
await expect(
signReleaseCandidate({
signReleaseCandidateForTest({
...f,
releaseKeyPath: join(process.cwd(), "release.pem"),
now: "2026-09-02T01:00:00Z",
@@ -424,7 +495,7 @@ describe("external scoped release signing", () => {
{ mode: 0o400 },
);
await expect(
signReleaseCandidate({
signReleaseCandidateForTest({
...f,
releaseKeyPath: keyPath,
now: "2026-09-02T01:00:00Z",