@sanning/proof 0.17.0 → 0.19.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/README.md +2 -2
- package/dist/case-file.js +2 -1
- package/dist/case.js +11 -4
- package/dist/chain.d.ts +28 -0
- package/dist/chain.js +61 -4
- package/dist/cli.js +226 -33
- package/dist/crypto.d.ts +17 -0
- package/dist/crypto.js +116 -16
- package/dist/declaration.d.ts +43 -4
- package/dist/declaration.js +198 -14
- package/dist/environment.d.ts +42 -0
- package/dist/environment.js +136 -0
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +32 -1
- package/dist/exit-codes.d.ts +18 -0
- package/dist/exit-codes.js +25 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +21 -5
- package/dist/run-adapters.d.ts +10 -0
- package/dist/run-adapters.js +35 -0
- package/dist/run.d.ts +209 -0
- package/dist/run.js +1405 -0
- package/dist/verifier.d.ts +43 -0
- package/dist/verifier.js +113 -6
- package/dist/witness-lookup.d.ts +13 -1
- package/dist/witness-lookup.js +12 -2
- package/package.json +2 -2
- package/src/case-file.ts +2 -1
- package/src/case.ts +11 -4
- package/src/chain.ts +81 -4
- package/src/cli.ts +259 -28
- package/src/crypto.ts +125 -16
- package/src/declaration.ts +242 -12
- package/src/environment.ts +145 -0
- package/src/evidence.ts +29 -1
- package/src/exit-codes.ts +28 -0
- package/src/index.ts +31 -1
- package/src/run-adapters.ts +44 -0
- package/src/run.ts +1598 -0
- package/src/verifier.ts +102 -5
- package/src/witness-lookup.ts +18 -1
- package/vectors/README.md +37 -0
- package/vectors/sandbox-evidence.json +3161 -0
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
// could not be reached reads `partial`; a pack whose records nobody hashed
|
|
18
18
|
// reads `incomplete` (see below)
|
|
19
19
|
// 4 mixed
|
|
20
|
+
// 5 sandbox: a record is signed environment dev; would be 0, or only a
|
|
21
|
+
// simulated witness is unchecked (`sandboxExit`)
|
|
20
22
|
// `sanningd verify-status` in sanning-io/agent mirrored the 0.12 table, which
|
|
21
23
|
// exited 0 on `partial`; it moves with this one.
|
|
22
24
|
//
|
|
@@ -47,13 +49,15 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
47
49
|
import { basename, resolve, sep } from "node:path";
|
|
48
50
|
import { fileURLToPath } from "node:url";
|
|
49
51
|
import { isAgentProofSpec, verifyAgentProof } from "./agent-proof.js";
|
|
50
|
-
import { CHAIN_READING_SCOPE, readChains, renderChainFinding } from "./chain.js";
|
|
52
|
+
import { CHAIN_READING_SCOPE, readChainLinks, readChains, renderChainFinding } from "./chain.js";
|
|
51
53
|
import { VERDICT_DISPLAY_TITLES, caseReliance, readCase, renderCaseNote, renderCaseSummary, renderException, renderWitnessLine, } from "./case.js";
|
|
52
54
|
import { CASE_FILE_INDEX_NAME, caseFileListedPaths, jsonDepthExceeds, readCaseFileIndex, renderCaseFileFinding, unreadableCaseFileIndex, } from "./case-file.js";
|
|
53
55
|
import { MAX_ZIP_ARCHIVE_BYTES, MAX_ZIP_BYTES, ZipReadError, readZip } from "./zip-reader.js";
|
|
54
56
|
import { composeExport } from "./compose.js";
|
|
55
57
|
import { isHexField, sha256Hex, utf8 } from "./crypto.js";
|
|
56
58
|
import { verifyEvidenceBundle } from "./evidence.js";
|
|
59
|
+
import { readRuns, renderAccountLine, renderEnclaveLines } from "./run.js";
|
|
60
|
+
import { loadInstalledAdapters } from "./run-adapters.js";
|
|
57
61
|
import { DuplicateKeyError, parseRefusingDuplicates } from "./verifier.js";
|
|
58
62
|
import { fileMatchToJson, matchCaseFiles, matchFiles, notAccountedFor, renderFileMatch, } from "./file-match.js";
|
|
59
63
|
import { WITNESS_CACHE_NAME, readWitnessCache, } from "./witness-cache.js";
|
|
@@ -65,7 +69,8 @@ import { WITNESS_CACHE_NAME, readWitnessCache, } from "./witness-cache.js";
|
|
|
65
69
|
import { onChainHalf, witnessLine } from "./witness-lookup.js";
|
|
66
70
|
import { checkDeclaration, renderDeclarationSummary, renderNote } from "./declaration.js";
|
|
67
71
|
import { completenessNotes, gradePack, renderCause } from "./grading.js";
|
|
68
|
-
import { EXIT_CODES_HELP, EXIT_FAILED, EXIT_MALFORMED, EXIT_UNDETERMINED, EXIT_VERIFIED, exitCodeFor, } from "./exit-codes.js";
|
|
72
|
+
import { EXIT_CODES_HELP, EXIT_FAILED, EXIT_MALFORMED, EXIT_SANDBOX, EXIT_UNDETERMINED, EXIT_VERIFIED, exitCodeFor, sandboxExit, } from "./exit-codes.js";
|
|
73
|
+
import { environmentMarker, onlySimulatedUnsettled, simulatedOnlyRun, SANDBOX_HEADLINE, SANDBOX_LINE, SANDBOX_TITLE, } from "./environment.js";
|
|
69
74
|
// ── What a person reads is escaped at one choke point ──
|
|
70
75
|
//
|
|
71
76
|
// Every human-readable line this command prints passes through `guard`, which
|
|
@@ -326,7 +331,7 @@ async function runVerify(rest, io, sink) {
|
|
|
326
331
|
gateways,
|
|
327
332
|
...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}),
|
|
328
333
|
});
|
|
329
|
-
return reportAgentProof(result, bundlePath, gateways, io, sink);
|
|
334
|
+
return reportAgentProof(result, bundlePath, gateways, io, sink, parsed);
|
|
330
335
|
}
|
|
331
336
|
// Default: treat anything else as an evidence bundle. verifyEvidenceBundle
|
|
332
337
|
// rejects an unknown major as "malformed" so a genuinely unknown shape still
|
|
@@ -338,8 +343,11 @@ async function runVerify(rest, io, sink) {
|
|
|
338
343
|
});
|
|
339
344
|
// Read the chains from the SAME parsed bundle the verifier saw. Findings are
|
|
340
345
|
// evidence about the records in front of us, not a verdict: they are printed
|
|
341
|
-
// and they
|
|
342
|
-
//
|
|
346
|
+
// and they move `exit` only through the pack's own declaration, where a v3
|
|
347
|
+
// declaration claims a chain complete and in order and a finding under that
|
|
348
|
+
// claim contradicts it (rule 11). Without a claim a chain is not a
|
|
349
|
+
// completeness proof, and an exit code is exactly the kind of thing people
|
|
350
|
+
// read as one.
|
|
343
351
|
//
|
|
344
352
|
// A READING THAT COULD NOT BE PRODUCED SAYS SO. This was an unguarded call:
|
|
345
353
|
// a throw out of `readChains` would have escaped the command entirely and
|
|
@@ -355,7 +363,17 @@ async function runVerify(rest, io, sink) {
|
|
|
355
363
|
catch (e) {
|
|
356
364
|
chains = { unreadable: errMsg(e) };
|
|
357
365
|
}
|
|
358
|
-
|
|
366
|
+
// The links rule 11 holds a claimed chain to (evidence-bundle.md 5.4.2), from
|
|
367
|
+
// the same parsed bundle. Links that cannot be read are none, which reads a
|
|
368
|
+
// claim UNKNOWN and never clean.
|
|
369
|
+
let links;
|
|
370
|
+
try {
|
|
371
|
+
links = "unreadable" in chains ? null : await readChainLinks(parsed);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
links = null;
|
|
375
|
+
}
|
|
376
|
+
const exit = reportEvidence(result, bundlePath, gateways, io, chains, links, parsed, logs, sink);
|
|
359
377
|
// Files handed in beside the pack (evidence-bundle.md 5.7, A29): findings
|
|
360
378
|
// beside the verdict, never inside it, so `exit` is untouched.
|
|
361
379
|
if (offered !== undefined) {
|
|
@@ -1234,7 +1252,7 @@ function cliDisclosureAccount(result, logs) {
|
|
|
1234
1252
|
extra: (logs?.suppliedIds ?? []).filter((id) => !known.has(id)),
|
|
1235
1253
|
};
|
|
1236
1254
|
}
|
|
1237
|
-
function reportEvidence(result, path, gateways, io, chains, parsed, logs, sink) {
|
|
1255
|
+
function reportEvidence(result, path, gateways, io, chains, links, parsed, logs, sink) {
|
|
1238
1256
|
if (result.status === "malformed") {
|
|
1239
1257
|
if (sink)
|
|
1240
1258
|
sink.data = verifyJson(result, null, null, null);
|
|
@@ -1324,10 +1342,24 @@ function reportEvidence(result, path, gateways, io, chains, parsed, logs, sink)
|
|
|
1324
1342
|
// their verdicts rather than a new check. A pack that DOES
|
|
1325
1343
|
// carry a declaration this reader cannot read is a different case and exits
|
|
1326
1344
|
// 3: the holder chose that field, so UNKNOWN is the honest answer.
|
|
1327
|
-
|
|
1345
|
+
//
|
|
1346
|
+
// A v3 declaration that claims a chain is checked against this command's
|
|
1347
|
+
// own chain links (rule 11, evidence-bundle.md 5.4.2 v1.36, A45). Links
|
|
1348
|
+
// that could not be read are passed as none, which reads the claim UNKNOWN
|
|
1349
|
+
// and never clean.
|
|
1350
|
+
const declaration = checkDeclaration(parsed, logs?.content, links);
|
|
1328
1351
|
const grade = gradePack(result, cliDisclosureAccount(result, logs), completenessNotes(parsed), declaration);
|
|
1352
|
+
// Sandbox evidence: read from the signed envelopes the pack holds.
|
|
1353
|
+
const marker = environmentMarker([parsed]);
|
|
1354
|
+
// A pack whose records are all signed `dev`, undetermined only because a
|
|
1355
|
+
// simulated witness cannot be looked up, would grade `verified` with that
|
|
1356
|
+
// witness accepted: it is sandbox evidence, exit 5, never 3.
|
|
1357
|
+
const simulatedOnly = grade.verdict === "partial" &&
|
|
1358
|
+
onlySimulatedUnsettled(result) &&
|
|
1359
|
+
gradePack({ ...result, status: "verified" }, cliDisclosureAccount(result, logs), completenessNotes(parsed), declaration).verdict === "verified";
|
|
1360
|
+
const exit = sandboxExit(mapGradeToExit(grade), marker, simulatedOnly);
|
|
1329
1361
|
io.out("");
|
|
1330
|
-
printRollup(grade.verdict, result.status, result.assertedStatus, gateways, result.onChainChecked, io);
|
|
1362
|
+
printRollup(grade.verdict, result.status, result.assertedStatus, gateways, result.onChainChecked, io, marker, exit === EXIT_SANDBOX, simulatedOnlyRun(result, gateways));
|
|
1331
1363
|
io.out(paint(` declaration: ${renderDeclarationSummary(declaration)}`, declaration.verdict === "failed" ? R : declaration.verdict === "verified" ? DIM : Y));
|
|
1332
1364
|
for (const n of declaration.notes)
|
|
1333
1365
|
io.out(paint(` note: ${renderNote(n)}`, Y));
|
|
@@ -1346,15 +1378,15 @@ function reportEvidence(result, path, gateways, io, chains, parsed, logs, sink)
|
|
|
1346
1378
|
io.out(paint(` logs: ${grade.recordsVerified}/${result.events.length} record(s) content-verified`, DIM));
|
|
1347
1379
|
}
|
|
1348
1380
|
if (sink)
|
|
1349
|
-
sink.data = verifyJson(result, grade, declaration, chains);
|
|
1350
|
-
return
|
|
1381
|
+
sink.data = verifyJson(result, grade, declaration, chains, marker);
|
|
1382
|
+
return exit;
|
|
1351
1383
|
}
|
|
1352
1384
|
/**
|
|
1353
1385
|
* What `verify --json` prints for an evidence bundle, the same keys and the
|
|
1354
1386
|
* same words as `_verify_json` in the Python twin. `grade`, `declaration` and
|
|
1355
1387
|
* `chains` are null for a bundle no verdict could be rendered over.
|
|
1356
1388
|
*/
|
|
1357
|
-
function verifyJson(result, grade, declaration, chains) {
|
|
1389
|
+
function verifyJson(result, grade, declaration, chains, marker = null) {
|
|
1358
1390
|
let chainBlock = null;
|
|
1359
1391
|
if (chains !== null) {
|
|
1360
1392
|
chainBlock =
|
|
@@ -1380,6 +1412,9 @@ function verifyJson(result, grade, declaration, chains) {
|
|
|
1380
1412
|
? { presented: result.events.length, content_verified: grade.recordsVerified }
|
|
1381
1413
|
: null,
|
|
1382
1414
|
chains: chainBlock,
|
|
1415
|
+
// Present only where the pack holds an envelope signed `dev`
|
|
1416
|
+
// (envelope-spec §2 MAY), so a production pack's JSON is unchanged.
|
|
1417
|
+
...(marker !== null ? { environment_marker: marker } : {}),
|
|
1383
1418
|
};
|
|
1384
1419
|
}
|
|
1385
1420
|
/**
|
|
@@ -1440,7 +1475,8 @@ function reportExport(exp, io) {
|
|
|
1440
1475
|
}
|
|
1441
1476
|
}
|
|
1442
1477
|
}
|
|
1443
|
-
function reportAgentProof(result, path, gateways, io, sink) {
|
|
1478
|
+
function reportAgentProof(result, path, gateways, io, sink, parsed) {
|
|
1479
|
+
const marker = result.status === "malformed" ? null : environmentMarker([parsed]);
|
|
1444
1480
|
if (sink) {
|
|
1445
1481
|
sink.data = {
|
|
1446
1482
|
verdict: result.status,
|
|
@@ -1448,6 +1484,7 @@ function reportAgentProof(result, path, gateways, io, sink) {
|
|
|
1448
1484
|
spec_version: "sanning.agent.proof/v1",
|
|
1449
1485
|
on_chain_checked: result.onChainChecked,
|
|
1450
1486
|
notes: [...result.errors],
|
|
1487
|
+
...(marker !== null ? { environment_marker: marker } : {}),
|
|
1451
1488
|
};
|
|
1452
1489
|
}
|
|
1453
1490
|
if (result.status === "malformed") {
|
|
@@ -1464,17 +1501,30 @@ function reportAgentProof(result, path, gateways, io, sink) {
|
|
|
1464
1501
|
io.out("");
|
|
1465
1502
|
// An agent inclusion proof carries no records and no disclosed bytes, so
|
|
1466
1503
|
// there is no pack to grade: the bundle status IS the headline here.
|
|
1467
|
-
|
|
1504
|
+
const exit = sandboxExit(exitCodeFor(result.status), marker);
|
|
1505
|
+
printRollup(result.status, result.status, null, gateways, result.onChainChecked, io, marker, exit === EXIT_SANDBOX);
|
|
1468
1506
|
for (const e of result.errors)
|
|
1469
1507
|
io.out(paint(` note: ${e}`, DIM));
|
|
1470
|
-
return
|
|
1508
|
+
return exit;
|
|
1471
1509
|
}
|
|
1472
|
-
function printRollup(headline, bundleStatus, asserted, gateways, onChainChecked, io) {
|
|
1510
|
+
function printRollup(headline, bundleStatus, asserted, gateways, onChainChecked, io, marker = null, sandbox = false, simulatedOnly = false) {
|
|
1511
|
+
// Three scopes, not two. A run with gateways whose only witnesses are
|
|
1512
|
+
// simulated looked nothing up (`simulatedOnlyRun`): saying "on-chain" there
|
|
1513
|
+
// claims a check that never happened, and saying "no gateway re-fetch
|
|
1514
|
+
// requested" denies the flag the reader passed. Every other run that is not
|
|
1515
|
+
// counted as checked on a chain, a run that stopped early among them, keeps
|
|
1516
|
+
// the second scope as before.
|
|
1473
1517
|
const scope = onChainChecked
|
|
1474
1518
|
? `verified offline + on-chain against ${gateways.length} gateway(s)`
|
|
1475
|
-
:
|
|
1519
|
+
: simulatedOnly
|
|
1520
|
+
? "verified offline; a simulated witness is not on a chain, so nothing was looked up"
|
|
1521
|
+
: "verified offline (no gateway re-fetch requested)";
|
|
1476
1522
|
let head;
|
|
1477
|
-
switch (headline) {
|
|
1523
|
+
switch (sandbox ? "sandbox" : headline) {
|
|
1524
|
+
// Sandbox evidence (exit 5) never prints VERIFIED.
|
|
1525
|
+
case "sandbox":
|
|
1526
|
+
head = paint(SANDBOX_HEADLINE, Y);
|
|
1527
|
+
break;
|
|
1478
1528
|
case "verified":
|
|
1479
1529
|
head = paint("VERIFIED", G);
|
|
1480
1530
|
break;
|
|
@@ -1492,6 +1542,8 @@ function printRollup(headline, bundleStatus, asserted, gateways, onChainChecked,
|
|
|
1492
1542
|
head = paint(headline.toUpperCase(), R);
|
|
1493
1543
|
}
|
|
1494
1544
|
io.out(`${head} ${paint(scope, DIM)}`);
|
|
1545
|
+
if (marker !== null)
|
|
1546
|
+
io.out(paint(` ${SANDBOX_LINE}`, Y));
|
|
1495
1547
|
// Both verdicts, whenever they differ, because they answer different
|
|
1496
1548
|
// questions: §3 grades the signed bundle, the pack verdict grades the bundle
|
|
1497
1549
|
// plus whatever bytes travelled beside it. A pack can be incomplete while the
|
|
@@ -2072,7 +2124,7 @@ async function unzip(arg, prefix, tree, seen) {
|
|
|
2072
2124
|
// 5.5.4 v1.16, A17), so it reads `failed` and exits 1.
|
|
2073
2125
|
export const CASE_USAGE = `sanning-proof case -- a case reading: one verdict over one pack or many, offline
|
|
2074
2126
|
|
|
2075
|
-
sanning-proof case <folder or case file>... [--gateways <url,url>] [--files <path>...] [--json]
|
|
2127
|
+
sanning-proof case <folder or case file>... [--gateways <url,url>] [--files <path>...] [--enclave-policy <file>] [--enclave-rpc <url>] [--json]
|
|
2076
2128
|
|
|
2077
2129
|
<folder> a pack folder (bundle.json plus logs/), a folder that holds
|
|
2078
2130
|
several packs, or an unzipped case file. Packs are found by the
|
|
@@ -2110,6 +2162,17 @@ export const CASE_USAGE = `sanning-proof case -- a case reading: one verdict ove
|
|
|
2110
2162
|
contribution, the packs, each with its index, the files outside
|
|
2111
2163
|
them, the case file index comparison, the witness line and the
|
|
2112
2164
|
on-chain half.
|
|
2165
|
+
--enclave-policy
|
|
2166
|
+
a sanning.enclave-policy/v1 file: what this reader accepts of
|
|
2167
|
+
an enclave attestation. Where the packs hold a runner's run, an
|
|
2168
|
+
enclave attestation line and a run-account line print beside
|
|
2169
|
+
the witness line (DRAFT); neither moves the verdict or the exit
|
|
2170
|
+
code. Without a policy, or without an adapter for the provider
|
|
2171
|
+
installed, the line reads not_checked.
|
|
2172
|
+
--enclave-rpc
|
|
2173
|
+
a chain JSON-RPC endpoint: a flare-fcc run also gets the online
|
|
2174
|
+
line, read from the contracts the policy names. It moves
|
|
2175
|
+
nothing either.
|
|
2113
2176
|
|
|
2114
2177
|
Files outside every pack are matched too, always, so
|
|
2115
2178
|
sanning-proof case audit-file.zip checks a whole download in one command: each
|
|
@@ -2184,12 +2247,21 @@ const titleOf = (word) => Object.prototype.hasOwnProperty.call(VERDICT_DISPLAY_T
|
|
|
2184
2247
|
* scope sentence and the witness line are printed with it every time.
|
|
2185
2248
|
*/
|
|
2186
2249
|
function caseReport(data) {
|
|
2187
|
-
const lines = [
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
` ${
|
|
2191
|
-
|
|
2192
|
-
|
|
2250
|
+
const lines = [data.title];
|
|
2251
|
+
// Sandbox evidence: printed with every headline the marker is on.
|
|
2252
|
+
if (data.environment_marker !== undefined)
|
|
2253
|
+
lines.push(` ${SANDBOX_LINE}`);
|
|
2254
|
+
lines.push(` ${data.summary}`, ` ${data.witness.rendered}`);
|
|
2255
|
+
// Where the packs hold a runner's run: its enclave attestation line and its
|
|
2256
|
+
// run-account line, beside the witness line.
|
|
2257
|
+
const enclave = data.enclave_attestation ?? [];
|
|
2258
|
+
const account = data.run_account ?? [];
|
|
2259
|
+
for (let i = 0; i < Math.min(enclave.length, account.length); i++) {
|
|
2260
|
+
for (const line of enclave[i].rendered)
|
|
2261
|
+
lines.push(` ${line}`);
|
|
2262
|
+
lines.push(` ${account[i].rendered}`);
|
|
2263
|
+
}
|
|
2264
|
+
lines.push(` ${data.reliance}`);
|
|
2193
2265
|
for (const e of data.exceptions)
|
|
2194
2266
|
lines.push(` - ${e.rendered}`);
|
|
2195
2267
|
for (const n of data.notes)
|
|
@@ -2197,7 +2269,11 @@ function caseReport(data) {
|
|
|
2197
2269
|
lines.push(`packs ${data.packs.length}`);
|
|
2198
2270
|
for (const p of data.packs) {
|
|
2199
2271
|
const key = p.signing_key !== null ? p.signing_key : "a key this reader cannot read";
|
|
2200
|
-
const grade = p.grade
|
|
2272
|
+
const grade = p.grade === null
|
|
2273
|
+
? "not graded"
|
|
2274
|
+
: p.grade === "verified" && p.environment_marker !== undefined
|
|
2275
|
+
? SANDBOX_TITLE
|
|
2276
|
+
: titleOf(p.grade);
|
|
2201
2277
|
// Each record's status, counted per pack (A43): the JSON lists every one.
|
|
2202
2278
|
const mine = data.records.filter((r) => r.pack_index === p.index);
|
|
2203
2279
|
const count = (status) => mine.filter((r) => r.status === status).length;
|
|
@@ -2235,13 +2311,23 @@ function caseReport(data) {
|
|
|
2235
2311
|
}
|
|
2236
2312
|
const oc = data.on_chain;
|
|
2237
2313
|
if (oc.checked) {
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2314
|
+
// A drop whose only witnesses are simulated had nothing looked up on a
|
|
2315
|
+
// chain, and the header says so rather than "looked up against".
|
|
2316
|
+
const simulated = oc.packs.reduce((n, r) => n + (r.simulated ?? 0), 0);
|
|
2317
|
+
const onlySimulated = simulated > 0 &&
|
|
2318
|
+
oc.packs.every((r) => (r.simulated ?? 0) === r.witnesses && (r.witness_times ?? []).length === 0);
|
|
2319
|
+
lines.push(onlySimulated
|
|
2320
|
+
? "on-chain nothing looked up: every witness here is simulated, " +
|
|
2321
|
+
"which is not on a chain; the reading above is offline and this " +
|
|
2322
|
+
"does not change it"
|
|
2323
|
+
: `on-chain looked up against ${oc.gateways.length} gateway(s), ` +
|
|
2324
|
+
"pack by pack; the reading above is offline and this does not " +
|
|
2325
|
+
"change it");
|
|
2241
2326
|
for (const r of oc.packs) {
|
|
2327
|
+
const sim = r.simulated ? `, ${r.simulated} of them simulated (not on a chain)` : "";
|
|
2242
2328
|
lines.push(` ${r.ref} ${r.witnesses} witness(es): ${r.confirmed} ` +
|
|
2243
2329
|
`confirmed, ${r.mismatched} mismatched, ${r.unreachable} ` +
|
|
2244
|
-
`unreachable, ${r.not_checked} not checked`);
|
|
2330
|
+
`unreachable, ${r.not_checked} not checked${sim}`);
|
|
2245
2331
|
}
|
|
2246
2332
|
}
|
|
2247
2333
|
return lines;
|
|
@@ -2333,6 +2419,12 @@ export async function caseFileReadings(tree, whole, cap = MAX_FOLDER_BYTES) {
|
|
|
2333
2419
|
}
|
|
2334
2420
|
return out;
|
|
2335
2421
|
}
|
|
2422
|
+
// One pack's environment marker, as the key its JSON row carries only where
|
|
2423
|
+
// the pack holds an envelope signed `dev`.
|
|
2424
|
+
function packMarker(bundle) {
|
|
2425
|
+
const m = environmentMarker([bundle]);
|
|
2426
|
+
return m !== null ? { environment_marker: m } : {};
|
|
2427
|
+
}
|
|
2336
2428
|
function caseError(e, asJson, io) {
|
|
2337
2429
|
if (asJson)
|
|
2338
2430
|
outJson(io, jsonText({ error: e.message, exit_code: EXIT_MALFORMED, hint: e.hint }));
|
|
@@ -2417,11 +2509,69 @@ export async function looseFiles(tree, split, cap = MAX_FOLDER_BYTES) {
|
|
|
2417
2509
|
}
|
|
2418
2510
|
return { offered, skipped };
|
|
2419
2511
|
}
|
|
2512
|
+
// A policy file that is not JSON: read, and unreadable (`policy_unreadable`).
|
|
2513
|
+
const POLICY_NOT_JSON = Symbol("policy not JSON");
|
|
2514
|
+
async function readEnclavePolicy(path) {
|
|
2515
|
+
if (path === null)
|
|
2516
|
+
return null;
|
|
2517
|
+
let raw;
|
|
2518
|
+
try {
|
|
2519
|
+
if (!(await stat(path)).isFile())
|
|
2520
|
+
throw new CaseInputError(`--enclave-policy ${path} is not a readable file`);
|
|
2521
|
+
raw = new Uint8Array(await readFile(path));
|
|
2522
|
+
}
|
|
2523
|
+
catch (e) {
|
|
2524
|
+
if (e instanceof CaseInputError)
|
|
2525
|
+
throw e;
|
|
2526
|
+
throw new CaseInputError(`--enclave-policy ${path} is not a readable file`);
|
|
2527
|
+
}
|
|
2528
|
+
try {
|
|
2529
|
+
return parseRefusingDuplicates(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw));
|
|
2530
|
+
}
|
|
2531
|
+
catch {
|
|
2532
|
+
return POLICY_NOT_JSON;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
/** The two line members of `--json`: one entry per run, with the lines it prints. */
|
|
2536
|
+
function runLines(runs) {
|
|
2537
|
+
const enclave = runs.map((r) => ({
|
|
2538
|
+
pack_ref: r.packRef,
|
|
2539
|
+
pack_index: r.packIndex,
|
|
2540
|
+
event_id: r.eventId,
|
|
2541
|
+
runner_public_key: r.runnerPublicKey,
|
|
2542
|
+
agent_public_key: r.agentPublicKey,
|
|
2543
|
+
state: r.enclave.state,
|
|
2544
|
+
kinds: r.enclave.kinds,
|
|
2545
|
+
notes: r.enclave.notes,
|
|
2546
|
+
reason: r.enclave.reason,
|
|
2547
|
+
provider: r.enclave.provider,
|
|
2548
|
+
evaluation: r.enclave.evaluation,
|
|
2549
|
+
evaluated_at: r.enclave.evaluatedAt,
|
|
2550
|
+
token_checked: r.enclave.tokenChecked,
|
|
2551
|
+
online: r.online !== null ? { state: r.online.state, kinds: r.online.kinds } : null,
|
|
2552
|
+
rendered: renderEnclaveLines(r).map(escapeControl),
|
|
2553
|
+
}));
|
|
2554
|
+
const account = runs.map((r) => ({
|
|
2555
|
+
pack_ref: r.packRef,
|
|
2556
|
+
pack_index: r.packIndex,
|
|
2557
|
+
event_id: r.eventId,
|
|
2558
|
+
runner_public_key: r.runnerPublicKey,
|
|
2559
|
+
state: r.account.state,
|
|
2560
|
+
findings: r.account.findings.map((f) => ({ kind: f.kind, event_id: f.eventId })),
|
|
2561
|
+
notes: r.account.notes.map((n) => ({ kind: n.kind, event_id: n.eventId })),
|
|
2562
|
+
closing_event_id: r.account.closingEventId,
|
|
2563
|
+
reason: r.account.reason,
|
|
2564
|
+
rendered: escapeControl(renderAccountLine(r)),
|
|
2565
|
+
}));
|
|
2566
|
+
return [enclave, account];
|
|
2567
|
+
}
|
|
2420
2568
|
async function caseCommand(argv, io) {
|
|
2421
2569
|
let asJson = false;
|
|
2422
2570
|
let gateways = [];
|
|
2423
2571
|
const paths = [];
|
|
2424
2572
|
let filePaths;
|
|
2573
|
+
let enclavePolicy = null;
|
|
2574
|
+
let enclaveRpc = null;
|
|
2425
2575
|
for (let i = 0; i < argv.length; i++) {
|
|
2426
2576
|
const arg = argv[i];
|
|
2427
2577
|
if ((arg === "--help" || arg === "-h" || arg === "help") && paths.length === 0) {
|
|
@@ -2454,6 +2604,19 @@ async function caseCommand(argv, io) {
|
|
|
2454
2604
|
filePaths = [...(filePaths ?? []), ...taken];
|
|
2455
2605
|
i += taken.length;
|
|
2456
2606
|
}
|
|
2607
|
+
else if (arg === "--enclave-policy" || arg === "--enclave-rpc") {
|
|
2608
|
+
const next = argv[i + 1];
|
|
2609
|
+
if (next === undefined) {
|
|
2610
|
+
io.err(arg === "--enclave-policy" ? "case: --enclave-policy requires a policy file" : "case: --enclave-rpc requires a URL");
|
|
2611
|
+
outBlock(io, CASE_USAGE);
|
|
2612
|
+
return EXIT_MALFORMED;
|
|
2613
|
+
}
|
|
2614
|
+
if (arg === "--enclave-policy")
|
|
2615
|
+
enclavePolicy = next;
|
|
2616
|
+
else
|
|
2617
|
+
enclaveRpc = next;
|
|
2618
|
+
i++;
|
|
2619
|
+
}
|
|
2457
2620
|
else if (arg.startsWith("--")) {
|
|
2458
2621
|
io.err(`case: unknown option ${arg}`);
|
|
2459
2622
|
outBlock(io, CASE_USAGE);
|
|
@@ -2477,6 +2640,15 @@ async function caseCommand(argv, io) {
|
|
|
2477
2640
|
offered = read.offered;
|
|
2478
2641
|
skipped = read.skipped;
|
|
2479
2642
|
}
|
|
2643
|
+
let policy;
|
|
2644
|
+
try {
|
|
2645
|
+
policy = await readEnclavePolicy(enclavePolicy);
|
|
2646
|
+
}
|
|
2647
|
+
catch (e) {
|
|
2648
|
+
if (e instanceof CaseInputError)
|
|
2649
|
+
return caseError(e, asJson, io);
|
|
2650
|
+
throw e;
|
|
2651
|
+
}
|
|
2480
2652
|
const whole = { n: MAX_FOLDER_BYTES };
|
|
2481
2653
|
let split;
|
|
2482
2654
|
let tree;
|
|
@@ -2506,13 +2678,24 @@ async function caseCommand(argv, io) {
|
|
|
2506
2678
|
for (const [p, bundle] of bundles) {
|
|
2507
2679
|
caches.push(readWitnessCache(bundle, await witnessCacheBytes(tree, split, p, whole, cacheRefused)));
|
|
2508
2680
|
}
|
|
2509
|
-
const
|
|
2681
|
+
const confirmedStamps = [];
|
|
2682
|
+
const onChain = await onChainHalf(bundles.map(([p, bundle], i) => [p.ref, bundle, caches[i]]), gateways, { ...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}) }, confirmedStamps);
|
|
2510
2683
|
const witness = witnessLine(onChain, caches.flat());
|
|
2511
|
-
|
|
2684
|
+
// Sandbox evidence: over every envelope every pack holds, and per
|
|
2685
|
+
// pack for its row. Only a case that would otherwise exit 0 exits 5.
|
|
2686
|
+
const marker = environmentMarker(bundles.map(([, b]) => b));
|
|
2687
|
+
// Undetermined only because a simulated witness cannot be looked up: every
|
|
2688
|
+
// other witness confirmed, none unreachable or mismatched, every pack names
|
|
2689
|
+
// one. With every record signed `dev` that is sandbox evidence, exit 5.
|
|
2690
|
+
const simulatedOnly = reading.verdict === "verified" &&
|
|
2691
|
+
onChain.verdict === "undetermined" &&
|
|
2692
|
+
onChain.packs.some((r) => (r.simulated ?? 0) > 0) &&
|
|
2693
|
+
onChain.packs.every((r) => r.witnesses > 0 && r.unreachable === 0 && r.mismatched === 0 && r.not_checked === (r.simulated ?? 0));
|
|
2694
|
+
const code = sandboxExit(caseExitCode(reading.verdict, onChain.verdict), marker, simulatedOnly);
|
|
2512
2695
|
const nil = (v) => (v === undefined ? null : v);
|
|
2513
2696
|
const data = {
|
|
2514
2697
|
verdict: reading.verdict,
|
|
2515
|
-
title: titleOf(reading.verdict),
|
|
2698
|
+
title: reading.verdict === "verified" && marker !== null ? SANDBOX_TITLE : titleOf(reading.verdict),
|
|
2516
2699
|
reliance: caseReliance(reading.verdict, reading.handoffRecords),
|
|
2517
2700
|
exit_code: code,
|
|
2518
2701
|
summary: renderCaseSummary(reading),
|
|
@@ -2572,13 +2755,23 @@ async function caseCommand(argv, io) {
|
|
|
2572
2755
|
grade: reading.packs[i].grade,
|
|
2573
2756
|
records_withheld: reading.packs[i].recordsWithheld,
|
|
2574
2757
|
declaration_version: reading.packs[i].declarationVersion,
|
|
2758
|
+
...packMarker(bundles[i][1]),
|
|
2575
2759
|
})),
|
|
2576
2760
|
outside: split.outside,
|
|
2577
2761
|
not_regular: split.notRegular,
|
|
2578
2762
|
case_file: caseFile,
|
|
2579
2763
|
on_chain: onChain,
|
|
2580
2764
|
...(cacheRefused.length > 0 ? { witness_cache_not_read: cacheRefused } : {}),
|
|
2765
|
+
...(marker !== null ? { environment_marker: marker } : {}),
|
|
2581
2766
|
};
|
|
2767
|
+
// The runs in the drop (DRAFT, specs/enclave-attestation.md and
|
|
2768
|
+
// run-records.md): an enclave attestation line and a run-account line per
|
|
2769
|
+
// run, beside the witness line. Neither moves `code`, and a drop that holds
|
|
2770
|
+
// no run prints and writes exactly what it did before.
|
|
2771
|
+
const runs = (await readRuns(bundles.map(([p, bundle]) => ({ ref: p.ref, bundle, content: p.content })), { policy, adapters: await loadInstalledAdapters(), rpc: enclaveRpc, witnessed: confirmedStamps })).runs;
|
|
2772
|
+
const withRuns = data;
|
|
2773
|
+
if (runs.length > 0)
|
|
2774
|
+
[withRuns.enclave_attestation, withRuns.run_account] = runLines(runs);
|
|
2582
2775
|
// The files outside every pack (evidence-bundle.md 5.7, A29, A41), always,
|
|
2583
2776
|
// then the files after --files. A pack's logs/ is its side content and is
|
|
2584
2777
|
// never an offered file; only the paths after --files are. Both are matched
|
package/dist/crypto.d.ts
CHANGED
|
@@ -4,6 +4,23 @@ export declare function isHexField(value: unknown, nibbles: number, lower?: bool
|
|
|
4
4
|
export declare function utf8(s: string): Uint8Array;
|
|
5
5
|
export declare function sha256Bytes(bytes: Uint8Array): Promise<Uint8Array>;
|
|
6
6
|
export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
|
|
7
|
+
/** The sentence a refused signature reads in an envelope result. */
|
|
8
|
+
export declare const SIGNATURE_FAILED = "Ed25519 signature verification failed";
|
|
9
|
+
export declare const ED25519_RULE_REASONS: readonly ["the public key is not the canonical encoding of a point", "the public key is a point of small order", "the public key is not in the prime-order subgroup", "R is not the canonical encoding of a point", "R is the identity", "R is a point of small order", "R is not in the prime-order subgroup", "S is not below the group order"];
|
|
10
|
+
/**
|
|
11
|
+
* The clause of the rule that (`publicKey`, `signature`) breaks, or `null`
|
|
12
|
+
* when it breaks none and the equation is what is left to check. Lengths are
|
|
13
|
+
* the caller's (32 and 64 bytes). Never throws.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ed25519RuleReason(signature: Uint8Array, publicKey: Uint8Array): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* `null` iff `signatureHex` is a signature of `message` under `publicKeyHex`
|
|
18
|
+
* by the rule; otherwise the sentence the refusal reads: `SIGNATURE_FAILED`,
|
|
19
|
+
* with `": "` and the clause of the rule it broke when it broke one. A
|
|
20
|
+
* malformed field, a wrong length and an equation that does not hold read
|
|
21
|
+
* the bare sentence. Never throws. The Python twin is `ed25519_refusal`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ed25519Refusal(message: Uint8Array, signatureHex: string, publicKeyHex: string): Promise<string | null>;
|
|
7
24
|
export declare function ed25519Verify(message: Uint8Array, signatureHex: string, publicKeyHex: string): Promise<boolean>;
|
|
8
25
|
export declare function ed25519Sign(message: Uint8Array, privateKeyHex: string): Promise<string>;
|
|
9
26
|
export declare function ed25519PublicKey(privateKeyHex: string): Promise<string>;
|
package/dist/crypto.js
CHANGED
|
@@ -6,7 +6,9 @@ import * as ed from "@noble/ed25519";
|
|
|
6
6
|
// @noble/ed25519 needs SHA-512 to verify. Wire it to WebCrypto rather than
|
|
7
7
|
// pulling in @noble/hashes. globalThis.crypto.subtle exists in every modern
|
|
8
8
|
// browser and in Node >= 19, so the same code path runs in the app and in tests.
|
|
9
|
-
|
|
9
|
+
// noble 3 ships this provider as its default; it is set here so the kernel
|
|
10
|
+
// says what it hashes with rather than inheriting it.
|
|
11
|
+
ed.hashes.sha512Async = async (message) => new Uint8Array(await crypto.subtle.digest("SHA-512", asBufferSource(message)));
|
|
10
12
|
// WebCrypto's digest() wants a BufferSource backed by a (non-shared) ArrayBuffer.
|
|
11
13
|
// Our byte arrays always are; this keeps TS 5.7's stricter Uint8Array<ArrayBufferLike>
|
|
12
14
|
// typing satisfied without a runtime copy.
|
|
@@ -70,31 +72,129 @@ export async function sha256Bytes(bytes) {
|
|
|
70
72
|
export async function sha256Hex(bytes) {
|
|
71
73
|
return bytesToHex(await sha256Bytes(bytes));
|
|
72
74
|
}
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// The Ed25519 rule (envelope-spec §7, A44, proof#230)
|
|
77
|
+
//
|
|
78
|
+
// Strict RFC 8032 (`zip215: false`, a frozen invariant), and more: the public
|
|
79
|
+
// key A and the signature's R must each be the canonical encoding of a point
|
|
80
|
+
// in the prime-order subgroup other than the identity, and S below the group
|
|
81
|
+
// order L. Only then is the equation checked.
|
|
82
|
+
//
|
|
83
|
+
// Why more than `zip215: false`: noble checks the equation with the cofactor,
|
|
84
|
+
// 8·S·B = 8·R + 8·k·A, and Go's crypto/ed25519 and libsodium without it, so on
|
|
85
|
+
// their own they disagree when A or R has a small-order component. noble 2.3
|
|
86
|
+
// also could not encode an identity R and hashed nothing for k, so it accepted
|
|
87
|
+
// one signature from a key holder over every message. With A and R in the
|
|
88
|
+
// prime-order subgroup the two equations are the same equation, and every
|
|
89
|
+
// kernel accepts exactly the same signatures.
|
|
90
|
+
//
|
|
91
|
+
// Each clause is checked here, in this order, and named in these words: the
|
|
92
|
+
// Python twin is `ed25519_rule_reason` in `src/sanning_proof/ed25519_rule.py`
|
|
93
|
+
// and the Go kernel's `verifyEd25519Strict` in sanning-io/agent `pkg/proof`.
|
|
94
|
+
// Several clauses are ones noble 3 refuses on its own; they are checked here
|
|
95
|
+
// anyway so the refusal names the clause, which a library's `false` cannot.
|
|
96
|
+
/** The sentence a refused signature reads in an envelope result. */
|
|
97
|
+
export const SIGNATURE_FAILED = "Ed25519 signature verification failed";
|
|
98
|
+
export const ED25519_RULE_REASONS = [
|
|
99
|
+
"the public key is not the canonical encoding of a point",
|
|
100
|
+
"the public key is a point of small order",
|
|
101
|
+
"the public key is not in the prime-order subgroup",
|
|
102
|
+
"R is not the canonical encoding of a point",
|
|
103
|
+
"R is the identity",
|
|
104
|
+
"R is a point of small order",
|
|
105
|
+
"R is not in the prime-order subgroup",
|
|
106
|
+
"S is not below the group order",
|
|
107
|
+
];
|
|
108
|
+
const [KEY_NOT_CANONICAL, KEY_SMALL_ORDER, KEY_NOT_IN_SUBGROUP, R_NOT_CANONICAL, R_IDENTITY, R_SMALL_ORDER, R_NOT_IN_SUBGROUP, S_NOT_BELOW_L,] = ED25519_RULE_REASONS;
|
|
109
|
+
const ED25519_L = 2n ** 252n + 27742317777372353535851937790883648493n;
|
|
110
|
+
function bytesToNumberLE(bytes) {
|
|
111
|
+
let v = 0n;
|
|
112
|
+
for (let i = bytes.length - 1; i >= 0; i--)
|
|
113
|
+
v = (v << 8n) | BigInt(bytes[i]);
|
|
114
|
+
return v;
|
|
115
|
+
}
|
|
116
|
+
// `Point.fromBytes(..., false)` is RFC 8032 decoding with its two strict
|
|
117
|
+
// branches: it throws on y at or above p and on x zero with the sign bit set,
|
|
118
|
+
// the two non-canonical encodings of a point, and on bytes that are no point.
|
|
119
|
+
function decodeStrict(bytes) {
|
|
120
|
+
try {
|
|
121
|
+
return ed.Point.fromBytes(bytes, false);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The clause of the rule that (`publicKey`, `signature`) breaks, or `null`
|
|
129
|
+
* when it breaks none and the equation is what is left to check. Lengths are
|
|
130
|
+
* the caller's (32 and 64 bytes). Never throws.
|
|
131
|
+
*/
|
|
132
|
+
export function ed25519RuleReason(signature, publicKey) {
|
|
133
|
+
const A = decodeStrict(publicKey);
|
|
134
|
+
if (A === null)
|
|
135
|
+
return KEY_NOT_CANONICAL;
|
|
136
|
+
if (A.isSmallOrder())
|
|
137
|
+
return KEY_SMALL_ORDER;
|
|
138
|
+
if (!A.isTorsionFree())
|
|
139
|
+
return KEY_NOT_IN_SUBGROUP;
|
|
140
|
+
const R = decodeStrict(signature.subarray(0, 32));
|
|
141
|
+
if (R === null)
|
|
142
|
+
return R_NOT_CANONICAL;
|
|
143
|
+
if (R.is0())
|
|
144
|
+
return R_IDENTITY;
|
|
145
|
+
if (R.isSmallOrder())
|
|
146
|
+
return R_SMALL_ORDER;
|
|
147
|
+
if (!R.isTorsionFree())
|
|
148
|
+
return R_NOT_IN_SUBGROUP;
|
|
149
|
+
if (bytesToNumberLE(signature.subarray(32, 64)) >= ED25519_L)
|
|
150
|
+
return S_NOT_BELOW_L;
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `null` iff `signatureHex` is a signature of `message` under `publicKeyHex`
|
|
155
|
+
* by the rule; otherwise the sentence the refusal reads: `SIGNATURE_FAILED`,
|
|
156
|
+
* with `": "` and the clause of the rule it broke when it broke one. A
|
|
157
|
+
* malformed field, a wrong length and an equation that does not hold read
|
|
158
|
+
* the bare sentence. Never throws. The Python twin is `ed25519_refusal`.
|
|
159
|
+
*/
|
|
160
|
+
export async function ed25519Refusal(message, signatureHex, publicKeyHex) {
|
|
161
|
+
let signature;
|
|
162
|
+
let publicKey;
|
|
163
|
+
try {
|
|
164
|
+
signature = hexToBytes(signatureHex);
|
|
165
|
+
publicKey = hexToBytes(publicKeyHex);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return SIGNATURE_FAILED;
|
|
169
|
+
}
|
|
170
|
+
if (signature.length !== 64 || publicKey.length !== 32)
|
|
171
|
+
return SIGNATURE_FAILED;
|
|
172
|
+
const reason = ed25519RuleReason(signature, publicKey);
|
|
173
|
+
if (reason !== null)
|
|
174
|
+
return `${SIGNATURE_FAILED}: ${reason}`;
|
|
175
|
+
try {
|
|
176
|
+
return (await ed.verifyAsync(signature, message, publicKey, { zip215: false })) ? null : SIGNATURE_FAILED;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return SIGNATURE_FAILED;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Verify an Ed25519 signature by the rule above. Inputs are hex (signature,
|
|
183
|
+
// public key) as they appear in the envelope; message is raw bytes. Never
|
|
184
|
+
// throws — a malformed signature or key is "not verified," not an exception,
|
|
185
|
+
// so a hostile envelope can't crash the checker.
|
|
77
186
|
// Arguments in the Python twin's order, `ed25519_verify(message,
|
|
78
187
|
// signature_hex, public_key_hex)` (P-03, 0.13.0). The message came second
|
|
79
188
|
// until 0.13.0; a caller that still passes the signature first gets a compile
|
|
80
189
|
// error, because a string is not a Uint8Array.
|
|
81
190
|
export async function ed25519Verify(message, signatureHex, publicKeyHex) {
|
|
82
|
-
|
|
83
|
-
// zip215:false → strict RFC-8032 verification, matching the Go agent's
|
|
84
|
-
// crypto/ed25519 exactly (noble defaults to the more lenient zip215:true).
|
|
85
|
-
return await ed.verifyAsync(hexToBytes(signatureHex), message, hexToBytes(publicKeyHex), {
|
|
86
|
-
zip215: false,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
191
|
+
return (await ed25519Refusal(message, signatureHex, publicKeyHex)) === null;
|
|
92
192
|
}
|
|
93
193
|
// Ed25519 SIGN the raw `message` bytes with a 32-byte private-key seed (lowercase
|
|
94
194
|
// hex). Returns the signature as lowercase hex. The signing counterpart to
|
|
95
195
|
// ed25519Verify — the ONE signing primitive the composer (`proof export`,
|
|
96
196
|
// evidence-export.md §5) needs to Ed25519-sign the export wrapper with the
|
|
97
|
-
// exporter's key ("anyone can export", P-4). The @noble sha512Async
|
|
197
|
+
// exporter's key ("anyone can export", P-4). The @noble sha512Async provider wired at
|
|
98
198
|
// module load serves signing and verification alike, so this stays WebCrypto-only.
|
|
99
199
|
export async function ed25519Sign(message, privateKeyHex) {
|
|
100
200
|
return bytesToHex(await ed.signAsync(message, hexToBytes(privateKeyHex)));
|