@sanning/proof 0.11.1 → 0.13.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 +43 -8
- package/dist/agent-proof.d.ts +2 -2
- package/dist/agent-proof.js +4 -4
- package/dist/case-file.d.ts +126 -0
- package/dist/case-file.js +349 -0
- package/dist/case.d.ts +199 -43
- package/dist/case.js +522 -146
- package/dist/cli.d.ts +132 -1
- package/dist/cli.js +1140 -96
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.js +5 -1
- package/dist/declaration.d.ts +58 -52
- package/dist/declaration.js +256 -33
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.js +134 -19
- package/dist/exit-codes.d.ts +17 -0
- package/dist/exit-codes.js +50 -0
- package/dist/grading.d.ts +28 -13
- package/dist/grading.js +90 -31
- package/dist/handoff.d.ts +16 -1
- package/dist/handoff.js +91 -16
- package/dist/index.d.ts +10 -7
- package/dist/index.js +15 -7
- package/dist/message-parts.d.ts +11 -9
- package/dist/message-parts.js +11 -9
- package/dist/types.d.ts +1 -0
- package/dist/verifier.d.ts +9 -2
- package/dist/verifier.js +28 -12
- package/dist/zip-reader.d.ts +50 -0
- package/dist/zip-reader.js +220 -0
- package/package.json +2 -2
- package/src/agent-proof.ts +4 -4
- package/src/case-file.ts +419 -0
- package/src/case.ts +651 -156
- package/src/cli.ts +1325 -100
- package/src/crypto.ts +5 -1
- package/src/declaration.ts +248 -52
- package/src/evidence.ts +142 -21
- package/src/exit-codes.ts +54 -0
- package/src/grading.ts +109 -52
- package/src/handoff.ts +108 -17
- package/src/index.ts +60 -3
- package/src/message-parts.ts +11 -9
- package/src/types.ts +5 -0
- package/src/verifier.ts +35 -13
- package/src/zip-reader.ts +229 -0
- package/vectors/README.md +56 -3
- package/vectors/case-file-index.json +668 -0
- package/vectors/case-split.json +1102 -0
- package/vectors/pack-grading.json +68 -4
- package/vectors/stamp-on-chain.json +3 -3
package/dist/verifier.js
CHANGED
|
@@ -19,7 +19,7 @@ import { ed25519Verify, isHexField, sha256Hex, utf8 } from "./crypto.js";
|
|
|
19
19
|
// profile-specific dialect (no underscore strip, that is mlflow-only).
|
|
20
20
|
//
|
|
21
21
|
// Exported for parity with the sibling kernels' registries — Python
|
|
22
|
-
// `
|
|
22
|
+
// `ACCEPTED_SPEC_MAJORS`, Go `SupportedSpecMajor` — so the accept-set is
|
|
23
23
|
// assertable rather than inferable: test/accept-set.test.ts holds it to the
|
|
24
24
|
// ratified §4/§5 set transcribed in cross-kernel/accept-set.json, and the
|
|
25
25
|
// cross-kernel gate probes one signed envelope per identifier. This entry list
|
|
@@ -217,6 +217,15 @@ const JCS_BOM = "";
|
|
|
217
217
|
* Throws for anything that is not conformant; every call site treats a throw as
|
|
218
218
|
* "this reader cannot read these bytes" and names the check that failed.
|
|
219
219
|
*/
|
|
220
|
+
/**
|
|
221
|
+
* Bytes as strict UTF-8, a leading byte-order mark KEPT as a character.
|
|
222
|
+
* `utf8_strict` in the Python twin's `canonicalize.py` is the same rule, and
|
|
223
|
+
* `jcsParse` below performs the identical decode. Throws on bytes that are not
|
|
224
|
+
* UTF-8.
|
|
225
|
+
*/
|
|
226
|
+
export function utf8Strict(bytes) {
|
|
227
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
228
|
+
}
|
|
220
229
|
export function jcsParse(bytes) {
|
|
221
230
|
const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
222
231
|
if (text.startsWith(JCS_BOM))
|
|
@@ -299,10 +308,14 @@ function hasLoneSurrogate(s) {
|
|
|
299
308
|
// `<major>.<minor>` where minor is `[0-9]+`. A non-numeric minor suffix is
|
|
300
309
|
// malformed, not a tolerated future version — matching the Python kernel's
|
|
301
310
|
// 0.1.1 semantics (the TS/Go fail-open lenience was closed deliberately).
|
|
302
|
-
|
|
311
|
+
//
|
|
312
|
+
// `accepted` NARROWS the registry and never widens it, as the Python twin's
|
|
313
|
+
// `spec_version_supported(spec_version, accepted)` does (P-10, 0.13.0).
|
|
314
|
+
export function specVersionSupported(specVersion, accepted) {
|
|
303
315
|
if (typeof specVersion !== "string" || specVersion === "")
|
|
304
316
|
return false;
|
|
305
|
-
|
|
317
|
+
const registry = accepted === undefined ? ACCEPTED_SPEC_MAJORS : ACCEPTED_SPEC_MAJORS.filter((m) => accepted.includes(m));
|
|
318
|
+
return registry.some((m) => {
|
|
306
319
|
if (specVersion === m)
|
|
307
320
|
return true;
|
|
308
321
|
if (!specVersion.startsWith(`${m}.`))
|
|
@@ -444,18 +457,21 @@ export function contentHashes(env) {
|
|
|
444
457
|
// signature, so stripping it to fake "external" — or injecting one to fake
|
|
445
458
|
// "inline" — breaks the signature).
|
|
446
459
|
//
|
|
447
|
-
// The second argument
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
460
|
+
// The second argument is a VerifyOptions object ({ payloadBytes,
|
|
461
|
+
// expectedContentHash, acceptedSpecMajors }). Until 0.13.0 it could also be a
|
|
462
|
+
// bare `expectedContentHash` string; that overload is gone (P-10), so the two
|
|
463
|
+
// kernels take one options shape: write `{ expectedContentHash: hash }`.
|
|
464
|
+
export async function verifyEnvelope(env, options) {
|
|
465
|
+
if (typeof options === "string") {
|
|
466
|
+
throw new TypeError("verifyEnvelope: pass { expectedContentHash } rather than a bare string (0.13.0)");
|
|
467
|
+
}
|
|
468
|
+
const opts = options ?? {};
|
|
453
469
|
// REFUSE an unknown option key rather than ignore it. `expected_content_hash`
|
|
454
470
|
// — the Python kernel's spelling — previously fell through as `undefined`, so
|
|
455
471
|
// the content bind silently never ran and the caller got a clean pass with no
|
|
456
472
|
// errors at all. A check that quietly does not happen is worse than one that
|
|
457
473
|
// fails: the caller believes they verified an artifact they never checked.
|
|
458
|
-
const KNOWN_VERIFY_OPTIONS = new Set(["payloadBytes", "expectedContentHash"]);
|
|
474
|
+
const KNOWN_VERIFY_OPTIONS = new Set(["payloadBytes", "expectedContentHash", "acceptedSpecMajors"]);
|
|
459
475
|
const unknownOptions = Object.keys(opts).filter((k) => !KNOWN_VERIFY_OPTIONS.has(k));
|
|
460
476
|
if (unknownOptions.length > 0) {
|
|
461
477
|
throw new TypeError(`verifyEnvelope: unknown option(s) ${unknownOptions.join(", ")}. ` +
|
|
@@ -528,7 +544,7 @@ async function verifyEnvelopeChecked(env, opts) {
|
|
|
528
544
|
errors: ["envelope is not a JSON object"],
|
|
529
545
|
};
|
|
530
546
|
}
|
|
531
|
-
const specVersionOk = specVersionSupported(env.spec_version);
|
|
547
|
+
const specVersionOk = specVersionSupported(env.spec_version, opts.acceptedSpecMajors);
|
|
532
548
|
if (!specVersionOk)
|
|
533
549
|
errors.push(`unsupported spec_version: ${JSON.stringify(env.spec_version)}`);
|
|
534
550
|
// Skeleton field format (envelope-spec §2, proof#95): a present `signed_at`
|
|
@@ -598,7 +614,7 @@ async function verifyEnvelopeChecked(env, opts) {
|
|
|
598
614
|
let signatureOk = false;
|
|
599
615
|
try {
|
|
600
616
|
const { signature: _signature, co_signatures: _coSignatures, ...envelopeForSig } = env;
|
|
601
|
-
signatureOk = await ed25519Verify(
|
|
617
|
+
signatureOk = await ed25519Verify(utf8(jcs(envelopeForSig)), env.signature, env.public_key);
|
|
602
618
|
if (!signatureOk)
|
|
603
619
|
errors.push("Ed25519 signature verification failed");
|
|
604
620
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one ZIP reader `case` uses, for a case file named on the command line.
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript half of `src/sanning_proof/zip_reader.py`. Node has no ZIP
|
|
5
|
+
* reader, and Python's `zipfile` is not used, so the two kernels read one
|
|
6
|
+
* archive by one rule rather than by two libraries that disagree on the edges:
|
|
7
|
+
* a central directory that contradicts a local header, a name that is not
|
|
8
|
+
* UTF-8, data after the end of a deflate stream. Where either library would
|
|
9
|
+
* have made a choice, this module refuses instead.
|
|
10
|
+
*
|
|
11
|
+
* What it reads (evidence-bundle.md 5.6.1): the central directory, and each
|
|
12
|
+
* entry's data through its local header. Stored and deflate only. What it
|
|
13
|
+
* REFUSES, as a whole archive: no end-of-central-directory record, ZIP64, more
|
|
14
|
+
* than one disk, an entry the central directory places outside the archive, a
|
|
15
|
+
* local header that does not match its central entry, a size or CRC-32 that
|
|
16
|
+
* does not match the data, two entries with one name, a name that is not
|
|
17
|
+
* UTF-8, more than MAX_ZIP_ENTRIES entries or MAX_ZIP_BYTES bytes in all, and
|
|
18
|
+
* a name nested deeper than MAX_ZIP_DEPTH folders. What it REPORTS and never
|
|
19
|
+
* reads, entry by entry: an encrypted entry, a symbolic link, a method other
|
|
20
|
+
* than stored or deflate, and a name that is absolute, holds a backslash or a
|
|
21
|
+
* NUL, or has an empty, `.` or `..` segment.
|
|
22
|
+
*
|
|
23
|
+
* Pure: bytes in, entries out. The caller turns them into the tree `case`
|
|
24
|
+
* splits.
|
|
25
|
+
*/
|
|
26
|
+
/** More entries than this refuse the archive: the walk's own bound on names. */
|
|
27
|
+
export declare const MAX_ZIP_ENTRIES = 50000;
|
|
28
|
+
/** More uncompressed bytes than this, across every entry, refuse the archive. */
|
|
29
|
+
export declare const MAX_ZIP_BYTES: number;
|
|
30
|
+
/** A name nested deeper than this many folders refuses the archive. */
|
|
31
|
+
export declare const MAX_ZIP_DEPTH = 32;
|
|
32
|
+
/** The archive cannot be read as a case file. `message` is the whole report, identical in both kernels. */
|
|
33
|
+
export declare class ZipReadError extends Error {
|
|
34
|
+
constructor(message: string);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One file entry. `refused` is why the entry is reported and not read, or
|
|
38
|
+
* `null`; `data` is its bytes where it was read.
|
|
39
|
+
*/
|
|
40
|
+
export interface ZipEntry {
|
|
41
|
+
name: string;
|
|
42
|
+
refused: string | null;
|
|
43
|
+
data: Uint8Array | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every FILE entry of a ZIP archive, in central-directory order. A directory
|
|
47
|
+
* entry (a name ending in `/`) is not a file and is not returned. Throws
|
|
48
|
+
* `ZipReadError`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readZip(data: Uint8Array): ZipEntry[];
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one ZIP reader `case` uses, for a case file named on the command line.
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript half of `src/sanning_proof/zip_reader.py`. Node has no ZIP
|
|
5
|
+
* reader, and Python's `zipfile` is not used, so the two kernels read one
|
|
6
|
+
* archive by one rule rather than by two libraries that disagree on the edges:
|
|
7
|
+
* a central directory that contradicts a local header, a name that is not
|
|
8
|
+
* UTF-8, data after the end of a deflate stream. Where either library would
|
|
9
|
+
* have made a choice, this module refuses instead.
|
|
10
|
+
*
|
|
11
|
+
* What it reads (evidence-bundle.md 5.6.1): the central directory, and each
|
|
12
|
+
* entry's data through its local header. Stored and deflate only. What it
|
|
13
|
+
* REFUSES, as a whole archive: no end-of-central-directory record, ZIP64, more
|
|
14
|
+
* than one disk, an entry the central directory places outside the archive, a
|
|
15
|
+
* local header that does not match its central entry, a size or CRC-32 that
|
|
16
|
+
* does not match the data, two entries with one name, a name that is not
|
|
17
|
+
* UTF-8, more than MAX_ZIP_ENTRIES entries or MAX_ZIP_BYTES bytes in all, and
|
|
18
|
+
* a name nested deeper than MAX_ZIP_DEPTH folders. What it REPORTS and never
|
|
19
|
+
* reads, entry by entry: an encrypted entry, a symbolic link, a method other
|
|
20
|
+
* than stored or deflate, and a name that is absolute, holds a backslash or a
|
|
21
|
+
* NUL, or has an empty, `.` or `..` segment.
|
|
22
|
+
*
|
|
23
|
+
* Pure: bytes in, entries out. The caller turns them into the tree `case`
|
|
24
|
+
* splits.
|
|
25
|
+
*/
|
|
26
|
+
import { inflateRawSync } from "node:zlib";
|
|
27
|
+
import { utf8Strict } from "./verifier.js";
|
|
28
|
+
/** More entries than this refuse the archive: the walk's own bound on names. */
|
|
29
|
+
export const MAX_ZIP_ENTRIES = 50_000;
|
|
30
|
+
/** More uncompressed bytes than this, across every entry, refuse the archive. */
|
|
31
|
+
export const MAX_ZIP_BYTES = 2 ** 30;
|
|
32
|
+
/** A name nested deeper than this many folders refuses the archive. */
|
|
33
|
+
export const MAX_ZIP_DEPTH = 32;
|
|
34
|
+
const EOCD = 0x06054b50;
|
|
35
|
+
const CENTRAL = 0x02014b50;
|
|
36
|
+
const LOCAL = 0x04034b50;
|
|
37
|
+
const S_IFMT = 0o170000;
|
|
38
|
+
const S_IFLNK = 0o120000;
|
|
39
|
+
const UNIX = 3;
|
|
40
|
+
/** The archive cannot be read as a case file. `message` is the whole report, identical in both kernels. */
|
|
41
|
+
export class ZipReadError extends Error {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "ZipReadError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const CRC_TABLE = (() => {
|
|
48
|
+
const table = new Uint32Array(256);
|
|
49
|
+
for (let n = 0; n < 256; n++) {
|
|
50
|
+
let c = n;
|
|
51
|
+
for (let k = 0; k < 8; k++)
|
|
52
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
53
|
+
table[n] = c >>> 0;
|
|
54
|
+
}
|
|
55
|
+
return table;
|
|
56
|
+
})();
|
|
57
|
+
function crc32(bytes) {
|
|
58
|
+
let c = 0xffffffff;
|
|
59
|
+
for (let i = 0; i < bytes.length; i++)
|
|
60
|
+
c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
61
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
62
|
+
}
|
|
63
|
+
function refusal(name) {
|
|
64
|
+
if (name.startsWith("/"))
|
|
65
|
+
return "an absolute name";
|
|
66
|
+
if (name.includes("\\"))
|
|
67
|
+
return "a name with a backslash";
|
|
68
|
+
if (name.includes("\u0000"))
|
|
69
|
+
return "a name with a NUL";
|
|
70
|
+
for (const segment of name.split("/")) {
|
|
71
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
72
|
+
return "a name with an empty, . or .. segment";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function sameBytes(a, b) {
|
|
78
|
+
if (a.length !== b.length)
|
|
79
|
+
return false;
|
|
80
|
+
for (let i = 0; i < a.length; i++)
|
|
81
|
+
if (a[i] !== b[i])
|
|
82
|
+
return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Every FILE entry of a ZIP archive, in central-directory order. A directory
|
|
87
|
+
* entry (a name ending in `/`) is not a file and is not returned. Throws
|
|
88
|
+
* `ZipReadError`.
|
|
89
|
+
*/
|
|
90
|
+
export function readZip(data) {
|
|
91
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
92
|
+
const n = data.length;
|
|
93
|
+
const u32 = (at) => view.getUint32(at, true);
|
|
94
|
+
const u16 = (at) => view.getUint16(at, true);
|
|
95
|
+
// The end-of-central-directory record: 22 bytes plus a comment of at most
|
|
96
|
+
// 65535, searched backwards so the LAST signature wins.
|
|
97
|
+
let eocd = -1;
|
|
98
|
+
const lowest = Math.max(0, n - 22 - 0xffff);
|
|
99
|
+
for (let at = n - 22; at >= lowest; at--) {
|
|
100
|
+
if (u32(at) === EOCD && at + 22 + u16(at + 20) === n) {
|
|
101
|
+
eocd = at;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (eocd < 0)
|
|
106
|
+
throw new ZipReadError("is not a readable .zip archive");
|
|
107
|
+
const disk = u16(eocd + 4);
|
|
108
|
+
const cdDisk = u16(eocd + 6);
|
|
109
|
+
const hereCount = u16(eocd + 8);
|
|
110
|
+
const count = u16(eocd + 10);
|
|
111
|
+
const cdSize = u32(eocd + 12);
|
|
112
|
+
const cdOffset = u32(eocd + 16);
|
|
113
|
+
if (count === 0xffff || cdSize === 0xffffffff || cdOffset === 0xffffffff) {
|
|
114
|
+
throw new ZipReadError("is a ZIP64 archive, which case does not read");
|
|
115
|
+
}
|
|
116
|
+
if (disk !== 0 || cdDisk !== 0 || hereCount !== count) {
|
|
117
|
+
throw new ZipReadError("spans more than one disk, which case does not read");
|
|
118
|
+
}
|
|
119
|
+
if (count > MAX_ZIP_ENTRIES) {
|
|
120
|
+
throw new ZipReadError(`holds more than ${MAX_ZIP_ENTRIES} entries; refusing to read it`);
|
|
121
|
+
}
|
|
122
|
+
if (cdOffset + cdSize > eocd)
|
|
123
|
+
throw new ZipReadError("is not a readable .zip archive");
|
|
124
|
+
const entries = [];
|
|
125
|
+
const names = new Set();
|
|
126
|
+
let total = 0;
|
|
127
|
+
let at = cdOffset;
|
|
128
|
+
for (let e = 0; e < count; e++) {
|
|
129
|
+
if (at + 46 > eocd || u32(at) !== CENTRAL) {
|
|
130
|
+
throw new ZipReadError("is not a readable .zip archive");
|
|
131
|
+
}
|
|
132
|
+
const madeBy = u16(at + 4);
|
|
133
|
+
const flags = u16(at + 8);
|
|
134
|
+
const method = u16(at + 10);
|
|
135
|
+
const crc = u32(at + 16);
|
|
136
|
+
const csize = u32(at + 20);
|
|
137
|
+
const usize = u32(at + 24);
|
|
138
|
+
const nameLen = u16(at + 28);
|
|
139
|
+
const extraLen = u16(at + 30);
|
|
140
|
+
const commentLen = u16(at + 32);
|
|
141
|
+
const external = u32(at + 38);
|
|
142
|
+
const localAt = u32(at + 42);
|
|
143
|
+
const rawName = data.subarray(at + 46, at + 46 + nameLen);
|
|
144
|
+
at += 46 + nameLen + extraLen + commentLen;
|
|
145
|
+
if (at > eocd)
|
|
146
|
+
throw new ZipReadError("is not a readable .zip archive");
|
|
147
|
+
let name;
|
|
148
|
+
try {
|
|
149
|
+
name = utf8Strict(rawName);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
throw new ZipReadError("holds a name that is not valid UTF-8; rename it, then run case again");
|
|
153
|
+
}
|
|
154
|
+
if (names.has(name))
|
|
155
|
+
throw new ZipReadError(`holds ${name} twice`);
|
|
156
|
+
names.add(name);
|
|
157
|
+
if (name.endsWith("/"))
|
|
158
|
+
continue;
|
|
159
|
+
if (name.split("/").length - 1 > MAX_ZIP_DEPTH) {
|
|
160
|
+
throw new ZipReadError(`is nested more than ${MAX_ZIP_DEPTH} folders deep; refusing to read it`);
|
|
161
|
+
}
|
|
162
|
+
let refused = refusal(name);
|
|
163
|
+
if (refused === null && flags & 0x1)
|
|
164
|
+
refused = "an encrypted entry";
|
|
165
|
+
if (refused === null &&
|
|
166
|
+
madeBy >> 8 === UNIX &&
|
|
167
|
+
((external >>> 16) & S_IFMT) === S_IFLNK) {
|
|
168
|
+
refused = "a symbolic link";
|
|
169
|
+
}
|
|
170
|
+
if (refused === null && method !== 0 && method !== 8) {
|
|
171
|
+
refused = "a compression method other than stored or deflate";
|
|
172
|
+
}
|
|
173
|
+
if (refused !== null) {
|
|
174
|
+
entries.push({ name, refused, data: null });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
total += usize;
|
|
178
|
+
if (total > MAX_ZIP_BYTES) {
|
|
179
|
+
throw new ZipReadError(`holds more than ${MAX_ZIP_BYTES} bytes uncompressed; refusing to read it`);
|
|
180
|
+
}
|
|
181
|
+
const unreadable = () => new ZipReadError(`holds ${name}, which cannot be read`);
|
|
182
|
+
if (localAt + 30 > cdOffset || u32(localAt) !== LOCAL)
|
|
183
|
+
throw unreadable();
|
|
184
|
+
const localNameLen = u16(localAt + 26);
|
|
185
|
+
const localExtraLen = u16(localAt + 28);
|
|
186
|
+
if (!sameBytes(data.subarray(localAt + 30, localAt + 30 + localNameLen), rawName)) {
|
|
187
|
+
throw unreadable();
|
|
188
|
+
}
|
|
189
|
+
const start = localAt + 30 + localNameLen + localExtraLen;
|
|
190
|
+
if (start + csize > cdOffset)
|
|
191
|
+
throw unreadable();
|
|
192
|
+
const packed = data.subarray(start, start + csize);
|
|
193
|
+
let body;
|
|
194
|
+
if (method === 0) {
|
|
195
|
+
body = packed;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// `info: true` hands back the engine, whose `bytesWritten` is how many
|
|
199
|
+
// input bytes the stream consumed. Anything left over is data after the
|
|
200
|
+
// end of the deflate stream, which Python's `unused_data` refuses too.
|
|
201
|
+
let result;
|
|
202
|
+
try {
|
|
203
|
+
result = inflateRawSync(packed, {
|
|
204
|
+
info: true,
|
|
205
|
+
maxOutputLength: usize + 1,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
throw unreadable();
|
|
210
|
+
}
|
|
211
|
+
if (result.engine.bytesWritten !== packed.length)
|
|
212
|
+
throw unreadable();
|
|
213
|
+
body = new Uint8Array(result.buffer.buffer, result.buffer.byteOffset, result.buffer.length);
|
|
214
|
+
}
|
|
215
|
+
if (body.length !== usize || crc32(body) !== crc)
|
|
216
|
+
throw unreadable();
|
|
217
|
+
entries.push({ name, refused: null, data: body.slice() });
|
|
218
|
+
}
|
|
219
|
+
return entries;
|
|
220
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanning/proof",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "TypeScript kernel of the Sanning verification stack: RFC 8785 (JCS) canonicalization, SHA-256, and Ed25519 verification for Verifiable Event Envelopes. Full-family verifier (inline + external commitment); accepts sanning.agent/v1 and sanning.events/v2. Conformance-gated byte-for-byte against the test-vectors corpus.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"main": "./dist/index.js",
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"bin": {
|
|
17
|
-
"proof": "./dist/cli.js"
|
|
17
|
+
"sanning-proof": "./dist/cli.js"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"src",
|
package/src/agent-proof.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Agent inclusion
|
|
1
|
+
// Agent inclusion proof verification (`sanning.agent.proof/v1`).
|
|
2
2
|
//
|
|
3
3
|
// The `sanning.agent.proof/v1` bundle is the agent's ~1.4 KB self-contained
|
|
4
4
|
// inclusion proof: a signed checkpoint envelope (sanning.agent/v1, INLINE payload)
|
|
@@ -19,7 +19,7 @@ import { leafHash, verifyInclusion } from "./merkle.js";
|
|
|
19
19
|
import { MAX_CANONICAL_DEPTH, exceedsDepth, jcs, jcsParse, verifyEnvelope } from "./verifier.js";
|
|
20
20
|
import type { Envelope } from "./types.js";
|
|
21
21
|
|
|
22
|
-
export const
|
|
22
|
+
export const AGENT_PROOF_SPEC_VERSION = "sanning.agent.proof/v1";
|
|
23
23
|
|
|
24
24
|
export interface AgentProofBundle {
|
|
25
25
|
spec_version: string;
|
|
@@ -63,10 +63,10 @@ function toHex(bytes: Uint8Array): string {
|
|
|
63
63
|
|
|
64
64
|
export function isAgentProofSpec(specVersion: unknown): boolean {
|
|
65
65
|
if (typeof specVersion !== "string") return false;
|
|
66
|
-
return specVersion ===
|
|
66
|
+
return specVersion === AGENT_PROOF_SPEC_VERSION || specVersion.startsWith(`${AGENT_PROOF_SPEC_VERSION}.`);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export async function
|
|
69
|
+
export async function verifyAgentProof(
|
|
70
70
|
bundle: unknown,
|
|
71
71
|
options: VerifyAgentProofOptions = {},
|
|
72
72
|
): Promise<AgentProofResult> {
|