fix: close release provenance and privacy gaps
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user