@vela-science/canopus 0.6.4 → 0.7.0
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/CHANGELOG.md +35 -0
- package/README.md +26 -16
- package/SECURITY.md +3 -2
- package/capsules/erdos1056-k15/bin/linux-arm64/{10428601-10428800 → 10428801-10429000}/verifier +0 -0
- package/capsules/erdos1056-k15/bin/linux-x86_64/{10428601-10428800 → 10428801-10429000}/verifier +0 -0
- package/dist/src/activity/events.d.ts +4 -1
- package/dist/src/activity/events.js +8 -2
- package/dist/src/activity/store.d.ts +2 -2
- package/dist/src/activity/store.js +4 -3
- package/dist/src/cli.js +2 -37
- package/dist/src/product/run.js +0 -7
- package/dist/src/run.d.ts +0 -7
- package/dist/src/run.js +0 -15
- package/dist/src/vela/cli.d.ts +0 -9
- package/dist/src/vela/cli.js +0 -48
- package/missions/erdos1056-k15-next/mission.draft.json +6 -6
- package/package.json +3 -10
- package/profiles/{erdos1056-k15-10428601-10428800.json → erdos1056-k15-10428801-10429000.json} +9 -9
- package/BUILD_WEEK.md +0 -198
- package/advisories/erdos1056-claim-fidelity/output.schema.json +0 -90
- package/advisories/erdos1056-claim-fidelity/registration.json +0 -42
- package/advisories/erdos1056-claim-fidelity/results/assessment.json +0 -1
- package/advisories/erdos1056-claim-fidelity/results/verification.json +0 -1
- package/dist/src/capability/withdrawal.d.ts +0 -47
- package/dist/src/capability/withdrawal.js +0 -487
- package/dist/src/product/withdraw.d.ts +0 -8
- package/dist/src/product/withdraw.js +0 -255
- package/docs/RELEASES.md +0 -1326
- package/evidence/build-week/run_eb6bcd46-cffd-4ae8-b630-2681bd84da71.public.json +0 -1
- package/evidence/build-week/run_f68e4cfc-e5c7-4c73-86cb-d79807c47ec4.public.json +0 -1
- package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/pending-commands.json +0 -1
- package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/public-run.json +0 -1
- package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/root-manifest.json +0 -1
- package/evidence/erdos/run_192b3bef-9d6e-49e5-b72d-7ae903b29d5e/web-import.json +0 -1
- package/scripts/run-claim-fidelity-advisory.mjs +0 -235
|
@@ -1,487 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { createPrivateKey, createPublicKey, verify as verifyEd25519, } from "node:crypto";
|
|
4
|
-
import { chmod, lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
-
import { AGENT_RE, exactKeys, gitObjectAt, objectAt, relativePathAt, sha256At, stringAt, } from "../contracts/validation.js";
|
|
6
|
-
import { canonicalJcs, canonicalJson, contentDigest, protocolDigest, sha256Bytes, } from "../util/canonical.js";
|
|
7
|
-
import { readBoundedRegularFile } from "../util/files.js";
|
|
8
|
-
export const WITHDRAWAL_CAPABILITY_SCHEMA = "canopus.withdrawal-capability.v1";
|
|
9
|
-
function capabilityBase(storeRoot) {
|
|
10
|
-
return path.resolve(storeRoot ?? path.join(os.homedir(), ".canopus", "capabilities"));
|
|
11
|
-
}
|
|
12
|
-
export function capabilityDirectory(proposalId, storeRoot) {
|
|
13
|
-
if (!/^vpr_[0-9a-f]{16}$/u.test(proposalId)) {
|
|
14
|
-
throw new Error("withdrawal capability requires a full vpr_<16 lowercase hex> proposal id");
|
|
15
|
-
}
|
|
16
|
-
return path.join(capabilityBase(storeRoot), proposalId);
|
|
17
|
-
}
|
|
18
|
-
function agentKeyPath(velaHome, actor) {
|
|
19
|
-
const safe = [...actor]
|
|
20
|
-
.map((character) => /[A-Za-z0-9_-]/u.test(character) ? character : "-")
|
|
21
|
-
.join("");
|
|
22
|
-
return path.join(velaHome, ".vela", "agents", safe, "private.key");
|
|
23
|
-
}
|
|
24
|
-
function object(value, label) {
|
|
25
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
26
|
-
throw new Error(`${label} must be an object`);
|
|
27
|
-
}
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
function text(value, label) {
|
|
31
|
-
if (typeof value !== "string" || value.length === 0)
|
|
32
|
-
throw new Error(`${label} must be a string`);
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
const RFC3339_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u;
|
|
36
|
-
const HEX_SEED_BYTES = 64;
|
|
37
|
-
const PRIVATE_KEY_MAX_BYTES = HEX_SEED_BYTES + 1;
|
|
38
|
-
function timestampAt(value, at) {
|
|
39
|
-
const parsed = stringAt(value, at, { min: 20, max: 64, pattern: RFC3339_RE });
|
|
40
|
-
if (!Number.isFinite(Date.parse(parsed)))
|
|
41
|
-
throw new Error(`${at} is not a valid RFC3339 timestamp`);
|
|
42
|
-
return parsed;
|
|
43
|
-
}
|
|
44
|
-
function frontierAt(value, at) {
|
|
45
|
-
if (value === ".")
|
|
46
|
-
return ".";
|
|
47
|
-
return relativePathAt(value, at);
|
|
48
|
-
}
|
|
49
|
-
function parseRoots(value, at) {
|
|
50
|
-
const roots = objectAt(value, at);
|
|
51
|
-
exactKeys(roots, ["git_commit", "git_tree", "vela_event_log", "vela_snapshot"], [], at);
|
|
52
|
-
return {
|
|
53
|
-
git_commit: gitObjectAt(roots.git_commit, `${at}.git_commit`),
|
|
54
|
-
git_tree: gitObjectAt(roots.git_tree, `${at}.git_tree`),
|
|
55
|
-
vela_event_log: sha256At(roots.vela_event_log, `${at}.vela_event_log`),
|
|
56
|
-
vela_snapshot: sha256At(roots.vela_snapshot, `${at}.vela_snapshot`),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function parseStrictBaseline(value, at) {
|
|
60
|
-
const baseline = objectAt(value, at);
|
|
61
|
-
exactKeys(baseline, ["status", "blocker_count", "blockers_root", "rule_counts"], [], at);
|
|
62
|
-
if (baseline.status !== "pass" && baseline.status !== "fail") {
|
|
63
|
-
throw new Error(`${at}.status must be pass or fail`);
|
|
64
|
-
}
|
|
65
|
-
if (typeof baseline.blocker_count !== "number" ||
|
|
66
|
-
!Number.isSafeInteger(baseline.blocker_count) ||
|
|
67
|
-
baseline.blocker_count < 0) {
|
|
68
|
-
throw new Error(`${at}.blocker_count must be a nonnegative integer`);
|
|
69
|
-
}
|
|
70
|
-
if (!Array.isArray(baseline.rule_counts) || baseline.rule_counts.length > 512) {
|
|
71
|
-
throw new Error(`${at}.rule_counts must be a bounded array`);
|
|
72
|
-
}
|
|
73
|
-
const seen = new Set();
|
|
74
|
-
let total = 0;
|
|
75
|
-
const ruleCounts = baseline.rule_counts.map((value, index) => {
|
|
76
|
-
const ruleCount = objectAt(value, `${at}.rule_counts[${index}]`);
|
|
77
|
-
exactKeys(ruleCount, ["rule", "count"], [], `${at}.rule_counts[${index}]`);
|
|
78
|
-
const rule = stringAt(ruleCount.rule, `${at}.rule_counts[${index}].rule`, {
|
|
79
|
-
min: 1,
|
|
80
|
-
max: 128,
|
|
81
|
-
pattern: /^[a-z][a-z0-9_]*$/u,
|
|
82
|
-
});
|
|
83
|
-
if (seen.has(rule))
|
|
84
|
-
throw new Error(`${at}.rule_counts contains duplicate rules`);
|
|
85
|
-
seen.add(rule);
|
|
86
|
-
if (typeof ruleCount.count !== "number" ||
|
|
87
|
-
!Number.isSafeInteger(ruleCount.count) ||
|
|
88
|
-
ruleCount.count < 1) {
|
|
89
|
-
throw new Error(`${at}.rule_counts[${index}].count must be a positive integer`);
|
|
90
|
-
}
|
|
91
|
-
total += ruleCount.count;
|
|
92
|
-
return { rule, count: ruleCount.count };
|
|
93
|
-
});
|
|
94
|
-
if (total !== baseline.blocker_count ||
|
|
95
|
-
(baseline.status === "pass") !== (baseline.blocker_count === 0)) {
|
|
96
|
-
throw new Error(`${at} status, count, and per-rule totals disagree`);
|
|
97
|
-
}
|
|
98
|
-
const sorted = [...ruleCounts].sort((left, right) => left.rule.localeCompare(right.rule));
|
|
99
|
-
if (canonicalJcs(sorted) !== canonicalJcs(ruleCounts)) {
|
|
100
|
-
throw new Error(`${at}.rule_counts must be sorted by rule`);
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
status: baseline.status,
|
|
104
|
-
blocker_count: baseline.blocker_count,
|
|
105
|
-
blockers_root: sha256At(baseline.blockers_root, `${at}.blockers_root`),
|
|
106
|
-
rule_counts: ruleCounts,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function parseManifest(value, proposalId) {
|
|
110
|
-
const manifest = objectAt(value, "withdrawal capability manifest");
|
|
111
|
-
exactKeys(manifest, [
|
|
112
|
-
"schema",
|
|
113
|
-
"state",
|
|
114
|
-
"proposal_id",
|
|
115
|
-
"proposal_root",
|
|
116
|
-
"receipt_root",
|
|
117
|
-
"identity_binding_id",
|
|
118
|
-
"actor",
|
|
119
|
-
"public_key",
|
|
120
|
-
"frontier",
|
|
121
|
-
"final_roots",
|
|
122
|
-
"strict_baseline",
|
|
123
|
-
"vela",
|
|
124
|
-
"created_at",
|
|
125
|
-
], ["consumed_at", "consumed_reason"], "withdrawal capability manifest");
|
|
126
|
-
if (manifest.schema !== WITHDRAWAL_CAPABILITY_SCHEMA) {
|
|
127
|
-
throw new Error(`withdrawal capability schema must be ${WITHDRAWAL_CAPABILITY_SCHEMA}`);
|
|
128
|
-
}
|
|
129
|
-
if (manifest.proposal_id !== proposalId) {
|
|
130
|
-
throw new Error("withdrawal capability manifest does not match its directory");
|
|
131
|
-
}
|
|
132
|
-
const state = manifest.state;
|
|
133
|
-
if (state !== "available" && state !== "consumed") {
|
|
134
|
-
throw new Error("withdrawal capability state must be available or consumed");
|
|
135
|
-
}
|
|
136
|
-
const vela = objectAt(manifest.vela, "withdrawal capability manifest.vela");
|
|
137
|
-
exactKeys(vela, ["binary", "version", "sha256"], [], "withdrawal capability manifest.vela");
|
|
138
|
-
const binary = stringAt(vela.binary, "withdrawal capability manifest.vela.binary", {
|
|
139
|
-
min: 1,
|
|
140
|
-
max: 4096,
|
|
141
|
-
});
|
|
142
|
-
if (!path.isAbsolute(binary) || binary.includes("\0")) {
|
|
143
|
-
throw new Error("withdrawal capability Vela binary must be an absolute path");
|
|
144
|
-
}
|
|
145
|
-
const consumedAt = manifest.consumed_at === undefined
|
|
146
|
-
? undefined
|
|
147
|
-
: timestampAt(manifest.consumed_at, "withdrawal capability manifest.consumed_at");
|
|
148
|
-
const consumedReason = manifest.consumed_reason;
|
|
149
|
-
if (consumedReason !== undefined &&
|
|
150
|
-
consumedReason !== "withdrawn" &&
|
|
151
|
-
consumedReason !== "human_decision_observed") {
|
|
152
|
-
throw new Error("withdrawal capability consumed_reason is invalid");
|
|
153
|
-
}
|
|
154
|
-
if ((state === "available" && (consumedAt !== undefined || consumedReason !== undefined)) ||
|
|
155
|
-
(state === "consumed" && (consumedAt === undefined || consumedReason === undefined))) {
|
|
156
|
-
throw new Error("withdrawal capability consumption fields disagree with state");
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
schema: WITHDRAWAL_CAPABILITY_SCHEMA,
|
|
160
|
-
state,
|
|
161
|
-
proposal_id: stringAt(manifest.proposal_id, "withdrawal capability manifest.proposal_id", {
|
|
162
|
-
max: 20,
|
|
163
|
-
pattern: /^vpr_[0-9a-f]{16}$/u,
|
|
164
|
-
}),
|
|
165
|
-
proposal_root: sha256At(manifest.proposal_root, "withdrawal capability manifest.proposal_root"),
|
|
166
|
-
receipt_root: sha256At(manifest.receipt_root, "withdrawal capability manifest.receipt_root"),
|
|
167
|
-
identity_binding_id: stringAt(manifest.identity_binding_id, "withdrawal capability manifest.identity_binding_id", { max: 20, pattern: /^vib_[0-9a-f]{16}$/u }),
|
|
168
|
-
actor: stringAt(manifest.actor, "withdrawal capability manifest.actor", {
|
|
169
|
-
max: 69,
|
|
170
|
-
pattern: AGENT_RE,
|
|
171
|
-
}),
|
|
172
|
-
public_key: stringAt(manifest.public_key, "withdrawal capability manifest.public_key", {
|
|
173
|
-
max: 64,
|
|
174
|
-
pattern: /^[0-9a-f]{64}$/u,
|
|
175
|
-
}),
|
|
176
|
-
frontier: frontierAt(manifest.frontier, "withdrawal capability manifest.frontier"),
|
|
177
|
-
final_roots: parseRoots(manifest.final_roots, "withdrawal capability manifest.final_roots"),
|
|
178
|
-
strict_baseline: parseStrictBaseline(manifest.strict_baseline, "withdrawal capability manifest.strict_baseline"),
|
|
179
|
-
vela: {
|
|
180
|
-
binary,
|
|
181
|
-
version: stringAt(vela.version, "withdrawal capability manifest.vela.version", {
|
|
182
|
-
min: 1,
|
|
183
|
-
max: 64,
|
|
184
|
-
pattern: /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/u,
|
|
185
|
-
}),
|
|
186
|
-
sha256: sha256At(vela.sha256, "withdrawal capability manifest.vela.sha256"),
|
|
187
|
-
},
|
|
188
|
-
created_at: timestampAt(manifest.created_at, "withdrawal capability manifest.created_at"),
|
|
189
|
-
...(consumedAt === undefined ? {} : { consumed_at: consumedAt }),
|
|
190
|
-
...(consumedReason === undefined ? {} : { consumed_reason: consumedReason }),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
function hexNibble(byte) {
|
|
194
|
-
if (byte >= 48 && byte <= 57)
|
|
195
|
-
return byte - 48;
|
|
196
|
-
if (byte >= 97 && byte <= 102)
|
|
197
|
-
return byte - 87;
|
|
198
|
-
return -1;
|
|
199
|
-
}
|
|
200
|
-
function decodeSeed(keyBytes) {
|
|
201
|
-
const length = keyBytes.length;
|
|
202
|
-
if ((length !== HEX_SEED_BYTES && length !== PRIVATE_KEY_MAX_BYTES) ||
|
|
203
|
-
(length === PRIVATE_KEY_MAX_BYTES && keyBytes[HEX_SEED_BYTES] !== 10)) {
|
|
204
|
-
throw new Error("producer session key must contain one 32-byte lowercase hex seed");
|
|
205
|
-
}
|
|
206
|
-
const seed = Buffer.alloc(32);
|
|
207
|
-
for (let index = 0; index < HEX_SEED_BYTES; index += 2) {
|
|
208
|
-
const high = hexNibble(keyBytes[index] ?? -1);
|
|
209
|
-
const low = hexNibble(keyBytes[index + 1] ?? -1);
|
|
210
|
-
if (high < 0 || low < 0) {
|
|
211
|
-
seed.fill(0);
|
|
212
|
-
throw new Error("producer session key must contain one 32-byte lowercase hex seed");
|
|
213
|
-
}
|
|
214
|
-
seed[index / 2] = (high << 4) | low;
|
|
215
|
-
}
|
|
216
|
-
return seed;
|
|
217
|
-
}
|
|
218
|
-
function derivePublicKey(seed) {
|
|
219
|
-
const privateDer = Buffer.concat([
|
|
220
|
-
Buffer.from("302e020100300506032b657004220420", "hex"),
|
|
221
|
-
seed,
|
|
222
|
-
]);
|
|
223
|
-
try {
|
|
224
|
-
const derivedPublicDer = createPublicKey(createPrivateKey({ key: privateDer, format: "der", type: "pkcs8" })).export({ format: "der", type: "spki" });
|
|
225
|
-
return Buffer.from(derivedPublicDer).subarray(-32).toString("hex");
|
|
226
|
-
}
|
|
227
|
-
finally {
|
|
228
|
-
privateDer.fill(0);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
function verifyIdentityBinding(value) {
|
|
232
|
-
const binding = objectAt(value, "Receipt identity binding");
|
|
233
|
-
exactKeys(binding, [
|
|
234
|
-
"schema",
|
|
235
|
-
"binding_id",
|
|
236
|
-
"actor_id",
|
|
237
|
-
"actor_class",
|
|
238
|
-
"public_key_hex",
|
|
239
|
-
"created_at",
|
|
240
|
-
"signature",
|
|
241
|
-
], [], "Receipt identity binding");
|
|
242
|
-
if (binding.schema !== "vela.identity_binding.v0.1" || binding.actor_class !== "agent") {
|
|
243
|
-
throw new Error("Receipt identity binding must be a Vela agent binding");
|
|
244
|
-
}
|
|
245
|
-
const actor = stringAt(binding.actor_id, "Receipt identity binding.actor_id", {
|
|
246
|
-
max: 69,
|
|
247
|
-
pattern: AGENT_RE,
|
|
248
|
-
});
|
|
249
|
-
const bindingId = stringAt(binding.binding_id, "Receipt identity binding.binding_id", {
|
|
250
|
-
max: 20,
|
|
251
|
-
pattern: /^vib_[0-9a-f]{16}$/u,
|
|
252
|
-
});
|
|
253
|
-
const publicKey = stringAt(binding.public_key_hex, "Receipt identity binding.public_key_hex", {
|
|
254
|
-
max: 64,
|
|
255
|
-
pattern: /^[0-9a-f]{64}$/u,
|
|
256
|
-
});
|
|
257
|
-
const createdAt = timestampAt(binding.created_at, "Receipt identity binding.created_at");
|
|
258
|
-
const signatureHex = stringAt(binding.signature, "Receipt identity binding.signature", {
|
|
259
|
-
max: 128,
|
|
260
|
-
pattern: /^[0-9a-f]{128}$/u,
|
|
261
|
-
});
|
|
262
|
-
const preimage = {
|
|
263
|
-
schema: "vela.identity_binding.v0.1",
|
|
264
|
-
binding_id: "",
|
|
265
|
-
actor_id: actor,
|
|
266
|
-
actor_class: "agent",
|
|
267
|
-
public_key_hex: publicKey,
|
|
268
|
-
created_at: createdAt,
|
|
269
|
-
signature: "",
|
|
270
|
-
};
|
|
271
|
-
const preimageBytes = Buffer.from(canonicalJcs(preimage));
|
|
272
|
-
const expectedId = `vib_${sha256Bytes(preimageBytes).slice("sha256:".length, "sha256:".length + 16)}`;
|
|
273
|
-
if (bindingId !== expectedId)
|
|
274
|
-
throw new Error("Receipt identity binding id does not rederive");
|
|
275
|
-
const publicDer = Buffer.concat([
|
|
276
|
-
Buffer.from("302a300506032b6570032100", "hex"),
|
|
277
|
-
Buffer.from(publicKey, "hex"),
|
|
278
|
-
]);
|
|
279
|
-
const signature = Buffer.from(signatureHex, "hex");
|
|
280
|
-
try {
|
|
281
|
-
const key = createPublicKey({ key: publicDer, format: "der", type: "spki" });
|
|
282
|
-
if (!verifyEd25519(null, preimageBytes, key, signature)) {
|
|
283
|
-
throw new Error("Receipt identity binding has no valid proof of possession");
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
finally {
|
|
287
|
-
publicDer.fill(0);
|
|
288
|
-
signature.fill(0);
|
|
289
|
-
}
|
|
290
|
-
return { actor, bindingId, publicKey };
|
|
291
|
-
}
|
|
292
|
-
async function checkedCapabilityDirectory(proposalId, storeRoot) {
|
|
293
|
-
const base = capabilityBase(storeRoot);
|
|
294
|
-
const directory = capabilityDirectory(proposalId, storeRoot);
|
|
295
|
-
const [baseMetadata, directoryMetadata] = await Promise.all([lstat(base), lstat(directory)]);
|
|
296
|
-
if (!baseMetadata.isDirectory() || baseMetadata.isSymbolicLink() || (baseMetadata.mode & 0o777) !== 0o700) {
|
|
297
|
-
throw new Error("withdrawal capability store must be one mode-0700 directory");
|
|
298
|
-
}
|
|
299
|
-
if (!directoryMetadata.isDirectory() ||
|
|
300
|
-
directoryMetadata.isSymbolicLink() ||
|
|
301
|
-
(directoryMetadata.mode & 0o777) !== 0o700) {
|
|
302
|
-
throw new Error("withdrawal capability directory must be one mode-0700 directory");
|
|
303
|
-
}
|
|
304
|
-
return { base, directory };
|
|
305
|
-
}
|
|
306
|
-
export async function retainWithdrawalCapability(options) {
|
|
307
|
-
if (options.landing.route !== "defer" && options.landing.originalRoute !== "defer") {
|
|
308
|
-
throw new Error("only a pending deferred proposal may retain a withdrawal capability");
|
|
309
|
-
}
|
|
310
|
-
const frontier = frontierAt(options.mission.frontier, "mission.frontier");
|
|
311
|
-
const frontierRoot = path.resolve(options.landingRepo, frontier);
|
|
312
|
-
const proposalPath = path.join(frontierRoot, ".vela", "proposals", `${options.landing.proposalId}.json`);
|
|
313
|
-
const proposal = object(JSON.parse((await readBoundedRegularFile(proposalPath, 8 * 1024 * 1024)).toString("utf8")), "proposal");
|
|
314
|
-
if (text(proposal.id, "proposal.id") !== options.landing.proposalId) {
|
|
315
|
-
throw new Error("landed proposal id does not match its canonical file");
|
|
316
|
-
}
|
|
317
|
-
if (text(proposal.status, "proposal.status") !== "pending_review") {
|
|
318
|
-
throw new Error("landed proposal is not pending_review");
|
|
319
|
-
}
|
|
320
|
-
const proposalRoot = protocolDigest(proposal);
|
|
321
|
-
const payload = object(proposal.payload, "proposal.payload");
|
|
322
|
-
const submission = object(payload.vela_submission, "proposal.payload.vela_submission");
|
|
323
|
-
const receiptRoot = text(submission.receipt_root, "vela_submission.receipt_root");
|
|
324
|
-
if (receiptRoot !== options.landing.receiptRoot)
|
|
325
|
-
throw new Error("landed Receipt root drifted");
|
|
326
|
-
const receiptPath = relativePathAt(submission.receipt_path, "vela_submission.receipt_path");
|
|
327
|
-
const receipt = object(JSON.parse((await readBoundedRegularFile(path.join(frontierRoot, receiptPath), 16 * 1024 * 1024)).toString("utf8")), "Receipt");
|
|
328
|
-
if (protocolDigest(receipt) !== receiptRoot)
|
|
329
|
-
throw new Error("bound Receipt bytes do not match their root");
|
|
330
|
-
const environment = object(receipt.environment, "Receipt.environment");
|
|
331
|
-
const producer = object(environment["vela:producer_context"], "Receipt producer context");
|
|
332
|
-
const binding = verifyIdentityBinding(producer.identity_binding);
|
|
333
|
-
const actor = binding.actor;
|
|
334
|
-
if (actor !== options.mission.actor || actor !== object(proposal.actor, "proposal.actor").id) {
|
|
335
|
-
throw new Error("Receipt identity actor does not match the mission and proposal");
|
|
336
|
-
}
|
|
337
|
-
const identityBindingId = binding.bindingId;
|
|
338
|
-
const publicKey = binding.publicKey;
|
|
339
|
-
const sourceKey = agentKeyPath(options.velaHome, actor);
|
|
340
|
-
const sourceMetadata = await lstat(sourceKey);
|
|
341
|
-
if (!sourceMetadata.isFile() || sourceMetadata.isSymbolicLink() || sourceMetadata.nlink !== 1 || (sourceMetadata.mode & 0o777) !== 0o600) {
|
|
342
|
-
throw new Error("producer session key must be one singly-linked mode-0600 regular file");
|
|
343
|
-
}
|
|
344
|
-
const keyBytes = await readBoundedRegularFile(sourceKey, PRIVATE_KEY_MAX_BYTES);
|
|
345
|
-
let seed;
|
|
346
|
-
let directory;
|
|
347
|
-
try {
|
|
348
|
-
seed = decodeSeed(keyBytes);
|
|
349
|
-
if (derivePublicKey(seed) !== publicKey) {
|
|
350
|
-
throw new Error("producer session key does not derive the Receipt-bound public key");
|
|
351
|
-
}
|
|
352
|
-
directory = capabilityDirectory(options.landing.proposalId, options.storeRoot);
|
|
353
|
-
await mkdir(capabilityBase(options.storeRoot), { recursive: true, mode: 0o700 });
|
|
354
|
-
await chmod(capabilityBase(options.storeRoot), 0o700);
|
|
355
|
-
await mkdir(directory, { mode: 0o700 });
|
|
356
|
-
const manifest = {
|
|
357
|
-
schema: WITHDRAWAL_CAPABILITY_SCHEMA,
|
|
358
|
-
state: "available",
|
|
359
|
-
proposal_id: options.landing.proposalId,
|
|
360
|
-
proposal_root: proposalRoot,
|
|
361
|
-
receipt_root: receiptRoot,
|
|
362
|
-
identity_binding_id: identityBindingId,
|
|
363
|
-
actor,
|
|
364
|
-
public_key: publicKey,
|
|
365
|
-
frontier,
|
|
366
|
-
final_roots: options.finalRoots,
|
|
367
|
-
strict_baseline: options.mission.schema === "canopus.mission.v1"
|
|
368
|
-
? options.mission.strict_baseline
|
|
369
|
-
: {
|
|
370
|
-
status: "pass",
|
|
371
|
-
blocker_count: 0,
|
|
372
|
-
blockers_root: sha256Bytes(canonicalJcs([])),
|
|
373
|
-
rule_counts: [],
|
|
374
|
-
},
|
|
375
|
-
vela: {
|
|
376
|
-
binary: path.resolve(options.velaBinary),
|
|
377
|
-
version: options.mission.vela_version,
|
|
378
|
-
sha256: options.mission.vela_sha256,
|
|
379
|
-
},
|
|
380
|
-
created_at: new Date().toISOString(),
|
|
381
|
-
};
|
|
382
|
-
await writeFile(path.join(directory, "manifest.json"), canonicalJson(manifest), { flag: "wx", mode: 0o600 });
|
|
383
|
-
// Publish the secret last: an interruption before this point can leave at
|
|
384
|
-
// most a public, unavailable manifest rather than an orphaned key.
|
|
385
|
-
await writeFile(path.join(directory, "private.key"), keyBytes, { flag: "wx", mode: 0o600 });
|
|
386
|
-
await chmod(path.join(directory, "private.key"), 0o600);
|
|
387
|
-
return { directory, manifest, manifest_root: contentDigest(manifest) };
|
|
388
|
-
}
|
|
389
|
-
catch (error) {
|
|
390
|
-
if (directory !== undefined)
|
|
391
|
-
await rm(directory, { recursive: true, force: true });
|
|
392
|
-
throw error;
|
|
393
|
-
}
|
|
394
|
-
finally {
|
|
395
|
-
seed?.fill(0);
|
|
396
|
-
keyBytes.fill(0);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
export async function loadWithdrawalCapability(proposalId, storeRoot) {
|
|
400
|
-
const { directory } = await checkedCapabilityDirectory(proposalId, storeRoot);
|
|
401
|
-
const manifestBytes = await readBoundedRegularFile(path.join(directory, "manifest.json"), 1024 * 1024);
|
|
402
|
-
const manifestMetadata = await lstat(path.join(directory, "manifest.json"));
|
|
403
|
-
if ((manifestMetadata.mode & 0o777) !== 0o600) {
|
|
404
|
-
throw new Error("withdrawal capability manifest must be mode 0600");
|
|
405
|
-
}
|
|
406
|
-
const raw = parseManifest(JSON.parse(manifestBytes.toString("utf8")), proposalId);
|
|
407
|
-
let secretAvailable = false;
|
|
408
|
-
try {
|
|
409
|
-
const metadata = await lstat(path.join(directory, "private.key"));
|
|
410
|
-
if (!metadata.isFile() ||
|
|
411
|
-
metadata.isSymbolicLink() ||
|
|
412
|
-
metadata.nlink !== 1 ||
|
|
413
|
-
(metadata.mode & 0o777) !== 0o600 ||
|
|
414
|
-
(metadata.size !== HEX_SEED_BYTES && metadata.size !== PRIVATE_KEY_MAX_BYTES)) {
|
|
415
|
-
throw new Error("withdrawal secret must be one singly-linked mode-0600 seed file");
|
|
416
|
-
}
|
|
417
|
-
secretAvailable = true;
|
|
418
|
-
}
|
|
419
|
-
catch (error) {
|
|
420
|
-
if (error.code !== "ENOENT")
|
|
421
|
-
throw error;
|
|
422
|
-
}
|
|
423
|
-
if (raw.state === "available" && !secretAvailable) {
|
|
424
|
-
throw new Error("withdrawal capability state disagrees with secret presence");
|
|
425
|
-
}
|
|
426
|
-
return { directory, manifest: raw, manifest_root: contentDigest(raw), secret_available: secretAvailable };
|
|
427
|
-
}
|
|
428
|
-
export async function installWithdrawalCapabilitySecret(proposalId, target, storeRoot) {
|
|
429
|
-
const loaded = await loadWithdrawalCapability(proposalId, storeRoot);
|
|
430
|
-
if (loaded.manifest.state !== "available" || !loaded.secret_available) {
|
|
431
|
-
throw new Error("withdrawal capability is not available");
|
|
432
|
-
}
|
|
433
|
-
const bytes = await readBoundedRegularFile(path.join(loaded.directory, "private.key"), PRIVATE_KEY_MAX_BYTES);
|
|
434
|
-
let seed;
|
|
435
|
-
try {
|
|
436
|
-
seed = decodeSeed(bytes);
|
|
437
|
-
if (derivePublicKey(seed) !== loaded.manifest.public_key) {
|
|
438
|
-
throw new Error("withdrawal secret does not match the capability public key");
|
|
439
|
-
}
|
|
440
|
-
await writeFile(target, bytes, { flag: "wx", mode: 0o600 });
|
|
441
|
-
await chmod(target, 0o600);
|
|
442
|
-
}
|
|
443
|
-
finally {
|
|
444
|
-
seed?.fill(0);
|
|
445
|
-
bytes.fill(0);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
export async function withdrawalCapabilityStatus(proposalId, storeRoot) {
|
|
449
|
-
try {
|
|
450
|
-
const loaded = await loadWithdrawalCapability(proposalId, storeRoot);
|
|
451
|
-
return {
|
|
452
|
-
proposal_id: proposalId,
|
|
453
|
-
state: loaded.manifest.state,
|
|
454
|
-
available: loaded.manifest.state === "available" && loaded.secret_available,
|
|
455
|
-
cleanup_required: loaded.manifest.state === "consumed" && loaded.secret_available,
|
|
456
|
-
manifest_root: loaded.manifest_root,
|
|
457
|
-
consumed_at: loaded.manifest.consumed_at ?? null,
|
|
458
|
-
consumed_reason: loaded.manifest.consumed_reason ?? null,
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
catch (error) {
|
|
462
|
-
if (error.code === "ENOENT") {
|
|
463
|
-
return { proposal_id: proposalId, state: "absent", available: false };
|
|
464
|
-
}
|
|
465
|
-
throw error;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
export async function consumeWithdrawalCapability(proposalId, reason, storeRoot) {
|
|
469
|
-
const loaded = await loadWithdrawalCapability(proposalId, storeRoot);
|
|
470
|
-
if (loaded.manifest.state === "consumed") {
|
|
471
|
-
if (loaded.secret_available) {
|
|
472
|
-
await rm(path.join(loaded.directory, "private.key"), { force: true });
|
|
473
|
-
}
|
|
474
|
-
return loaded.manifest;
|
|
475
|
-
}
|
|
476
|
-
const consumed = {
|
|
477
|
-
...loaded.manifest,
|
|
478
|
-
state: "consumed",
|
|
479
|
-
consumed_at: new Date().toISOString(),
|
|
480
|
-
consumed_reason: reason,
|
|
481
|
-
};
|
|
482
|
-
const temporary = path.join(loaded.directory, `.manifest.${process.pid}.tmp`);
|
|
483
|
-
await writeFile(temporary, canonicalJson(consumed), { flag: "wx", mode: 0o600 });
|
|
484
|
-
await rename(temporary, path.join(loaded.directory, "manifest.json"));
|
|
485
|
-
await rm(path.join(loaded.directory, "private.key"), { force: true });
|
|
486
|
-
return consumed;
|
|
487
|
-
}
|