witnora 0.9.3 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -9
- package/dist/assurance-contract.js +312 -0
- package/dist/assurance-loop-demo.js +245 -0
- package/dist/authorization-state-machine.js +35 -0
- package/dist/authorization-v02.js +60 -0
- package/dist/bundle.js +22 -0
- package/dist/canonical-v02.js +197 -0
- package/dist/canonical.js +31 -0
- package/dist/cli.js +132 -0
- package/dist/command-help.js +81 -0
- package/dist/control-plane.js +12 -3
- package/dist/credentials.js +1 -1
- package/dist/design-partner-pilot.js +402 -0
- package/dist/design-partner-v02.js +449 -0
- package/dist/evidence-v02.js +118 -0
- package/dist/generic-eval.js +160 -0
- package/dist/github-design-partner-v02.js +399 -0
- package/dist/github-live-v02.js +45 -0
- package/dist/guided-setup.js +1 -1
- package/dist/index.js +1 -0
- package/dist/offline-verifier.js +375 -0
- package/dist/onboard.js +132 -0
- package/dist/onboarding-templates.js +49 -6
- package/dist/outcome-evaluator.js +51 -0
- package/dist/privacy-manifest.js +273 -0
- package/dist/probe-protocol-v02.js +101 -0
- package/dist/runtime-context.js +75 -0
- package/dist/sandbox.js +1 -1
- package/dist/schema-validator.js +141 -3
- package/dist/trust-v02.js +111 -0
- package/dist/try.js +1 -1
- package/dist/vendor/design-partner/failure-exercises.mjs +353 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +8 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -1
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +43 -1
- package/dist/vendor/witnora-probe/canonical.d.ts +3 -0
- package/dist/vendor/witnora-probe/canonical.js +39 -0
- package/dist/vendor/witnora-probe/cli.d.ts +2 -0
- package/dist/vendor/witnora-probe/cli.js +36 -0
- package/dist/vendor/witnora-probe/config.d.ts +2 -0
- package/dist/vendor/witnora-probe/config.js +63 -0
- package/dist/vendor/witnora-probe/credential-resolver.d.ts +6 -0
- package/dist/vendor/witnora-probe/credential-resolver.js +17 -0
- package/dist/vendor/witnora-probe/crypto.d.ts +2 -0
- package/dist/vendor/witnora-probe/crypto.js +14 -0
- package/dist/vendor/witnora-probe/emulator.d.ts +51 -0
- package/dist/vendor/witnora-probe/emulator.js +164 -0
- package/dist/vendor/witnora-probe/index.d.ts +11 -0
- package/dist/vendor/witnora-probe/index.js +11 -0
- package/dist/vendor/witnora-probe/logger.d.ts +3 -0
- package/dist/vendor/witnora-probe/logger.js +26 -0
- package/dist/vendor/witnora-probe/metrics.d.ts +12 -0
- package/dist/vendor/witnora-probe/metrics.js +31 -0
- package/dist/vendor/witnora-probe/probe.d.ts +22 -0
- package/dist/vendor/witnora-probe/probe.js +208 -0
- package/dist/vendor/witnora-probe/request-verifier.d.ts +9 -0
- package/dist/vendor/witnora-probe/request-verifier.js +84 -0
- package/dist/vendor/witnora-probe/server.d.ts +9 -0
- package/dist/vendor/witnora-probe/server.js +85 -0
- package/dist/vendor/witnora-probe/storage.d.ts +16 -0
- package/dist/vendor/witnora-probe/storage.js +114 -0
- package/dist/vendor/witnora-probe/types.d.ts +126 -0
- package/dist/vendor/witnora-probe/types.js +2 -0
- package/dist/vendor/witnora-verifier-python/README.md +43 -0
- package/dist/vendor/witnora-verifier-python/pyproject.toml +23 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/__init__.py +33 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/canonical.py +218 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/cli.py +75 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/conformance.py +124 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/crypto.py +139 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/errors.py +15 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/event_chain.py +80 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/evidence.py +336 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/probe.py +188 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/producer.py +127 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/trust.py +337 -0
- package/package.json +5 -5
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { compileAssuranceContract, exampleAssuranceContract } from "./assurance-contract.js";
|
|
4
|
+
import { examplePrivacyManifest } from "./privacy-manifest.js";
|
|
5
|
+
import { collectSupportBundle, formatDoctorReport, formatPrivacyInspection, genericEvalExample, initializePilotState, markPilotLifecycle, pilotPackageFiles, referenceReleaseCandidateManifest, runPilotDoctor, runPilotLifecycleDemonstration, snapshotPilotRuntime, writePilotAcceptanceReport } from "./design-partner-pilot.js";
|
|
6
|
+
import { createAcceptanceTrustMaterial, createGitHubAcceptanceIdentity, executionBoundaryDigest, githubActionDigest, issueAcceptanceAuthorization, readGitHubTargetState, revokeProbeCertificate, rotateProbeCertificate, runDesignPartnerAcceptance, startGitHubTargetEmulator } from "./github-design-partner-v02.js";
|
|
7
|
+
import { verifyEvidencePacketV02 } from "./evidence-v02.js";
|
|
8
|
+
import { assertLiveGitHubSafety, readLiveGitHubPullRequest } from "./github-live-v02.js";
|
|
9
|
+
export function hardenedDesignPartnerCompose(relativeRoot = ".") {
|
|
10
|
+
return baseDesignPartnerCompose()
|
|
11
|
+
.replace("design-partner target-emulator --state", "design-partner target-emulator --dir /kit --state")
|
|
12
|
+
.replace(" - acceptance-data:/output\n", " - ./acceptance:/output\n")
|
|
13
|
+
.replace(" - acceptance-data:/output:ro\n", " - ./acceptance:/output:ro\n")
|
|
14
|
+
.replaceAll("context: \${WITNORA_SOURCE_ROOT:-../..}/packages/witnora-cli\n dockerfile: Dockerfile.design-partner", "context: ./docker/runtime\n dockerfile: Dockerfile")
|
|
15
|
+
.replace("context: \${WITNORA_SOURCE_ROOT:-../..}/packages/witnora-probe\n command: [\"node\", \"dist/cli.js\"]", "context: ./docker/runtime\n dockerfile: Dockerfile\n entrypoint: [\"node\", \"/app/dist/vendor/witnora-probe/cli.js\"]")
|
|
16
|
+
.replace(/^\s{2}acceptance-data: \{\}\r?\n/mu, "")
|
|
17
|
+
.replaceAll("- ./", `- ${relativeRoot}/`)
|
|
18
|
+
.replaceAll("file: ./secrets/", `file: ${relativeRoot}/secrets/`)
|
|
19
|
+
.replaceAll("context: ./docker/runtime", `context: ${relativeRoot}/docker/runtime`);
|
|
20
|
+
}
|
|
21
|
+
export async function runDesignPartnerCommand(args) {
|
|
22
|
+
const action = args[0] ?? "help";
|
|
23
|
+
if (action === "init")
|
|
24
|
+
return init(args);
|
|
25
|
+
if (action === "doctor")
|
|
26
|
+
return doctor(args);
|
|
27
|
+
if (action === "dry-run")
|
|
28
|
+
return dryRun(args);
|
|
29
|
+
if (action === "run-acceptance")
|
|
30
|
+
return acceptance(args);
|
|
31
|
+
if (action === "recover-acceptance")
|
|
32
|
+
return acceptance([...args, "--recover-only"]);
|
|
33
|
+
if (action === "prepare-live-github")
|
|
34
|
+
return prepareLiveGitHub(args);
|
|
35
|
+
if (action === "authorization" && args[1] === "issue")
|
|
36
|
+
return issueAuthorization(args);
|
|
37
|
+
if (action === "verify-offline")
|
|
38
|
+
return verifyOffline(args);
|
|
39
|
+
if (action === "privacy" && args[1] === "inspect")
|
|
40
|
+
return inspectPrivacy(args);
|
|
41
|
+
if (action === "collect-support-bundle")
|
|
42
|
+
return supportBundle(args);
|
|
43
|
+
if (action === "pilot-report")
|
|
44
|
+
return pilotReport(args);
|
|
45
|
+
if (action === "pilot-lifecycle")
|
|
46
|
+
return pilotLifecycle(args);
|
|
47
|
+
if (action === "trust" && args[1] === "inspect")
|
|
48
|
+
return inspectTrust(args);
|
|
49
|
+
if (action === "trust" && args[1] === "revoke-probe")
|
|
50
|
+
return revokeProbe(args);
|
|
51
|
+
if (action === "trust" && args[1] === "rotate-probe")
|
|
52
|
+
return rotateProbe(args);
|
|
53
|
+
if (action === "target-emulator")
|
|
54
|
+
return targetEmulator(args);
|
|
55
|
+
if (action === "help" || action === "--help" || action === "-h") {
|
|
56
|
+
process.stdout.write(help());
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Unknown design-partner command: ${action}. Run 'witnora design-partner --help'.`);
|
|
60
|
+
}
|
|
61
|
+
async function init(args) {
|
|
62
|
+
if ((args[1] ?? "github-pr-merge") !== "github-pr-merge")
|
|
63
|
+
throw new Error("Only github-pr-merge is supported in v0.2.");
|
|
64
|
+
const outDir = resolve(flag(args, "--out") ?? ".witnora/design-partner");
|
|
65
|
+
const trustDir = join(outDir, "trust");
|
|
66
|
+
const secretsDir = join(outDir, "secrets");
|
|
67
|
+
await Promise.all([mkdir(trustDir, { recursive: true }), mkdir(secretsDir, { recursive: true })]);
|
|
68
|
+
const declaredTarget = { owner: "witnora-design-partner", repository: "assurance-sandbox", pullRequestNumber: 17, baseBranch: "acceptance", baseSha: "base-a", headBranch: "agent-change", headSha: "head-a", open: true, merged: false, checks: { build: "success", policy: "success" }, mergeable: true, targetVersion: 1 };
|
|
69
|
+
const config = { protocolVersion: "witnora.design_partner_github.v0.2", tenantRef: "tenant:design-partner", projectRef: "project:github-sandbox", environment: "sandbox", targetUrl: "http://target-sandbox:4311", probeUrl: "http://probe:4312", readCredentialEnv: "WITNORA_GITHUB_READ_CREDENTIAL", writeCredentialEnv: "WITNORA_GITHUB_WRITE_CREDENTIAL", trustDir: "./trust", legacyUnprotectedPathEnabled: false, declaredTarget };
|
|
70
|
+
const trust = createAcceptanceTrustMaterial({ tenantRef: config.tenantRef, projectRef: config.projectRef, environment: config.environment });
|
|
71
|
+
const contract = exampleAssuranceContract();
|
|
72
|
+
contract.subject.agentId = "design-partner-github-agent";
|
|
73
|
+
contract.subject.environment = "sandbox";
|
|
74
|
+
contract.capability.id = "github.pull_request.merge";
|
|
75
|
+
contract.capability.action = "UPDATE";
|
|
76
|
+
const files = {
|
|
77
|
+
"design-partner.json": JSON.stringify(config, null, 2),
|
|
78
|
+
"witnora.assurance-contract.json": JSON.stringify(contract, null, 2),
|
|
79
|
+
"witnora.privacy-manifest.json": JSON.stringify(examplePrivacyManifest(), null, 2),
|
|
80
|
+
".env.example": "# Synthetic emulator values only; do not reuse for a live sandbox.\nWITNORA_GITHUB_READ_CREDENTIAL=local-emulator-read-only\nWITNORA_GITHUB_WRITE_CREDENTIAL=local-emulator-write-only\nWITNORA_TARGET_URL=http://127.0.0.1:4311\nWITNORA_PROBE_URL=http://127.0.0.1:4312\nWITNORA_PROBE_STORAGE_KEY_BASE64=AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=\n",
|
|
81
|
+
".gitignore": "secrets/\nprobe/data/\nacceptance/\nclaims/\nsupport-bundle/\n.env\n",
|
|
82
|
+
"sandbox-policy.json": JSON.stringify({ repositories: ["witnora-design-partner/assurance-sandbox"], protectedBranches: ["main", "master"], liveModeDefault: false }, null, 2),
|
|
83
|
+
"acceptance-checklist.md": checklist(),
|
|
84
|
+
"docker-compose.yml": hardenedDesignPartnerCompose("."),
|
|
85
|
+
"docker/compose.yml": hardenedDesignPartnerCompose(".."),
|
|
86
|
+
"trust/pinned-root.json": JSON.stringify(trust.root, null, 2),
|
|
87
|
+
"trust/gateway-certificate.json": JSON.stringify(trust.gatewayCertificate, null, 2),
|
|
88
|
+
"trust/request-signer-certificate.json": JSON.stringify(trust.requestSignerCertificate, null, 2),
|
|
89
|
+
"trust/probe-certificate.json": JSON.stringify(trust.probeCertificate, null, 2),
|
|
90
|
+
"trust/approval-certificate.json": JSON.stringify(trust.approvalCertificate, null, 2),
|
|
91
|
+
"trust/grant-certificate.json": JSON.stringify(trust.grantCertificate, null, 2),
|
|
92
|
+
"trust/revocation-journal.json": JSON.stringify(trust.journal, null, 2),
|
|
93
|
+
"probe/config.json": JSON.stringify(probeConfig(config, trust, "http://127.0.0.1:4311", "./data", "../secrets/probe-private.pem"), null, 2),
|
|
94
|
+
"probe/config.container.json": JSON.stringify(probeConfig(config, trust, "http://target-sandbox:4311", "/app/data", "/app/keys/probe-private.pem"), null, 2),
|
|
95
|
+
"probe/request-signer-public.pem": trust.requestSignerCertificate.publicKeyPem,
|
|
96
|
+
"secrets/root-private.pem": trust.rootPrivateKeyPem,
|
|
97
|
+
"secrets/request-signer-private.pem": trust.requestSignerPrivateKeyPem,
|
|
98
|
+
"secrets/gateway-private.pem": trust.gatewayPrivateKeyPem,
|
|
99
|
+
"secrets/probe-private.pem": trust.probePrivateKeyPem,
|
|
100
|
+
"secrets/approval-private.pem": trust.approvalPrivateKeyPem,
|
|
101
|
+
"secrets/grant-private.pem": trust.grantPrivateKeyPem,
|
|
102
|
+
};
|
|
103
|
+
const customerFiles = pilotPackageFiles(config);
|
|
104
|
+
customerFiles["contract/assurance-contract.json"] = JSON.stringify(contract, null, 2);
|
|
105
|
+
customerFiles["privacy/privacy-manifest.json"] = JSON.stringify(examplePrivacyManifest(), null, 2);
|
|
106
|
+
customerFiles["examples/generic-eval.json"] = JSON.stringify(genericEvalExample(compileAssuranceContract(contract).revalidationFingerprint), null, 2);
|
|
107
|
+
Object.assign(files, customerFiles);
|
|
108
|
+
await Promise.all(Object.entries(files).map(async ([relative, content]) => { const path = join(outDir, relative); await mkdir(resolve(path, ".."), { recursive: true }); await writeFile(path, `${content.trimEnd()}\n`, { flag: "wx" }); }));
|
|
109
|
+
for (const secret of [join(secretsDir, "root-private.pem"), join(secretsDir, "request-signer-private.pem"), join(secretsDir, "gateway-private.pem"), join(secretsDir, "probe-private.pem"), join(secretsDir, "approval-private.pem"), join(secretsDir, "grant-private.pem")])
|
|
110
|
+
await chmod(secret, 0o600).catch(() => undefined);
|
|
111
|
+
await writeFile(join(outDir, "target-state.json"), `${JSON.stringify(declaredTarget, null, 2)}\n`);
|
|
112
|
+
await snapshotPilotRuntime(outDir);
|
|
113
|
+
await Promise.all([mkdir(join(outDir, "acceptance"), { recursive: true }), mkdir(join(outDir, "claims"), { recursive: true })]);
|
|
114
|
+
await initializePilotState(outDir);
|
|
115
|
+
await writeFile(join(outDir, "release-candidate-manifest.json"), `${JSON.stringify(referenceReleaseCandidateManifest(), null, 2)}\n`);
|
|
116
|
+
process.stdout.write(`Initialized the self-contained GitHub design-partner kit at ${outDir}\n\nNext:\n 1. Open QUICKSTART.md. The first run uses local synthetic credentials only.\n 2. Issue a short-lived signed approval and Grant.\n 3. Start the Docker Compose topology.\n 4. Run doctor, privacy inspect, and dry-run.\n 5. Run acceptance only against the allowlisted sandbox.\n`);
|
|
117
|
+
}
|
|
118
|
+
async function doctor(args) {
|
|
119
|
+
const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner");
|
|
120
|
+
const report = await runPilotDoctor(dir, await readConfig(dir));
|
|
121
|
+
process.stdout.write(formatDoctorReport(report));
|
|
122
|
+
if (!report.ready)
|
|
123
|
+
throw new Error(`Design-partner doctor found ${report.summary.FAIL + report.summary.NOT_CONFIGURED} unsafe or incomplete condition(s).`);
|
|
124
|
+
}
|
|
125
|
+
async function dryRun(args) {
|
|
126
|
+
const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner");
|
|
127
|
+
const config = await readConfig(dir);
|
|
128
|
+
const readCredential = requiredEnvironment(config.readCredentialEnv);
|
|
129
|
+
const targetUrl = flag(args, "--target-url") ?? process.env.WITNORA_TARGET_URL ?? config.targetUrl;
|
|
130
|
+
const before = await readGitHubTargetState(targetUrl, readCredential);
|
|
131
|
+
if (before.merged)
|
|
132
|
+
throw new Error("Dry-run target is already merged; reset the sandbox fixture.");
|
|
133
|
+
process.stdout.write(`PASS Contract validation\nPASS Canonical target identity preview\nPASS Policy requires exact approval\nPASS Read-only target precondition\nPASS No target write performed\nTarget: ${before.owner}/${before.repository}#${before.pullRequestNumber}\n`);
|
|
134
|
+
}
|
|
135
|
+
async function acceptance(args) {
|
|
136
|
+
const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner");
|
|
137
|
+
const config = await readConfig(dir);
|
|
138
|
+
const outDir = flag(args, "--out") ?? join(dir, "acceptance");
|
|
139
|
+
const authorization = await readAuthorization(dir);
|
|
140
|
+
const outcome = await runDesignPartnerAcceptance({ outDir, attemptStatePath: join(outDir, "gateway-attempt-v0.2.json"), grantClaimDirectory: process.env.WITNORA_GRANT_CLAIM_DIR ?? join(dir, "claims"), targetUrl: flag(args, "--target-url") ?? process.env.WITNORA_TARGET_URL ?? config.targetUrl, probeUrl: flag(args, "--probe-url") ?? process.env.WITNORA_PROBE_URL ?? config.probeUrl, writeCredential: requiredEnvironment(config.writeCredentialEnv), declaredTarget: config.declaredTarget, tenantRef: config.tenantRef, projectRef: config.projectRef, environment: config.environment, trustMaterial: await readGatewayTrustMaterial(dir), authorization, crashAfterTargetSuccess: args.includes("--crash-after-target-success"), crashAfterTargetAcknowledged: args.includes("--crash-after-target-acknowledged"), recoverOnly: args.includes("--recover-only") });
|
|
141
|
+
const now = new Date().toISOString();
|
|
142
|
+
await markPilotLifecycle(dir, { firstSignedObservationAt: now, firstTargetDispatchAt: now, ...(outcome.report.overall === "PASS" ? { firstVerifiedOutcomeAt: now } : {}), recoveryRequiredCount: args.includes("--recover-only") ? 1 : 0 });
|
|
143
|
+
await writePilotAcceptanceReport(dir);
|
|
144
|
+
process.stdout.write(`PASS 1/5 Approval and one-use Grant validated\nPASS 2/5 Durable dispatch journal written\nPASS 3/5 Target dispatch completed or reconciled\nPASS 4/5 Independent Probe observation received\n${outcome.report.overall === "PASS" ? "PASS" : "FAIL"} 5/5 Evidence Packet verified\nEvidence Packet v0.2: ${outcome.paths.packet}\nOffline verification: ${outcome.report.overall}\nMetrics: ${JSON.stringify(outcome.metrics)}\n`);
|
|
145
|
+
if (outcome.report.overall !== "PASS")
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
}
|
|
148
|
+
async function prepareLiveGitHub(args) {
|
|
149
|
+
const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner");
|
|
150
|
+
const config = await readConfig(dir);
|
|
151
|
+
const repository = requiredFlag(args, "--repository");
|
|
152
|
+
const pullRequestNumber = Number(requiredFlag(args, "--pull-request"));
|
|
153
|
+
const [owner, name, ...rest] = repository.split("/");
|
|
154
|
+
if (!owner || !name || rest.length > 0 || !Number.isSafeInteger(pullRequestNumber) || pullRequestNumber < 1) {
|
|
155
|
+
throw new Error("Live GitHub target must use --repository owner/name and a positive --pull-request number.");
|
|
156
|
+
}
|
|
157
|
+
const allowlist = requiredEnvironment("WITNORA_LIVE_GITHUB_ALLOWLIST");
|
|
158
|
+
const target = { owner, repository: name, pullRequestNumber };
|
|
159
|
+
const state = await readLiveGitHubPullRequest({
|
|
160
|
+
...target,
|
|
161
|
+
credential: requiredEnvironment(config.readCredentialEnv),
|
|
162
|
+
requiredChecks: Object.keys(config.declaredTarget.checks),
|
|
163
|
+
apiBase: flag(args, "--api-base"),
|
|
164
|
+
});
|
|
165
|
+
assertLiveGitHubSafety(target, state.baseBranch, allowlist);
|
|
166
|
+
if (state.merged || !state.open)
|
|
167
|
+
throw new Error("Live GitHub sandbox pull request must be open and unmerged.");
|
|
168
|
+
config.declaredTarget = state;
|
|
169
|
+
await writeFile(join(dir, "design-partner.json"), `${JSON.stringify(config, null, 2)}\n`);
|
|
170
|
+
const trust = await readGatewayTrustMaterial(dir);
|
|
171
|
+
const liveProbeConfig = probeConfig(config, trust, "", "./data-live", "../secrets/probe-private.pem");
|
|
172
|
+
delete liveProbeConfig.githubTarget;
|
|
173
|
+
liveProbeConfig.githubLive = {
|
|
174
|
+
apiBase: flag(args, "--api-base") ?? "https://api.github.com",
|
|
175
|
+
requiredCredentialRef: `env:${config.readCredentialEnv}`,
|
|
176
|
+
};
|
|
177
|
+
await writeFile(join(dir, "probe/config.live.json"), `${JSON.stringify(liveProbeConfig, null, 2)}\n`);
|
|
178
|
+
process.stdout.write(`Prepared live GitHub sandbox ${repository}#${pullRequestNumber}. Review the exact SHAs, then issue fresh authorization before dispatch.\n`);
|
|
179
|
+
}
|
|
180
|
+
async function verifyOffline(args) {
|
|
181
|
+
const packetPath = flag(args, "--packet") ?? args[1];
|
|
182
|
+
const rootPath = flag(args, "--root");
|
|
183
|
+
if (!packetPath || !rootPath || !flag(args, "--revocation-journal"))
|
|
184
|
+
throw new Error("verify-offline requires --packet, --root, and --revocation-journal from a trusted current source.");
|
|
185
|
+
const packet = JSON.parse(await readFile(resolve(packetPath), "utf8"));
|
|
186
|
+
const root = JSON.parse(await readFile(resolve(rootPath), "utf8"));
|
|
187
|
+
const journalPath = flag(args, "--revocation-journal");
|
|
188
|
+
const latestJournal = JSON.parse(await readFile(resolve(journalPath), "utf8"));
|
|
189
|
+
const historicalAt = flag(args, "--historical-at");
|
|
190
|
+
const report = verifyEvidencePacketV02(packet, root, historicalAt ? new Date(historicalAt) : new Date(), latestJournal);
|
|
191
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
192
|
+
const dir = flag(args, "--dir");
|
|
193
|
+
if (report.overall === "PASS" && dir) {
|
|
194
|
+
await markPilotLifecycle(resolve(dir), { firstOfflineVerificationAt: new Date().toISOString() });
|
|
195
|
+
await writePilotAcceptanceReport(resolve(dir));
|
|
196
|
+
}
|
|
197
|
+
if (report.overall !== "PASS")
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
}
|
|
200
|
+
async function inspectPrivacy(args) { const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner"); const manifest = JSON.parse(await readFile(join(dir, "witnora.privacy-manifest.json"), "utf8")); process.stdout.write(formatPrivacyInspection(manifest, await readConfig(dir))); }
|
|
201
|
+
async function supportBundle(args) { const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner"); const out = flag(args, "--out"); const result = await collectSupportBundle(dir, await readConfig(dir), out ? resolve(out) : undefined); process.stdout.write(`Support bundle: ${result.bundlePath}\nPrivacy scan: ${result.scanPath}\nRaw secret canary matches: 0\n`); }
|
|
202
|
+
async function pilotReport(args) { const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner"); process.stdout.write(`${JSON.stringify(await writePilotAcceptanceReport(dir), null, 2)}\n`); }
|
|
203
|
+
async function pilotLifecycle(args) {
|
|
204
|
+
const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner");
|
|
205
|
+
const report = await runPilotLifecycleDemonstration(dir);
|
|
206
|
+
const stages = report.stages;
|
|
207
|
+
process.stdout.write(`${stages.map((stage, index) => `${index + 1}/4 ${stage.status.padEnd(30)} ${stage.stage}`).join("\n")}\nReport: ${join(dir, "pilot/lifecycle-demonstration.json")}\n`);
|
|
208
|
+
}
|
|
209
|
+
async function inspectTrust(args) { const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner"); const trust = await readGatewayTrustMaterial(dir); process.stdout.write(`Pinned root: ${trust.root.rootFingerprint}\nGateway role: ${trust.gatewayCertificate.role}\nProbe role: ${trust.probeCertificate.role}\nRevocation checkpoint: ${trust.journal.checkpointDigest}\n`); }
|
|
210
|
+
async function issueAuthorization(args) {
|
|
211
|
+
const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner");
|
|
212
|
+
const config = await readConfig(dir);
|
|
213
|
+
const trust = await readAdministrativeTrustMaterial(dir);
|
|
214
|
+
const identity = createGitHubAcceptanceIdentity(config.declaredTarget, config.targetUrl);
|
|
215
|
+
const authorization = issueAcceptanceAuthorization(trust, { tenantRef: config.tenantRef, projectRef: config.projectRef, environment: config.environment, actionDigest: githubActionDigest(identity) });
|
|
216
|
+
await mkdir(join(dir, "authorization"), { recursive: true });
|
|
217
|
+
await Promise.all([writeFile(join(dir, "authorization/approval.json"), `${JSON.stringify(authorization.approval, null, 2)}\n`), writeFile(join(dir, "authorization/grant.json"), `${JSON.stringify(authorization.grant, null, 2)}\n`)]);
|
|
218
|
+
await markPilotLifecycle(dir, { firstGrantIssuedAt: new Date().toISOString() });
|
|
219
|
+
process.stdout.write(`Issued signed approval ${authorization.approval.approvalId} and single-use Grant ${authorization.grant.grantId}. Private issuer keys remain outside Gateway and target runtime mounts.\n`);
|
|
220
|
+
}
|
|
221
|
+
async function revokeProbe(args) {
|
|
222
|
+
const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner");
|
|
223
|
+
const trust = revokeProbeCertificate(await readAdministrativeTrustMaterial(dir), { effectiveAt: flag(args, "--effective-at") ?? new Date().toISOString(), retroactive: args.includes("--retroactive"), reason: flag(args, "--reason") ?? "Customer-operated Probe key revoked." });
|
|
224
|
+
await writeFile(join(dir, "trust/revocation-journal.json"), `${JSON.stringify(trust.journal, null, 2)}\n`);
|
|
225
|
+
process.stdout.write(`Revoked Probe certificate ${trust.probeCertificate.certificateId}. New observations at or after the effective time will fail closed.\n`);
|
|
226
|
+
}
|
|
227
|
+
async function rotateProbe(args) {
|
|
228
|
+
const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner");
|
|
229
|
+
const trust = rotateProbeCertificate(await readAdministrativeTrustMaterial(dir));
|
|
230
|
+
await Promise.all([
|
|
231
|
+
writeFile(join(dir, "trust/probe-certificate.json"), `${JSON.stringify(trust.probeCertificate, null, 2)}\n`),
|
|
232
|
+
writeFile(join(dir, "secrets/probe-private.pem"), trust.probePrivateKeyPem),
|
|
233
|
+
]);
|
|
234
|
+
const updateConfig = async (relative) => { const path = join(dir, relative); const value = JSON.parse(await readFile(path, "utf8")); value.probeSigningKey.keyCertificateId = trust.probeCertificate.certificateId; await writeFile(path, `${JSON.stringify(value, null, 2)}\n`); };
|
|
235
|
+
await Promise.all([updateConfig("probe/config.json"), updateConfig("probe/config.container.json")]);
|
|
236
|
+
await chmod(join(dir, "secrets/probe-private.pem"), 0o600).catch(() => undefined);
|
|
237
|
+
process.stdout.write(`Rotated Probe certificate to ${trust.probeCertificate.certificateId}. Restart the Probe before requesting a new observation.\n`);
|
|
238
|
+
}
|
|
239
|
+
async function targetEmulator(args) { const dir = resolve(flag(args, "--dir") ?? ".witnora/design-partner"); const trust = await readAuthorizationTrustMaterial(dir); const config = await readConfig(dir); await startGitHubTargetEmulator({ host: flag(args, "--host"), port: Number(flag(args, "--port") ?? "4311"), statePath: flag(args, "--state") ?? join(dir, "target-state.json"), readCredential: requiredEnvironment("WITNORA_GITHUB_READ_CREDENTIAL"), writeCredential: requiredEnvironment("WITNORA_GITHUB_WRITE_CREDENTIAL"), expectedExecutionBoundaryDigest: executionBoundaryDigest(config.targetUrl), authorizationTrust: trust }); }
|
|
240
|
+
async function readConfig(dir) { const value = JSON.parse(await readFile(join(dir, "design-partner.json"), "utf8")); if (value.protocolVersion !== "witnora.design_partner_github.v0.2")
|
|
241
|
+
throw new Error("Unsupported design-partner config version."); return value; }
|
|
242
|
+
async function readGatewayTrustMaterial(dir) {
|
|
243
|
+
const read = async (relative) => JSON.parse(await readFile(join(dir, relative), "utf8"));
|
|
244
|
+
const requestSignerKeyPath = process.env.WITNORA_REQUEST_SIGNER_PRIVATE_KEY_PATH ?? join(dir, "secrets/request-signer-private.pem");
|
|
245
|
+
const gatewayKeyPath = process.env.WITNORA_GATEWAY_PRIVATE_KEY_PATH ?? join(dir, "secrets/gateway-private.pem");
|
|
246
|
+
return { root: await read("trust/pinned-root.json"), requestSignerCertificate: await read("trust/request-signer-certificate.json"), gatewayCertificate: await read("trust/gateway-certificate.json"), probeCertificate: await read("trust/probe-certificate.json"), approvalCertificate: await read("trust/approval-certificate.json"), grantCertificate: await read("trust/grant-certificate.json"), journal: await read("trust/revocation-journal.json"), requestSignerPrivateKeyPem: await readFile(requestSignerKeyPath, "utf8"), gatewayPrivateKeyPem: await readFile(gatewayKeyPath, "utf8") };
|
|
247
|
+
}
|
|
248
|
+
async function readAuthorizationTrustMaterial(dir) {
|
|
249
|
+
const read = async (relative) => JSON.parse(await readFile(join(dir, relative), "utf8"));
|
|
250
|
+
return {
|
|
251
|
+
root: await read("trust/pinned-root.json"),
|
|
252
|
+
approvalCertificate: await read("trust/approval-certificate.json"),
|
|
253
|
+
grantCertificate: await read("trust/grant-certificate.json"),
|
|
254
|
+
journal: await read("trust/revocation-journal.json"),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function readAdministrativeTrustMaterial(dir) {
|
|
258
|
+
return { ...await readGatewayTrustMaterial(dir), rootPrivateKeyPem: await readFile(join(dir, "secrets/root-private.pem"), "utf8"), probePrivateKeyPem: await readFile(join(dir, "secrets/probe-private.pem"), "utf8"), approvalPrivateKeyPem: await readFile(join(dir, "secrets/approval-private.pem"), "utf8"), grantPrivateKeyPem: await readFile(join(dir, "secrets/grant-private.pem"), "utf8") };
|
|
259
|
+
}
|
|
260
|
+
async function readAuthorization(dir) { return { approval: JSON.parse(await readFile(join(dir, "authorization/approval.json"), "utf8")), grant: JSON.parse(await readFile(join(dir, "authorization/grant.json"), "utf8")) }; }
|
|
261
|
+
function requiredEnvironment(name) { const value = process.env[name]; if (!value)
|
|
262
|
+
throw new Error(`${name} must be supplied by the customer secret store.`); return value; }
|
|
263
|
+
function flag(args, name) { const index = args.indexOf(name); return index >= 0 ? args[index + 1] : undefined; }
|
|
264
|
+
function requiredFlag(args, name) { const value = flag(args, name); if (!value)
|
|
265
|
+
throw new Error(`${name} is required.`); return value; }
|
|
266
|
+
function help() { return `Usage:\n witnora design-partner init github-pr-merge --out ./witnora-pilot\n witnora design-partner authorization issue --dir ./witnora-pilot\n witnora design-partner doctor --dir ./witnora-pilot\n witnora design-partner privacy inspect --dir ./witnora-pilot\n witnora design-partner collect-support-bundle --dir ./witnora-pilot\n witnora design-partner pilot-lifecycle --dir ./witnora-pilot\n witnora design-partner pilot-report --dir ./witnora-pilot\n witnora design-partner trust inspect --dir ./witnora-pilot\n witnora design-partner trust revoke-probe [--effective-at <timestamp>]\n witnora design-partner trust rotate-probe\n witnora design-partner dry-run --dir ./witnora-pilot --target-url http://127.0.0.1:4311\n witnora design-partner run-acceptance --dir ./witnora-pilot\n witnora design-partner recover-acceptance --dir ./witnora-pilot\n witnora design-partner prepare-live-github --repository owner/sandbox-repo --pull-request 1 (read-only preflight)\n witnora design-partner verify-offline --packet <path> --root <path> --revocation-journal <latest.json> [--historical-at <timestamp>]\n`; }
|
|
267
|
+
function checklist() { return "# GitHub sandbox acceptance checklist\n\n- [ ] Repository name contains sandbox or test.\n- [ ] Base branch is not main/master.\n- [ ] Agent has no target write credential.\n- [ ] Gateway write and Probe read credentials differ.\n- [ ] Pinned root fingerprint was verified out of band.\n- [ ] Probe and Gateway secret mounts are disjoint.\n- [ ] Dry-run completed without a target write.\n- [ ] Evidence limitations were reviewed.\n"; }
|
|
268
|
+
function probeConfig(config, trust, targetUrl, stateDirectory, privateKeyPemPath) {
|
|
269
|
+
return {
|
|
270
|
+
host: "0.0.0.0", port: 4312, stateDirectory, requestBodyLimitBytes: 65_536,
|
|
271
|
+
observationTimeoutMs: 5_000, shutdownTimeoutMs: 10_000,
|
|
272
|
+
retry: { maxAttempts: 4, baseDelayMs: 250, scanIntervalMs: 500 },
|
|
273
|
+
bindings: { tenantRef: config.tenantRef, projectRef: config.projectRef, environment: config.environment, probeId: "customer-github-probe" },
|
|
274
|
+
requestVerificationKeys: [{ keyId: trust.requestSignerCertificate.subjectKeyId, publicKeyPemPath: "./request-signer-public.pem", role: "probe_observation_request_signer", validFrom: trust.requestSignerCertificate.notBefore, validUntil: trust.requestSignerCertificate.notAfter }],
|
|
275
|
+
probeSigningKey: { keyCertificateId: trust.probeCertificate.certificateId, privateKeyPemPath },
|
|
276
|
+
probeVersion: "0.2.0", storageEncryptionKeyEnv: "WITNORA_PROBE_STORAGE_KEY_BASE64",
|
|
277
|
+
githubTarget: { url: targetUrl, requiredCredentialRef: `env:${config.readCredentialEnv}` },
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function baseDesignPartnerCompose() {
|
|
281
|
+
return `services:
|
|
282
|
+
target-data-init:
|
|
283
|
+
image: node:22-alpine
|
|
284
|
+
user: "0:0"
|
|
285
|
+
read_only: true
|
|
286
|
+
network_mode: none
|
|
287
|
+
tmpfs: ["/tmp:rw,noexec,nosuid,size=4m"]
|
|
288
|
+
command: ["sh", "-eu", "-c", "if [ ! -f /state/target-state.json ]; then cp /seed/target-state.json /state/target-state.json; fi; chown -R 10001:10001 /state; chmod 0700 /state; chmod 0600 /state/target-state.json"]
|
|
289
|
+
volumes:
|
|
290
|
+
- ./target-state.json:/seed/target-state.json:ro
|
|
291
|
+
- target-data:/state
|
|
292
|
+
target-sandbox:
|
|
293
|
+
build:
|
|
294
|
+
context: \${WITNORA_SOURCE_ROOT:-../..}/packages/witnora-cli
|
|
295
|
+
dockerfile: Dockerfile.design-partner
|
|
296
|
+
entrypoint: ["/bin/sh", "-c"]
|
|
297
|
+
command: ["exec node /app/dist/cli.js design-partner target-emulator --state /state/target-state.json --port 4311"]
|
|
298
|
+
user: "10001:10001"
|
|
299
|
+
read_only: true
|
|
300
|
+
tmpfs: ["/tmp:rw,noexec,nosuid,size=16m"]
|
|
301
|
+
volumes:
|
|
302
|
+
- ./target-state.json:/seed/target-state.json:ro
|
|
303
|
+
- ./design-partner.json:/kit/design-partner.json:ro
|
|
304
|
+
- ./trust:/kit/trust:ro
|
|
305
|
+
- target-data:/state
|
|
306
|
+
environment:
|
|
307
|
+
WITNORA_GITHUB_READ_CREDENTIAL: \${WITNORA_GITHUB_READ_CREDENTIAL:?set a read-only sandbox credential}
|
|
308
|
+
WITNORA_GITHUB_WRITE_CREDENTIAL: \${WITNORA_GITHUB_WRITE_CREDENTIAL:?set a distinct write sandbox credential}
|
|
309
|
+
ports: ["127.0.0.1:4311:4311"]
|
|
310
|
+
networks: [read-path, write-path]
|
|
311
|
+
depends_on:
|
|
312
|
+
target-data-init:
|
|
313
|
+
condition: service_completed_successfully
|
|
314
|
+
probe-data-init:
|
|
315
|
+
image: node:22-alpine
|
|
316
|
+
user: "0:0"
|
|
317
|
+
read_only: true
|
|
318
|
+
network_mode: none
|
|
319
|
+
tmpfs: ["/tmp:rw,noexec,nosuid,size=4m"]
|
|
320
|
+
command: ["sh", "-eu", "-c", "chown -R 10001:10001 /data; chmod 0700 /data"]
|
|
321
|
+
volumes:
|
|
322
|
+
- probe-data:/data
|
|
323
|
+
probe-key-init:
|
|
324
|
+
image: node:22-alpine
|
|
325
|
+
user: "0:0"
|
|
326
|
+
read_only: true
|
|
327
|
+
network_mode: none
|
|
328
|
+
tmpfs: ["/tmp:rw,noexec,nosuid,size=4m"]
|
|
329
|
+
command: ["sh", "-eu", "-c", "umask 077; cp /run/secrets/probe-private.pem /keys/probe-private.pem; chown 10001:10001 /keys/probe-private.pem; chmod 0400 /keys/probe-private.pem"]
|
|
330
|
+
volumes:
|
|
331
|
+
- probe-key-data:/keys
|
|
332
|
+
secrets:
|
|
333
|
+
- source: probe_signing_key
|
|
334
|
+
target: probe-private.pem
|
|
335
|
+
probe:
|
|
336
|
+
build:
|
|
337
|
+
context: \${WITNORA_SOURCE_ROOT:-../..}/packages/witnora-probe
|
|
338
|
+
command: ["node", "dist/cli.js"]
|
|
339
|
+
user: "10001:10001"
|
|
340
|
+
read_only: true
|
|
341
|
+
tmpfs: ["/tmp:rw,noexec,nosuid,size=16m"]
|
|
342
|
+
volumes:
|
|
343
|
+
- ./probe/config.container.json:/kit/probe/config.container.json:ro
|
|
344
|
+
- ./probe/request-signer-public.pem:/kit/probe/request-signer-public.pem:ro
|
|
345
|
+
- probe-data:/app/data
|
|
346
|
+
- probe-key-data:/app/keys:ro
|
|
347
|
+
environment:
|
|
348
|
+
WITNORA_PROBE_CONFIG: /kit/probe/config.container.json
|
|
349
|
+
WITNORA_PROBE_STORAGE_KEY_BASE64: \${WITNORA_PROBE_STORAGE_KEY_BASE64:?set a base64 32-byte local storage key}
|
|
350
|
+
WITNORA_GITHUB_READ_CREDENTIAL: \${WITNORA_GITHUB_READ_CREDENTIAL:?set a read-only sandbox credential}
|
|
351
|
+
ports: ["127.0.0.1:4312:4312"]
|
|
352
|
+
networks: [read-path]
|
|
353
|
+
depends_on:
|
|
354
|
+
target-sandbox:
|
|
355
|
+
condition: service_started
|
|
356
|
+
probe-data-init:
|
|
357
|
+
condition: service_completed_successfully
|
|
358
|
+
probe-key-init:
|
|
359
|
+
condition: service_completed_successfully
|
|
360
|
+
gateway-key-init:
|
|
361
|
+
profiles: ["acceptance"]
|
|
362
|
+
image: node:22-alpine
|
|
363
|
+
user: "0:0"
|
|
364
|
+
read_only: true
|
|
365
|
+
network_mode: none
|
|
366
|
+
tmpfs: ["/tmp:rw,noexec,nosuid,size=4m"]
|
|
367
|
+
command: ["sh", "-eu", "-c", "umask 077; cp /run/secrets/request-signer-private.pem /keys/request-signer-private.pem; cp /run/secrets/gateway-private.pem /keys/gateway-private.pem; chown 10001:10001 /keys/*.pem; chmod 0400 /keys/*.pem"]
|
|
368
|
+
volumes:
|
|
369
|
+
- gateway-key-data:/keys
|
|
370
|
+
secrets:
|
|
371
|
+
- source: request_signing_key
|
|
372
|
+
target: request-signer-private.pem
|
|
373
|
+
- source: gateway_signing_key
|
|
374
|
+
target: gateway-private.pem
|
|
375
|
+
acceptance-output-init:
|
|
376
|
+
profiles: ["acceptance"]
|
|
377
|
+
image: node:22-alpine
|
|
378
|
+
user: "0:0"
|
|
379
|
+
read_only: true
|
|
380
|
+
network_mode: none
|
|
381
|
+
tmpfs: ["/tmp:rw,noexec,nosuid,size=4m"]
|
|
382
|
+
command: ["sh", "-eu", "-c", "chown -R 10001:10001 /output /claims /pilot; chmod 0700 /output /claims /pilot"]
|
|
383
|
+
volumes:
|
|
384
|
+
- ./acceptance:/output
|
|
385
|
+
- ./claims:/claims
|
|
386
|
+
- ./pilot:/pilot
|
|
387
|
+
gateway-acceptance:
|
|
388
|
+
profiles: ["acceptance"]
|
|
389
|
+
build:
|
|
390
|
+
context: \${WITNORA_SOURCE_ROOT:-../..}/packages/witnora-cli
|
|
391
|
+
dockerfile: Dockerfile.design-partner
|
|
392
|
+
command: ["design-partner", "run-acceptance", "--dir", "/kit", "--out", "/output", "--target-url", "http://target-sandbox:4311", "--probe-url", "http://probe:4312"]
|
|
393
|
+
user: "10001:10001"
|
|
394
|
+
read_only: true
|
|
395
|
+
tmpfs: ["/tmp:rw,noexec,nosuid,size=16m"]
|
|
396
|
+
volumes:
|
|
397
|
+
- ./design-partner.json:/kit/design-partner.json:ro
|
|
398
|
+
- ./trust:/kit/trust:ro
|
|
399
|
+
- ./authorization:/kit/authorization:ro
|
|
400
|
+
- ./pilot:/kit/pilot
|
|
401
|
+
- gateway-key-data:/app/keys:ro
|
|
402
|
+
- acceptance-data:/output
|
|
403
|
+
- ./claims:/claims
|
|
404
|
+
environment:
|
|
405
|
+
WITNORA_REQUEST_SIGNER_PRIVATE_KEY_PATH: /app/keys/request-signer-private.pem
|
|
406
|
+
WITNORA_GATEWAY_PRIVATE_KEY_PATH: /app/keys/gateway-private.pem
|
|
407
|
+
WITNORA_GRANT_CLAIM_DIR: /claims
|
|
408
|
+
WITNORA_GITHUB_WRITE_CREDENTIAL: \${WITNORA_GITHUB_WRITE_CREDENTIAL:?set a sandbox write credential}
|
|
409
|
+
networks: [read-path, write-path]
|
|
410
|
+
depends_on:
|
|
411
|
+
target-sandbox:
|
|
412
|
+
condition: service_started
|
|
413
|
+
probe:
|
|
414
|
+
condition: service_started
|
|
415
|
+
gateway-key-init:
|
|
416
|
+
condition: service_completed_successfully
|
|
417
|
+
acceptance-output-init:
|
|
418
|
+
condition: service_completed_successfully
|
|
419
|
+
offline-verifier:
|
|
420
|
+
profiles: ["acceptance"]
|
|
421
|
+
build:
|
|
422
|
+
context: \${WITNORA_SOURCE_ROOT:-../..}/packages/witnora-cli
|
|
423
|
+
dockerfile: Dockerfile.design-partner
|
|
424
|
+
command: ["design-partner", "verify-offline", "--packet", "/output/witnora-evidence-packet-v0.2.json", "--root", "/trust/pinned-root.json", "--revocation-journal", "/trust/revocation-journal.json"]
|
|
425
|
+
user: "10001:10001"
|
|
426
|
+
read_only: true
|
|
427
|
+
tmpfs: ["/tmp:rw,noexec,nosuid,size=16m"]
|
|
428
|
+
volumes:
|
|
429
|
+
- ./trust:/trust:ro
|
|
430
|
+
- acceptance-data:/output:ro
|
|
431
|
+
network_mode: none
|
|
432
|
+
volumes:
|
|
433
|
+
target-data: {}
|
|
434
|
+
probe-data: {}
|
|
435
|
+
probe-key-data: {}
|
|
436
|
+
gateway-key-data: {}
|
|
437
|
+
acceptance-data: {}
|
|
438
|
+
secrets:
|
|
439
|
+
probe_signing_key:
|
|
440
|
+
file: ./secrets/probe-private.pem
|
|
441
|
+
request_signing_key:
|
|
442
|
+
file: ./secrets/request-signer-private.pem
|
|
443
|
+
gateway_signing_key:
|
|
444
|
+
file: ./secrets/gateway-private.pem
|
|
445
|
+
networks:
|
|
446
|
+
write-path: {}
|
|
447
|
+
read-path: {}
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createPublicKey, verify } from "node:crypto";
|
|
2
|
+
import { canonicalBytesV02, sha256CanonicalV02 } from "./canonical-v02.js";
|
|
3
|
+
import { verifyAuthorizationV02 } from "./authorization-v02.js";
|
|
4
|
+
import { verifyProbeAttestation, verifyProbeRequest } from "./probe-protocol-v02.js";
|
|
5
|
+
import { assertNotRevoked, assertRevocationJournalExtends, verifyKeyCertificate, verifyRevocationJournal } from "./trust-v02.js";
|
|
6
|
+
export function gatewaySignedValue(packet) {
|
|
7
|
+
const { gatewaySignature: _signature, ...value } = packet;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function verifyEvidencePacketV02(packet, pinnedRoot, verificationTime = new Date(), latestRevocationJournal) {
|
|
11
|
+
const failures = [];
|
|
12
|
+
if (!packet || typeof packet !== "object" || packet.protocolVersion !== "witnora.verifiable_evidence_packet.v0.2"
|
|
13
|
+
|| !packet.approval || !packet.grant || !packet.executionAttempt || !packet.reconciliation || !packet.probeRequest
|
|
14
|
+
|| !packet.probeAttestation || !packet.trust || !packet.separation || !packet.privacy || !packet.evidenceStrength || !packet.gatewaySignature) {
|
|
15
|
+
return failedReport("Evidence packet v0.2 is structurally incomplete.");
|
|
16
|
+
}
|
|
17
|
+
let crypto = true;
|
|
18
|
+
try {
|
|
19
|
+
if (packet.protocolVersion !== "witnora.verifiable_evidence_packet.v0.2")
|
|
20
|
+
throw new Error("Unsupported evidence protocol version.");
|
|
21
|
+
if (packet.trust.rootId !== pinnedRoot.rootId)
|
|
22
|
+
throw new Error("Packet does not reference the pinned root.");
|
|
23
|
+
if (!latestRevocationJournal)
|
|
24
|
+
throw new Error("Latest trusted revocation journal is required; packet-embedded trust state cannot establish current verification.");
|
|
25
|
+
const journal = latestRevocationJournal;
|
|
26
|
+
verifyRevocationJournal(journal, pinnedRoot);
|
|
27
|
+
verifyRevocationJournal(packet.trust.revocationJournal, pinnedRoot);
|
|
28
|
+
assertRevocationJournalExtends(packet.trust.revocationJournal, journal);
|
|
29
|
+
const requestedAt = new Date(packet.probeRequest.requestedAt);
|
|
30
|
+
verifyKeyCertificate(packet.trust.requestSignerCertificate, pinnedRoot, requestedAt, { role: "server_attestor", tenantRef: packet.tenantRef, projectRef: packet.projectRef, environment: packet.environment });
|
|
31
|
+
assertNotRevoked(packet.trust.requestSignerCertificate, journal, requestedAt);
|
|
32
|
+
if (packet.probeRequest.requestSignerKeyId !== packet.trust.requestSignerCertificate.subjectKeyId)
|
|
33
|
+
throw new Error("Probe request signer certificate ID mismatch.");
|
|
34
|
+
const observedAt = new Date(packet.probeAttestation.observedAt);
|
|
35
|
+
const eventVerificationTime = verificationTime;
|
|
36
|
+
verifyProbeRequest(packet.probeRequest, packet.trust.requestSignerCertificate.publicKeyPem, {
|
|
37
|
+
tenantRef: packet.tenantRef, projectRef: packet.projectRef, environment: packet.environment,
|
|
38
|
+
actionDigest: packet.actionDigest, contractDigest: packet.contractDigest, resourceCommitment: packet.resourceCommitment,
|
|
39
|
+
predicateDigest: packet.predicateDigest, probeId: packet.probeAttestation.probeId, now: observedAt,
|
|
40
|
+
});
|
|
41
|
+
const issuedAt = new Date(packet.issuedAt);
|
|
42
|
+
verifyKeyCertificate(packet.trust.gatewayCertificate, pinnedRoot, issuedAt, { role: "gateway_runtime", tenantRef: packet.tenantRef, projectRef: packet.projectRef, environment: packet.environment });
|
|
43
|
+
assertNotRevoked(packet.trust.gatewayCertificate, journal, issuedAt);
|
|
44
|
+
if (packet.gatewaySignature.keyId !== packet.trust.gatewayCertificate.subjectKeyId || !verify(null, canonicalBytesV02(gatewaySignedValue(packet)), createPublicKey(packet.trust.gatewayCertificate.publicKeyPem), Buffer.from(packet.gatewaySignature.signature, "base64")))
|
|
45
|
+
throw new Error("Gateway packet signature is invalid.");
|
|
46
|
+
verifyAuthorizationV02({ approval: packet.approval, grant: packet.grant, trust: { root: pinnedRoot, approvalCertificate: packet.trust.approvalCertificate, grantCertificate: packet.trust.grantCertificate, revocationJournal: journal }, expected: { tenantRef: packet.tenantRef, projectRef: packet.projectRef, environment: packet.environment, actionDigest: packet.actionDigest }, at: issuedAt });
|
|
47
|
+
verifyProbeAttestation(packet.probeAttestation, packet.probeRequest, { root: pinnedRoot, certificate: packet.trust.probeCertificate, revocationJournal: journal }, {
|
|
48
|
+
tenantRef: packet.tenantRef, projectRef: packet.projectRef, environment: packet.environment,
|
|
49
|
+
actionDigest: packet.actionDigest, contractDigest: packet.contractDigest, resourceCommitment: packet.resourceCommitment,
|
|
50
|
+
predicateDigest: packet.predicateDigest, probeId: packet.probeAttestation.probeId, now: eventVerificationTime, maxFreshnessMs: 300_000,
|
|
51
|
+
});
|
|
52
|
+
if (packet.probeAttestation.probeId !== packet.trust.probeCertificate.operatorIdentity)
|
|
53
|
+
throw new Error("Probe identity is not bound to the certified operator identity.");
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
crypto = false;
|
|
57
|
+
failures.push(error instanceof Error ? error.message : String(error));
|
|
58
|
+
}
|
|
59
|
+
const terminalState = packet.executionAttempt.state === "RECONCILED_SUCCESS";
|
|
60
|
+
const recoveryChainValid = sha256CanonicalV02(packet.executionAttempt.recoveryHistory) === packet.eventChainDigest && packet.executionAttempt.recoveryHistory.at(-1)?.state === packet.executionAttempt.state;
|
|
61
|
+
const dispatchReconciled = packet.executionAttempt.dispatchCount === 1 && terminalState && recoveryChainValid && packet.reconciliation.status === "TARGET_STATE_RECONCILED";
|
|
62
|
+
const grantUseBound = packet.reconciliation.authorizationBindingSource === "TARGET_GRANT_ECHO"
|
|
63
|
+
? packet.probeAttestation.normalizedObservation.consumedGrantId === packet.grant.grantId
|
|
64
|
+
: packet.reconciliation.authorizationBindingSource === "GATEWAY_DISPATCH_RECORD" && dispatchReconciled;
|
|
65
|
+
const authorizationBound = crypto && packet.approval.actionDigest === packet.actionDigest && packet.grant.actionDigest === packet.actionDigest && packet.grant.maxUses === 1 && grantUseBound;
|
|
66
|
+
const outcomeObserved = packet.probeAttestation.outcomeStatus === "VERIFIED" && dispatchReconciled;
|
|
67
|
+
const customerOperated = ["separate_process", "separate_container", "customer_network"].includes(packet.separation.probeDeploymentMode)
|
|
68
|
+
&& pinnedRoot.ownerType === "CUSTOMER_ASSURANCE_ROOT"
|
|
69
|
+
&& packet.evidenceStrength.outcomeObservation === "CUSTOMER_OPERATED_READ_PATH";
|
|
70
|
+
const expectedSeparation = packet.separation.probeDeploymentMode === "customer_network"
|
|
71
|
+
? "CUSTOMER_NETWORK_SEPARATED"
|
|
72
|
+
: packet.separation.probeDeploymentMode === "separate_container" ? "CONTAINER_SEPARATED" : "PROCESS_SEPARATED";
|
|
73
|
+
const expectedAuthorizationStrength = authorizationBound
|
|
74
|
+
? packet.reconciliation.authorizationBindingSource === "TARGET_GRANT_ECHO" ? "EXACT_ACTION_GRANT_BOUND" : "GATEWAY_DISPATCH_BOUND"
|
|
75
|
+
: "NOT_ESTABLISHED";
|
|
76
|
+
const strengthConsistent = packet.evidenceStrength.authorizationBinding === expectedAuthorizationStrength
|
|
77
|
+
&& packet.evidenceStrength.executionBinding === (dispatchReconciled ? "TARGET_STATE_RECONCILED" : packet.executionAttempt.state === "DISPATCH_UNKNOWN" ? "DISPATCH_UNKNOWN" : "NOT_ESTABLISHED")
|
|
78
|
+
&& packet.evidenceStrength.sourceIntegrity === (crypto ? "CUSTOMER_ROOT_CERTIFIED" : "SELF_ASSERTED")
|
|
79
|
+
&& packet.evidenceStrength.outcomeObservation === (outcomeObserved ? "CUSTOMER_OPERATED_READ_PATH" : "NOT_OBSERVED")
|
|
80
|
+
&& packet.evidenceStrength.operatorSeparation === expectedSeparation
|
|
81
|
+
&& packet.evidenceStrength.review !== "EXTERNAL"
|
|
82
|
+
&& packet.privacy.rawCanaryMatchesOutsideLocal === 0;
|
|
83
|
+
if (!authorizationBound)
|
|
84
|
+
failures.push("Authorization is not bound to the exact action.");
|
|
85
|
+
if (!dispatchReconciled)
|
|
86
|
+
failures.push("Target dispatch is not reconciled.");
|
|
87
|
+
if (!outcomeObserved)
|
|
88
|
+
failures.push("Outcome was not independently observed.");
|
|
89
|
+
if (!recoveryChainValid)
|
|
90
|
+
failures.push("Execution recovery history is incomplete or does not match its digest.");
|
|
91
|
+
if (!strengthConsistent)
|
|
92
|
+
failures.push("Evidence-strength vector overclaims the packet evidence.");
|
|
93
|
+
if (sha256CanonicalV02(packet.probeRequest) !== packet.probeAttestation.requestDigest)
|
|
94
|
+
failures.push("Probe request digest mismatch.");
|
|
95
|
+
return {
|
|
96
|
+
protocolVersion: "witnora.offline_verification_report.v0.2",
|
|
97
|
+
overall: crypto && authorizationBound && dispatchReconciled && outcomeObserved && customerOperated && strengthConsistent && recoveryChainValid ? "PASS" : "FAIL",
|
|
98
|
+
networkUsed: false,
|
|
99
|
+
dimensions: {
|
|
100
|
+
cryptographicallyValid: crypto,
|
|
101
|
+
trustedByPinnedRoot: crypto,
|
|
102
|
+
authorizationBound,
|
|
103
|
+
dispatchReconciled,
|
|
104
|
+
outcomeObserved,
|
|
105
|
+
customerOperated,
|
|
106
|
+
// v0.2 has no independent reviewer signature object, so this claim cannot be established from the packet.
|
|
107
|
+
thirdPartyReviewed: false,
|
|
108
|
+
},
|
|
109
|
+
failures,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function failedReport(message) {
|
|
113
|
+
return {
|
|
114
|
+
protocolVersion: "witnora.offline_verification_report.v0.2", overall: "FAIL", networkUsed: false,
|
|
115
|
+
dimensions: { cryptographicallyValid: false, trustedByPinnedRoot: false, authorizationBound: false, dispatchReconciled: false, outcomeObserved: false, customerOperated: false, thirdPartyReviewed: false },
|
|
116
|
+
failures: [message],
|
|
117
|
+
};
|
|
118
|
+
}
|