@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/src/verifier.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import canonicalize from "canonicalize";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { SIGNATURE_FAILED, ed25519Refusal, isHexField, sha256Hex, utf8 } from "./crypto.js";
|
|
13
13
|
import type { ContentRole, Envelope, VerificationResult, VerifyOptions } from "./types.js";
|
|
14
14
|
|
|
15
15
|
// Fail-closed accepted-profile registry (envelope-spec §2): exactly the
|
|
@@ -157,6 +157,18 @@ export function exceedsDepth(value: unknown, max: number): boolean {
|
|
|
157
157
|
return boundedInput(value, max) === OVER_DEEP;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* The bytes an envelope's inline `payload` commits to: `JCS(payload)`. The ONE
|
|
162
|
+
* place this is spelled: `verifyEnvelope` binds `payload_hash` against these
|
|
163
|
+
* bytes, and the chain links rule 11 reads (`readChainLinks`) read an inline
|
|
164
|
+
* record through them, so the binding rule and the linkage rule cannot
|
|
165
|
+
* disagree about which bytes a record is. Throws where the payload cannot be
|
|
166
|
+
* canonicalized. The twin is Python's `inline_payload_bytes`.
|
|
167
|
+
*/
|
|
168
|
+
export function inlinePayloadBytes(envelope: { payload: unknown }): Uint8Array {
|
|
169
|
+
return utf8(jcs(envelope.payload));
|
|
170
|
+
}
|
|
171
|
+
|
|
160
172
|
// RFC 8785 (JCS) canonicalization. The `canonicalize` package is the reference
|
|
161
173
|
// JS implementation; correctness is pinned by the conformance vectors.
|
|
162
174
|
//
|
|
@@ -251,6 +263,87 @@ export function jcsParse(bytes: Uint8Array): unknown {
|
|
|
251
263
|
return value;
|
|
252
264
|
}
|
|
253
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Whether the bytes nest arrays and objects deeper than `max`, read from the
|
|
268
|
+
* bytes before any parser sees them: the greatest number of `[` and `{` open
|
|
269
|
+
* at once outside strings. A `"` opens a string and the next `"` no `\`
|
|
270
|
+
* escapes closes it; a `]` or `}` outside a string closes one open level, if
|
|
271
|
+
* one is open. Defined for every byte string, JSON or not. The Python twin is
|
|
272
|
+
* `exceeds_text_depth` in `canonicalize.py`.
|
|
273
|
+
*
|
|
274
|
+
* On the bytes because a parser's own limit is its host's: CPython 3.10
|
|
275
|
+
* stops near 1,000 levels, `JSON.parse` and CPython 3.13 read far deeper, Go's
|
|
276
|
+
* decoder stops at 10,000 (enclave-attestation section 2.1).
|
|
277
|
+
*/
|
|
278
|
+
export function exceedsTextDepth(bytes: Uint8Array, max: number): boolean {
|
|
279
|
+
let brackets = 0;
|
|
280
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
281
|
+
const b = bytes[i]!;
|
|
282
|
+
if (b === 0x5b || b === 0x7b) brackets++;
|
|
283
|
+
}
|
|
284
|
+
if (brackets <= max) return false; // it cannot open more levels than it has brackets
|
|
285
|
+
let depth = 0;
|
|
286
|
+
let inString = false;
|
|
287
|
+
let escaped = false;
|
|
288
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
289
|
+
const b = bytes[i]!;
|
|
290
|
+
if (inString) {
|
|
291
|
+
if (escaped) escaped = false;
|
|
292
|
+
else if (b === 0x5c) escaped = true;
|
|
293
|
+
else if (b === 0x22) inString = false;
|
|
294
|
+
} else if (b === 0x22) {
|
|
295
|
+
inString = true;
|
|
296
|
+
} else if (b === 0x5b || b === 0x7b) {
|
|
297
|
+
if (++depth > max) return true;
|
|
298
|
+
} else if ((b === 0x5d || b === 0x7d) && depth > 0) {
|
|
299
|
+
depth--;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Whether a parsed value holds a number no IEEE 754 double holds, at any
|
|
307
|
+
* depth. `JSON.parse` reads `1e400`, and an integer of 400 digits, as
|
|
308
|
+
* `Infinity`. Iterative: the value is a producer's. The Python twin is
|
|
309
|
+
* `has_non_finite_number`.
|
|
310
|
+
*/
|
|
311
|
+
export function hasNonFiniteNumber(value: unknown): boolean {
|
|
312
|
+
const stack: unknown[] = [value];
|
|
313
|
+
while (stack.length > 0) {
|
|
314
|
+
const node = stack.pop();
|
|
315
|
+
if (typeof node === "number") {
|
|
316
|
+
if (!Number.isFinite(node)) return true;
|
|
317
|
+
} else if (Array.isArray(node)) {
|
|
318
|
+
for (const child of node) stack.push(child);
|
|
319
|
+
} else if (typeof node === "object" && node !== null) {
|
|
320
|
+
for (const child of Object.values(node)) stack.push(child);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The deepest a JSON text an enclave reader parses may nest, and the depth
|
|
328
|
+
* every kernel canonicalizes to (`MAX_CANONICAL_DEPTH`).
|
|
329
|
+
*/
|
|
330
|
+
export const MAX_READ_DEPTH = MAX_CANONICAL_DEPTH;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* `jcsParse`, within the reader limits of enclave-attestation section 2.1:
|
|
334
|
+
* the bytes nest at most `MAX_READ_DEPTH` levels, checked on the bytes first,
|
|
335
|
+
* and every number in the value is a finite double. Throws past either limit,
|
|
336
|
+
* and wherever `jcsParse` throws. The Python twin is `jcs_loads_within_limits`.
|
|
337
|
+
*/
|
|
338
|
+
export function jcsParseWithinLimits(bytes: Uint8Array): unknown {
|
|
339
|
+
if (exceedsTextDepth(bytes, MAX_READ_DEPTH)) {
|
|
340
|
+
throw new Error(`jcsParseWithinLimits: nests deeper than ${MAX_READ_DEPTH} levels`);
|
|
341
|
+
}
|
|
342
|
+
const value = jcsParse(bytes);
|
|
343
|
+
if (hasNonFiniteNumber(value)) throw new Error("jcsParseWithinLimits: holds a number no double holds");
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
|
|
254
347
|
/**
|
|
255
348
|
* A JSON object that names one member twice (I-JSON, RFC 7493 section 2.3).
|
|
256
349
|
* The Python twin is `DuplicateKey` in `canonicalize.py`, and the message is
|
|
@@ -809,7 +902,7 @@ async function verifyEnvelopeChecked(
|
|
|
809
902
|
const checks: boolean[] = [];
|
|
810
903
|
if ("payload" in env && env.payload !== undefined) {
|
|
811
904
|
try {
|
|
812
|
-
const recomputed = await sha256Hex(
|
|
905
|
+
const recomputed = await sha256Hex(inlinePayloadBytes(env as { payload: unknown }));
|
|
813
906
|
checks.push(recomputed === env.payload_hash);
|
|
814
907
|
} catch {
|
|
815
908
|
checks.push(false);
|
|
@@ -835,16 +928,20 @@ async function verifyEnvelopeChecked(
|
|
|
835
928
|
// invalidating the primary signature; the field is reserved/default-absent.
|
|
836
929
|
// The corpus has no co-signed vectors, so this strip is pinned by an explicit
|
|
837
930
|
// unit test rather than by conformance.
|
|
931
|
+
// The refusal names the clause of the Ed25519 rule a signature breaks
|
|
932
|
+
// (envelope-spec §7, A44), so a forged shape reads differently from a
|
|
933
|
+
// corrupted byte; an equation that does not hold reads the bare sentence.
|
|
838
934
|
let signatureOk = false;
|
|
839
935
|
try {
|
|
840
936
|
const { signature: _signature, co_signatures: _coSignatures, ...envelopeForSig } = env;
|
|
841
|
-
|
|
842
|
-
|
|
937
|
+
const refusal = await ed25519Refusal(utf8(jcs(envelopeForSig)), env.signature, env.public_key);
|
|
938
|
+
signatureOk = refusal === null;
|
|
939
|
+
if (refusal !== null) errors.push(refusal);
|
|
843
940
|
} catch {
|
|
844
941
|
// The sentence the Python kernel writes for the same failure. What was
|
|
845
942
|
// thrown is the host's wording (`jcs: input nesting exceeds ...`), and a
|
|
846
943
|
// public result carries the kernel's own words only (proof#150).
|
|
847
|
-
errors.push(
|
|
944
|
+
errors.push(SIGNATURE_FAILED);
|
|
848
945
|
}
|
|
849
946
|
|
|
850
947
|
// Check 3 (optional) — Content Bind: the user's bytes match a hash this
|
package/src/witness-lookup.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// `witness-cache.json` read as part of the pack rather than as a side file a
|
|
21
21
|
// caller must remember to also pass in.
|
|
22
22
|
|
|
23
|
-
import { verifyEvidenceBundle } from "./evidence.js";
|
|
23
|
+
import { SIMULATED_WITNESS_KIND, verifyEvidenceBundle } from "./evidence.js";
|
|
24
24
|
import type { WitnessLine, WitnessState } from "./case.js";
|
|
25
25
|
import { WITNESS_NOT_LOOKED_UP, renderWitnessLine } from "./case.js";
|
|
26
26
|
import {
|
|
@@ -44,6 +44,12 @@ export interface OnChainRow {
|
|
|
44
44
|
mismatched: number;
|
|
45
45
|
unreachable: number;
|
|
46
46
|
not_checked: number;
|
|
47
|
+
/**
|
|
48
|
+
* How many of the `not_checked` witnesses are simulated: a sandbox plane's
|
|
49
|
+
* stand-in for a chain, never looked up. Present only where there
|
|
50
|
+
* is one, so a production pack's row is unchanged.
|
|
51
|
+
*/
|
|
52
|
+
simulated?: number;
|
|
47
53
|
/** Present only where the pack caches a witness time (5.6.6, v1.25, A30). */
|
|
48
54
|
witness_times?: WitnessTimeRow[];
|
|
49
55
|
}
|
|
@@ -97,6 +103,12 @@ export async function onChainHalf(
|
|
|
97
103
|
packs: readonly (readonly [string, unknown, readonly CachedWitnessTime[]])[],
|
|
98
104
|
gateways: readonly string[],
|
|
99
105
|
options: WitnessLookupOptions = {},
|
|
106
|
+
/**
|
|
107
|
+
* Where given, receives for each pack the `stamp_ref` of every stamp whose
|
|
108
|
+
* witness this lookup confirmed: the run reading evaluates an enclave token
|
|
109
|
+
* at a stamp's interval end only there.
|
|
110
|
+
*/
|
|
111
|
+
witnessed?: string[][],
|
|
100
112
|
): Promise<OnChainHalf> {
|
|
101
113
|
if (gateways.length === 0) return { checked: false, gateways: [], packs: [], verdict: "not_checked" };
|
|
102
114
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -113,6 +125,9 @@ export async function onChainHalf(
|
|
|
113
125
|
unreachable: 0,
|
|
114
126
|
not_checked: 0,
|
|
115
127
|
};
|
|
128
|
+
witnessed?.push(
|
|
129
|
+
(result.stamps ?? []).filter((st) => st.ok && st.onChain?.rollup === "confirm").map((st) => st.stampRef),
|
|
130
|
+
);
|
|
116
131
|
for (const w of witnesses) {
|
|
117
132
|
const rollup = w.onChain?.rollup ?? null;
|
|
118
133
|
if (rollup === "confirm") row.confirmed++;
|
|
@@ -120,6 +135,8 @@ export async function onChainHalf(
|
|
|
120
135
|
else if (rollup === "unreachable") row.unreachable++;
|
|
121
136
|
else row.not_checked++;
|
|
122
137
|
}
|
|
138
|
+
const simulated = witnesses.filter((w) => w.anchorRef?.kind === SIMULATED_WITNESS_KIND).length;
|
|
139
|
+
if (simulated > 0) row.simulated = simulated;
|
|
123
140
|
// The witness times the pack's witness-cache.json gives, looked up where
|
|
124
141
|
// it gives any (5.6.6, v1.25, A30). A pack with no cache gets no key and
|
|
125
142
|
// no request.
|
package/vectors/README.md
CHANGED
|
@@ -214,3 +214,40 @@ tree. One splitting row disagrees with the page on purpose: `record-file-names`
|
|
|
214
214
|
a record named `__proto__`, which the page's `assemble` loses because it keys
|
|
215
215
|
disclosed content by a plain object. A page that vendors this table fails that
|
|
216
216
|
row until it keys content by a `Map`.
|
|
217
|
+
|
|
218
|
+
## `sandbox-evidence.json`
|
|
219
|
+
|
|
220
|
+
What both commands print and exit for a pack whose records are signed
|
|
221
|
+
`environment: "dev"` (envelope-spec §2, where a verifier MAY surface the value
|
|
222
|
+
as `environment_marker`), and what they may claim about a witness a sandbox
|
|
223
|
+
plane simulated. Implemented by [`../src/environment.ts`](../src/environment.ts),
|
|
224
|
+
the `sandboxExit` of [`../src/exit-codes.ts`](../src/exit-codes.ts) and the two
|
|
225
|
+
commands in [`../src/cli.ts`](../src/cli.ts), and asserted against this table by
|
|
226
|
+
[`../test/sandbox-evidence.test.ts`](../test/sandbox-evidence.test.ts) **and by
|
|
227
|
+
the Python kernel against the same bytes**, in `tests/test_sandbox_evidence.py`,
|
|
228
|
+
and through both commands as child processes by `cross-kernel/run_sandbox.sh`.
|
|
229
|
+
Every expectation is written by hand in
|
|
230
|
+
`tools/gen-vectors/gen_sandbox_evidence_table.py`, which writes this file. Two
|
|
231
|
+
of the cases are the real sandbox pack in
|
|
232
|
+
[`../test/fixtures/sandbox/`](../test/fixtures/sandbox/), with its provenance.
|
|
233
|
+
|
|
234
|
+
**The one thing a consumer must not do with it: print a `dev` pack as
|
|
235
|
+
verified.** The verdict word stays what the checks established; the `verify`
|
|
236
|
+
headline is `SANDBOX` and the `case` title `Sandbox`, the line
|
|
237
|
+
`Sandbox evidence (environment dev): not production evidence` is printed
|
|
238
|
+
wherever the marker is present, and the exit code is 5 where it would be 0, or
|
|
239
|
+
where every record is `dev` and only simulated witnesses are unchecked. A
|
|
240
|
+
failure is still 1, and a production or mixed pack with a simulated witness is
|
|
241
|
+
still 3. And a `simulated` witness, a stamp's or a checkpoint's, is never
|
|
242
|
+
fetched and never counted as checked on a chain. Each case's
|
|
243
|
+
`simulatedSentence` says whether `verify` says "a simulated witness is not on a
|
|
244
|
+
chain, so nothing was looked up": only a run with `--gateways` that counted a
|
|
245
|
+
simulated witness and was not checked on a chain. Both tests assert the
|
|
246
|
+
TypeScript verdict line and the Python `Gateways` line exactly.
|
|
247
|
+
|
|
248
|
+
Two more members: `stoppedEarly` holds packs whose verification stops before
|
|
249
|
+
any witness is read, each of which must print and write the same with
|
|
250
|
+
`--gateways`, reachable or not, as offline (the Python `Gateways` line names
|
|
251
|
+
the gateways passed); `agentProof` holds an agent proof signed `dev`, which the
|
|
252
|
+
TypeScript `verify` prints as `SANDBOX` with exit 5 and the Python one, which
|
|
253
|
+
implements no agent-proof verifier, refuses with exit 2.
|