@hcs-witness/verify 0.0.0 → 1.0.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 +36 -0
- package/LICENSE +202 -0
- package/README.md +145 -2
- package/bin/hcs-witness-verify.mjs +2 -0
- package/dist/anchor.d.ts +20 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +82 -0
- package/dist/base64.d.ts +3 -0
- package/dist/base64.d.ts.map +1 -0
- package/dist/base64.js +46 -0
- package/dist/binding.d.ts +22 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +38 -0
- package/dist/check.d.ts +22 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +100 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +103 -0
- package/dist/hash.d.ts +3 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/jcs.d.ts +5 -0
- package/dist/jcs.d.ts.map +1 -0
- package/dist/jcs.js +60 -0
- package/dist/keyset.d.ts +30 -0
- package/dist/keyset.d.ts.map +1 -0
- package/dist/keyset.js +62 -0
- package/dist/merkle.d.ts +26 -0
- package/dist/merkle.d.ts.map +1 -0
- package/dist/merkle.js +120 -0
- package/dist/ots.d.ts +48 -0
- package/dist/ots.d.ts.map +1 -0
- package/dist/ots.js +386 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +201 -0
- package/dist/verify.d.ts +12 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +145 -0
- package/package.json +69 -4
package/dist/ots.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { sha256 } from "./hash.js";
|
|
2
|
+
/**
|
|
3
|
+
* Client OpenTimestamps minimal (ADR 0009) — format .ots uniquement.
|
|
4
|
+
* Périmètre strict : opérations sha256 / append / prepend,
|
|
5
|
+
* attestations pending et Bitcoin. Toute autre opération ou
|
|
6
|
+
* attestation est une erreur explicite, jamais ignorée. Aucun réseau.
|
|
7
|
+
*
|
|
8
|
+
* Référence du format : opentimestamps/python-opentimestamps,
|
|
9
|
+
* opentimestamps/core/{timestamp,op,notary,serialize}.py.
|
|
10
|
+
*/
|
|
11
|
+
export class OtsError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "OtsError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const MAGIC = Uint8Array.from([
|
|
20
|
+
...new TextEncoder().encode("\0OpenTimestamps\0\0Proof\0"),
|
|
21
|
+
0xbf,
|
|
22
|
+
0x89,
|
|
23
|
+
0xe2,
|
|
24
|
+
0xe8,
|
|
25
|
+
0x84,
|
|
26
|
+
0xe8,
|
|
27
|
+
0x92,
|
|
28
|
+
0x94,
|
|
29
|
+
]);
|
|
30
|
+
const MAJOR_VERSION = 1;
|
|
31
|
+
const OP_SHA256 = 0x08;
|
|
32
|
+
const OP_APPEND = 0xf0;
|
|
33
|
+
const OP_PREPEND = 0xf1;
|
|
34
|
+
const SEP_FORK = 0xff;
|
|
35
|
+
const SEP_ATTESTATION = 0x00;
|
|
36
|
+
const TAG_PENDING = "83dfe30d2ef90c8e";
|
|
37
|
+
const TAG_BITCOIN = "0588960d73d71901";
|
|
38
|
+
const MAX_FILE_BYTES = 64 * 1024;
|
|
39
|
+
const MAX_MSG_BYTES = 4096;
|
|
40
|
+
const MAX_ATTESTATION_PAYLOAD = 8192;
|
|
41
|
+
const MAX_URI_BYTES = 1000;
|
|
42
|
+
const MAX_DEPTH = 256;
|
|
43
|
+
const MAX_NODES = 8192;
|
|
44
|
+
const MAX_HEIGHT = 0xffffffff;
|
|
45
|
+
const URI_ALLOWED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._/:";
|
|
46
|
+
class Reader {
|
|
47
|
+
buf;
|
|
48
|
+
pos = 0;
|
|
49
|
+
nodes = 0;
|
|
50
|
+
constructor(buf) {
|
|
51
|
+
this.buf = buf;
|
|
52
|
+
}
|
|
53
|
+
byte() {
|
|
54
|
+
const b = this.buf.at(this.pos);
|
|
55
|
+
if (b === undefined) {
|
|
56
|
+
throw new OtsError("TRUNCATED", "unexpected end of stream");
|
|
57
|
+
}
|
|
58
|
+
this.pos += 1;
|
|
59
|
+
return b;
|
|
60
|
+
}
|
|
61
|
+
bytes(n) {
|
|
62
|
+
if (n < 0 || this.pos + n > this.buf.length) {
|
|
63
|
+
throw new OtsError("TRUNCATED", "unexpected end of stream");
|
|
64
|
+
}
|
|
65
|
+
const out = this.buf.subarray(this.pos, this.pos + n);
|
|
66
|
+
this.pos += n;
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
varuint() {
|
|
70
|
+
let value = 0;
|
|
71
|
+
let shift = 0;
|
|
72
|
+
for (let i = 0; i < 9; i += 1) {
|
|
73
|
+
const b = this.byte();
|
|
74
|
+
value += (b & 0x7f) * 2 ** shift;
|
|
75
|
+
if ((b & 0x80) === 0) {
|
|
76
|
+
// Canonique : le dernier octet ne peut pas valoir 0 sauf si
|
|
77
|
+
// c'est le seul octet.
|
|
78
|
+
if (b === 0 && i > 0) {
|
|
79
|
+
throw new OtsError("VARUINT", "non-canonical varuint");
|
|
80
|
+
}
|
|
81
|
+
if (value > Number.MAX_SAFE_INTEGER) {
|
|
82
|
+
throw new OtsError("VARUINT", "varuint overflow");
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
shift += 7;
|
|
87
|
+
}
|
|
88
|
+
throw new OtsError("VARUINT", "varuint too long");
|
|
89
|
+
}
|
|
90
|
+
varbytes(maxLen, minLen = 0) {
|
|
91
|
+
const n = this.varuint();
|
|
92
|
+
if (n < minLen || n > maxLen) {
|
|
93
|
+
throw new OtsError("LENGTH", `varbytes length ${String(n)} out of bounds`);
|
|
94
|
+
}
|
|
95
|
+
return this.bytes(n);
|
|
96
|
+
}
|
|
97
|
+
done() {
|
|
98
|
+
return this.pos === this.buf.length;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function applyOp(op, msg) {
|
|
102
|
+
let out;
|
|
103
|
+
if (op.kind === "sha256") {
|
|
104
|
+
out = sha256(msg);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const arg = op.arg;
|
|
108
|
+
/* c8 ignore next 3 — défensif : le parseur garantit arg pour les ops binaires */
|
|
109
|
+
if (arg === undefined) {
|
|
110
|
+
throw new OtsError("OP", "binary op without arg");
|
|
111
|
+
}
|
|
112
|
+
out =
|
|
113
|
+
op.kind === "append"
|
|
114
|
+
? Uint8Array.from([...msg, ...arg])
|
|
115
|
+
: Uint8Array.from([...arg, ...msg]);
|
|
116
|
+
}
|
|
117
|
+
/* c8 ignore next 3 — défensif : out.length === 0 impossible (ops >= 1 octet) */
|
|
118
|
+
if (out.length === 0 || out.length > MAX_MSG_BYTES) {
|
|
119
|
+
throw new OtsError("MSG_LENGTH", "message length out of bounds");
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function hexOf(bytes) {
|
|
124
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
125
|
+
}
|
|
126
|
+
function readAttestation(r) {
|
|
127
|
+
const tag = hexOf(r.bytes(8));
|
|
128
|
+
const payload = r.varbytes(MAX_ATTESTATION_PAYLOAD);
|
|
129
|
+
if (tag === TAG_BITCOIN) {
|
|
130
|
+
const pr = new Reader(payload);
|
|
131
|
+
const height = pr.varuint();
|
|
132
|
+
if (!pr.done()) {
|
|
133
|
+
throw new OtsError("ATTESTATION", "trailing bytes in attestation");
|
|
134
|
+
}
|
|
135
|
+
if (height > MAX_HEIGHT) {
|
|
136
|
+
throw new OtsError("ATTESTATION", "block height out of range");
|
|
137
|
+
}
|
|
138
|
+
return { kind: "bitcoin", height };
|
|
139
|
+
}
|
|
140
|
+
if (tag === TAG_PENDING) {
|
|
141
|
+
const pr = new Reader(payload);
|
|
142
|
+
const raw = pr.varbytes(MAX_URI_BYTES);
|
|
143
|
+
if (!pr.done()) {
|
|
144
|
+
throw new OtsError("ATTESTATION", "trailing bytes in attestation");
|
|
145
|
+
}
|
|
146
|
+
let uri;
|
|
147
|
+
try {
|
|
148
|
+
uri = new TextDecoder("utf-8", { fatal: true }).decode(raw);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
throw new OtsError("ATTESTATION", "pending URI is not valid UTF-8");
|
|
152
|
+
}
|
|
153
|
+
for (const ch of uri) {
|
|
154
|
+
if (!URI_ALLOWED.includes(ch)) {
|
|
155
|
+
throw new OtsError("ATTESTATION", "forbidden char in pending URI");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { kind: "pending", uri };
|
|
159
|
+
}
|
|
160
|
+
throw new OtsError("UNKNOWN_ATTESTATION", `unknown attestation tag ${tag}`);
|
|
161
|
+
}
|
|
162
|
+
function readTimestamp(r, initialMsg, depth) {
|
|
163
|
+
if (depth > MAX_DEPTH) {
|
|
164
|
+
throw new OtsError("DEPTH", "timestamp tree too deep");
|
|
165
|
+
}
|
|
166
|
+
/* c8 ignore next 3 — défensif : l'en-tête impose un digest de 32 octets */
|
|
167
|
+
if (initialMsg.length === 0 || initialMsg.length > MAX_MSG_BYTES) {
|
|
168
|
+
throw new OtsError("MSG_LENGTH", "message length out of bounds");
|
|
169
|
+
}
|
|
170
|
+
r.nodes += 1;
|
|
171
|
+
/* c8 ignore next 3 — défensif : > 8192 nœuds excéderait la borne fichier de 64 Ko */
|
|
172
|
+
if (r.nodes > MAX_NODES) {
|
|
173
|
+
throw new OtsError("NODES", "timestamp tree too large");
|
|
174
|
+
}
|
|
175
|
+
const stamp = { msg: initialMsg, attestations: [], ops: [] };
|
|
176
|
+
const seenOps = new Set();
|
|
177
|
+
const readItem = (tag) => {
|
|
178
|
+
if (tag === SEP_ATTESTATION) {
|
|
179
|
+
stamp.attestations.push(readAttestation(r));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
let op;
|
|
183
|
+
if (tag === OP_SHA256) {
|
|
184
|
+
op = { kind: "sha256" };
|
|
185
|
+
}
|
|
186
|
+
else if (tag === OP_APPEND || tag === OP_PREPEND) {
|
|
187
|
+
op = {
|
|
188
|
+
kind: tag === OP_APPEND ? "append" : "prepend",
|
|
189
|
+
arg: r.varbytes(MAX_MSG_BYTES, 1),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
throw new OtsError("UNKNOWN_OP", `unsupported operation tag 0x${tag.toString(16)}`);
|
|
194
|
+
}
|
|
195
|
+
const key = `${String(tag)}:${hexOf(op.arg ?? new Uint8Array())}`;
|
|
196
|
+
if (seenOps.has(key)) {
|
|
197
|
+
throw new OtsError("DUPLICATE_OP", "repeated operation in timestamp");
|
|
198
|
+
}
|
|
199
|
+
seenOps.add(key);
|
|
200
|
+
stamp.ops.push({
|
|
201
|
+
op,
|
|
202
|
+
stamp: readTimestamp(r, applyOp(op, initialMsg), depth + 1),
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
let tag = r.byte();
|
|
206
|
+
while (tag === SEP_FORK) {
|
|
207
|
+
readItem(r.byte());
|
|
208
|
+
tag = r.byte();
|
|
209
|
+
}
|
|
210
|
+
readItem(tag);
|
|
211
|
+
return stamp;
|
|
212
|
+
}
|
|
213
|
+
/** Lit un fichier .ots complet. Jamais d'exception non gérée : OtsError. */
|
|
214
|
+
export function parseOtsFile(input) {
|
|
215
|
+
if (!(input instanceof Uint8Array)) {
|
|
216
|
+
throw new OtsError("TYPE", "input must be a Uint8Array");
|
|
217
|
+
}
|
|
218
|
+
if (input.length === 0 || input.length > MAX_FILE_BYTES) {
|
|
219
|
+
throw new OtsError("SIZE", "file size out of bounds");
|
|
220
|
+
}
|
|
221
|
+
const r = new Reader(input);
|
|
222
|
+
const magic = r.bytes(MAGIC.length);
|
|
223
|
+
if (hexOf(magic) !== hexOf(MAGIC)) {
|
|
224
|
+
throw new OtsError("BAD_MAGIC", "bad file magic");
|
|
225
|
+
}
|
|
226
|
+
if (r.varuint() !== MAJOR_VERSION) {
|
|
227
|
+
throw new OtsError("VERSION", "unsupported .ots major version");
|
|
228
|
+
}
|
|
229
|
+
const fileHashOp = r.byte();
|
|
230
|
+
if (fileHashOp !== OP_SHA256) {
|
|
231
|
+
throw new OtsError("FILE_HASH_OP", `unsupported file digest op 0x${fileHashOp.toString(16)}`);
|
|
232
|
+
}
|
|
233
|
+
const fileDigest = r.bytes(32);
|
|
234
|
+
const timestamp = readTimestamp(r, fileDigest, 0);
|
|
235
|
+
if (!r.done()) {
|
|
236
|
+
throw new OtsError("TRAILING", "trailing garbage after timestamp");
|
|
237
|
+
}
|
|
238
|
+
return { fileDigest, timestamp };
|
|
239
|
+
}
|
|
240
|
+
/* ------------------------------------------------------------------ */
|
|
241
|
+
/* Sérialisation canonique (miroir de timestamp.py serialize) */
|
|
242
|
+
/* ------------------------------------------------------------------ */
|
|
243
|
+
class Writer {
|
|
244
|
+
out = [];
|
|
245
|
+
byte(b) {
|
|
246
|
+
this.out.push(b);
|
|
247
|
+
}
|
|
248
|
+
bytes(bs) {
|
|
249
|
+
for (const b of bs)
|
|
250
|
+
this.out.push(b);
|
|
251
|
+
}
|
|
252
|
+
varuint(value) {
|
|
253
|
+
let v = value;
|
|
254
|
+
for (;;) {
|
|
255
|
+
const b = v % 128;
|
|
256
|
+
v = Math.floor(v / 128);
|
|
257
|
+
if (v === 0) {
|
|
258
|
+
this.byte(b);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.byte(b | 0x80);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
varbytes(bs) {
|
|
265
|
+
this.varuint(bs.length);
|
|
266
|
+
this.bytes(bs);
|
|
267
|
+
}
|
|
268
|
+
toBytes() {
|
|
269
|
+
return Uint8Array.from(this.out);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function bytesFromHex(hex) {
|
|
273
|
+
/* c8 ignore next — défensif : les tags internes sont de longueur paire */
|
|
274
|
+
return Uint8Array.from(hex.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));
|
|
275
|
+
}
|
|
276
|
+
function opSortKey(op) {
|
|
277
|
+
const tag = op.kind === "sha256"
|
|
278
|
+
? OP_SHA256
|
|
279
|
+
: op.kind === "append"
|
|
280
|
+
? OP_APPEND
|
|
281
|
+
: OP_PREPEND;
|
|
282
|
+
return Uint8Array.from([tag, ...(op.arg ?? [])]);
|
|
283
|
+
}
|
|
284
|
+
function cmpBytes(a, b) {
|
|
285
|
+
for (let i = 0; i < Math.min(a.length, b.length); i += 1) {
|
|
286
|
+
/* c8 ignore next — défensif : i < min(len) garantit la présence */
|
|
287
|
+
const d = (a.at(i) ?? 0) - (b.at(i) ?? 0);
|
|
288
|
+
if (d !== 0)
|
|
289
|
+
return d;
|
|
290
|
+
}
|
|
291
|
+
return a.length - b.length;
|
|
292
|
+
}
|
|
293
|
+
function attTag(att) {
|
|
294
|
+
return bytesFromHex(att.kind === "bitcoin" ? TAG_BITCOIN : TAG_PENDING);
|
|
295
|
+
}
|
|
296
|
+
function writeAttestation(w, att) {
|
|
297
|
+
w.bytes(attTag(att));
|
|
298
|
+
const p = new Writer();
|
|
299
|
+
if (att.kind === "bitcoin") {
|
|
300
|
+
p.varuint(att.height ?? 0);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
p.varbytes(new TextEncoder().encode(att.uri ?? ""));
|
|
304
|
+
}
|
|
305
|
+
w.varbytes(p.toBytes());
|
|
306
|
+
}
|
|
307
|
+
function writeOp(w, op) {
|
|
308
|
+
w.byte(op.kind === "sha256"
|
|
309
|
+
? OP_SHA256
|
|
310
|
+
: op.kind === "append"
|
|
311
|
+
? OP_APPEND
|
|
312
|
+
: OP_PREPEND);
|
|
313
|
+
if (op.arg !== undefined) {
|
|
314
|
+
w.varbytes(op.arg);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/** Sérialise un Timestamp (ordre canonique : attestations puis ops). */
|
|
318
|
+
export function serializeTimestamp(stamp) {
|
|
319
|
+
const w = new Writer();
|
|
320
|
+
const atts = [...stamp.attestations].sort((a, b) => cmpBytes(attTag(a), attTag(b)));
|
|
321
|
+
const ops = [...stamp.ops].sort((x, y) => cmpBytes(opSortKey(x.op), opSortKey(y.op)));
|
|
322
|
+
for (const att of atts.slice(0, -1)) {
|
|
323
|
+
w.byte(SEP_FORK);
|
|
324
|
+
w.byte(SEP_ATTESTATION);
|
|
325
|
+
writeAttestation(w, att);
|
|
326
|
+
}
|
|
327
|
+
const lastAtt = atts.at(-1);
|
|
328
|
+
if (ops.length === 0) {
|
|
329
|
+
if (lastAtt === undefined) {
|
|
330
|
+
throw new OtsError("EMPTY", "empty timestamp cannot be serialized");
|
|
331
|
+
}
|
|
332
|
+
w.byte(SEP_ATTESTATION);
|
|
333
|
+
writeAttestation(w, lastAtt);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
if (lastAtt !== undefined) {
|
|
337
|
+
w.byte(SEP_FORK);
|
|
338
|
+
w.byte(SEP_ATTESTATION);
|
|
339
|
+
writeAttestation(w, lastAtt);
|
|
340
|
+
}
|
|
341
|
+
for (const { op, stamp: sub } of ops.slice(0, -1)) {
|
|
342
|
+
w.byte(SEP_FORK);
|
|
343
|
+
writeOp(w, op);
|
|
344
|
+
w.bytes(serializeTimestamp(sub));
|
|
345
|
+
}
|
|
346
|
+
const last = ops.at(-1);
|
|
347
|
+
/* c8 ignore next 3 — défensif : ops.length > 0 garanti en amont */
|
|
348
|
+
if (last === undefined) {
|
|
349
|
+
throw new OtsError("EMPTY", "no operations");
|
|
350
|
+
}
|
|
351
|
+
writeOp(w, last.op);
|
|
352
|
+
w.bytes(serializeTimestamp(last.stamp));
|
|
353
|
+
}
|
|
354
|
+
return w.toBytes();
|
|
355
|
+
}
|
|
356
|
+
/** Fabrique un fichier .ots : magic ‖ version ‖ sha256 ‖ digest ‖ timestamp. */
|
|
357
|
+
export function serializeOtsFile(fileDigest, timestampBytes) {
|
|
358
|
+
if (fileDigest.length !== 32) {
|
|
359
|
+
throw new OtsError("DIGEST", "file digest must be 32 bytes");
|
|
360
|
+
}
|
|
361
|
+
return Uint8Array.from([
|
|
362
|
+
...MAGIC,
|
|
363
|
+
MAJOR_VERSION,
|
|
364
|
+
OP_SHA256,
|
|
365
|
+
...fileDigest,
|
|
366
|
+
...timestampBytes,
|
|
367
|
+
]);
|
|
368
|
+
}
|
|
369
|
+
/** Toutes les attestations de l'arbre (avec le msg attesté). */
|
|
370
|
+
export function allAttestations(stamp) {
|
|
371
|
+
const out = stamp.attestations.map((attestation) => ({
|
|
372
|
+
msg: stamp.msg,
|
|
373
|
+
attestation,
|
|
374
|
+
}));
|
|
375
|
+
for (const { stamp: sub } of stamp.ops) {
|
|
376
|
+
out.push(...allAttestations(sub));
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
/** Plus petite hauteur de bloc Bitcoin attestée, si présente. */
|
|
381
|
+
export function bitcoinBlockHeight(stamp) {
|
|
382
|
+
const heights = allAttestations(stamp)
|
|
383
|
+
.filter((a) => a.attestation.kind === "bitcoin")
|
|
384
|
+
.map((a) => a.attestation.height ?? 0);
|
|
385
|
+
return heights.length === 0 ? undefined : Math.min(...heights);
|
|
386
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type JsonSchema } from "./check.js";
|
|
2
|
+
/** Schéma JSON du document signé — contrat de format, immuable sans ADR. */
|
|
3
|
+
export declare const proofSchema: JsonSchema;
|
|
4
|
+
/** Partie signée de la preuve (payload JCS sans `sig` ni `anchor`). */
|
|
5
|
+
export declare const unsignedProofSchema: JsonSchema;
|
|
6
|
+
export interface PresenceChallenge {
|
|
7
|
+
kind: "color" | "numeric" | "stroop" | "size";
|
|
8
|
+
/** Niveau de décision : absent sur les preuves antérieures à prof-3. */
|
|
9
|
+
level?: "standard" | "enhanced";
|
|
10
|
+
trials: number;
|
|
11
|
+
completed: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Bloc présence : consigné dans la preuve signée, JAMAIS utilisé pour la
|
|
14
|
+
* validité cryptographique. Présent sur v:2, absent sur v:1. */
|
|
15
|
+
export interface Presence {
|
|
16
|
+
model: "presence-v1";
|
|
17
|
+
calibrated: false;
|
|
18
|
+
band: "unrated";
|
|
19
|
+
challenge: PresenceChallenge;
|
|
20
|
+
signals: "trusted" | "untrusted" | "absent";
|
|
21
|
+
automation_indicators: number;
|
|
22
|
+
profile_sha256: string | null;
|
|
23
|
+
rotation: {
|
|
24
|
+
tw: number;
|
|
25
|
+
ok: boolean;
|
|
26
|
+
replayed: boolean;
|
|
27
|
+
};
|
|
28
|
+
flags: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface ProofSignature {
|
|
31
|
+
kid: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ProofAnchor {
|
|
35
|
+
batch_id: string;
|
|
36
|
+
merkle_root: string;
|
|
37
|
+
leaf_index: number;
|
|
38
|
+
path: string[];
|
|
39
|
+
ots?: string;
|
|
40
|
+
bitcoin_block_height?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface UnsignedProof {
|
|
43
|
+
v: 1 | 2;
|
|
44
|
+
proof_id: string;
|
|
45
|
+
tenant_id: string;
|
|
46
|
+
approval_id: string;
|
|
47
|
+
action: string;
|
|
48
|
+
subject_ref: string;
|
|
49
|
+
context_sha256: string;
|
|
50
|
+
display_sha256: string;
|
|
51
|
+
decision: "approved" | "denied" | "expired";
|
|
52
|
+
human_factor: {
|
|
53
|
+
method: "webauthn";
|
|
54
|
+
user_verified: true;
|
|
55
|
+
credential_id_sha256: string;
|
|
56
|
+
authenticator_aaguid: string;
|
|
57
|
+
};
|
|
58
|
+
signals?: Record<string, unknown>;
|
|
59
|
+
presence?: Presence;
|
|
60
|
+
issued_at: string;
|
|
61
|
+
expires_at: string;
|
|
62
|
+
}
|
|
63
|
+
export interface Proof extends UnsignedProof {
|
|
64
|
+
sig: {
|
|
65
|
+
es256: ProofSignature;
|
|
66
|
+
mldsa65: ProofSignature;
|
|
67
|
+
};
|
|
68
|
+
anchor?: ProofAnchor;
|
|
69
|
+
}
|
|
70
|
+
export declare function isProof(value: unknown): value is Proof;
|
|
71
|
+
export declare function isUnsignedProof(value: unknown): value is UnsignedProof;
|
|
72
|
+
/** Canonical JCS bytes of the proof without `sig` and `anchor` (SPEC §3). */
|
|
73
|
+
export declare function signedPayload(proof: UnsignedProof): Uint8Array;
|
|
74
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAgK1D,4EAA4E;AAC5E,eAAO,MAAM,WAAW,EAAE,UA6BzB,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,EAAE,UAKjC,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,wEAAwE;IACxE,KAAK,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;iEACiE;AACjE,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,aAAa,CAAC;IACrB,UAAU,EAAE,KAAK,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC5C,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,EAAE,EAAE,OAAO,CAAC;QACZ,QAAQ,EAAE,OAAO,CAAC;KACnB,CAAC;IACF,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC5C,YAAY,EAAE;QACZ,MAAM,EAAE,UAAU,CAAC;QACnB,aAAa,EAAE,IAAI,CAAC;QACpB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,oBAAoB,EAAE,MAAM,CAAC;KAC9B,CAAC;IACF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,KAAM,SAAQ,aAAa;IAC1C,GAAG,EAAE;QACH,KAAK,EAAE,cAAc,CAAC;QACtB,OAAO,EAAE,cAAc,CAAC;KACzB,CAAC;IACF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAEtD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CAEtE;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,UAAU,CAK9D"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { checkSchema } from "./check.js";
|
|
2
|
+
import { canonicalBytes } from "./jcs.js";
|
|
3
|
+
const ULID_CHARS = "[0-9A-HJKMNP-TV-Z]{26}";
|
|
4
|
+
const ulidString = (prefix) => ({
|
|
5
|
+
type: "string",
|
|
6
|
+
pattern: `^${prefix}_${ULID_CHARS}$`,
|
|
7
|
+
});
|
|
8
|
+
const hex64 = { type: "string", pattern: "^[0-9a-f]{64}$" };
|
|
9
|
+
const b64url = { type: "string", pattern: "^[A-Za-z0-9_-]+$" };
|
|
10
|
+
const rfc3339Utc = {
|
|
11
|
+
type: "string",
|
|
12
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,3})?Z$",
|
|
13
|
+
};
|
|
14
|
+
const uuid = {
|
|
15
|
+
type: "string",
|
|
16
|
+
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
17
|
+
};
|
|
18
|
+
const presenceChallengeSchema = {
|
|
19
|
+
type: "object",
|
|
20
|
+
required: ["kind", "trials", "completed"],
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
properties: {
|
|
23
|
+
kind: {
|
|
24
|
+
anyOf: [
|
|
25
|
+
{ const: "color", type: "string" },
|
|
26
|
+
{ const: "numeric", type: "string" },
|
|
27
|
+
{ const: "stroop", type: "string" },
|
|
28
|
+
{ const: "size", type: "string" },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
level: {
|
|
32
|
+
anyOf: [
|
|
33
|
+
{ const: "standard", type: "string" },
|
|
34
|
+
{ const: "enhanced", type: "string" },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
trials: { type: "integer", minimum: 0 },
|
|
38
|
+
completed: { type: "boolean" },
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const presenceSchema = {
|
|
42
|
+
type: "object",
|
|
43
|
+
required: [
|
|
44
|
+
"model",
|
|
45
|
+
"calibrated",
|
|
46
|
+
"band",
|
|
47
|
+
"challenge",
|
|
48
|
+
"signals",
|
|
49
|
+
"automation_indicators",
|
|
50
|
+
"profile_sha256",
|
|
51
|
+
"rotation",
|
|
52
|
+
"flags",
|
|
53
|
+
],
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
properties: {
|
|
56
|
+
model: { const: "presence-v1", type: "string" },
|
|
57
|
+
calibrated: { const: false, type: "boolean" },
|
|
58
|
+
band: { const: "unrated", type: "string" },
|
|
59
|
+
challenge: presenceChallengeSchema,
|
|
60
|
+
signals: {
|
|
61
|
+
anyOf: [
|
|
62
|
+
{ const: "trusted", type: "string" },
|
|
63
|
+
{ const: "untrusted", type: "string" },
|
|
64
|
+
{ const: "absent", type: "string" },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
automation_indicators: { type: "integer", minimum: 0 },
|
|
68
|
+
profile_sha256: { anyOf: [hex64, { type: "null" }] },
|
|
69
|
+
rotation: {
|
|
70
|
+
type: "object",
|
|
71
|
+
required: ["tw", "ok", "replayed"],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
properties: {
|
|
74
|
+
tw: { type: "integer", minimum: 0 },
|
|
75
|
+
ok: { type: "boolean" },
|
|
76
|
+
replayed: { type: "boolean" },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
flags: { type: "array", items: { type: "string" } },
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const signatureSchema = {
|
|
83
|
+
type: "object",
|
|
84
|
+
required: ["kid", "value"],
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
properties: {
|
|
87
|
+
kid: { type: "string", minLength: 1 },
|
|
88
|
+
value: b64url,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const humanFactorSchema = {
|
|
92
|
+
type: "object",
|
|
93
|
+
required: [
|
|
94
|
+
"method",
|
|
95
|
+
"user_verified",
|
|
96
|
+
"credential_id_sha256",
|
|
97
|
+
"authenticator_aaguid",
|
|
98
|
+
],
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
properties: {
|
|
101
|
+
method: { const: "webauthn", type: "string" },
|
|
102
|
+
user_verified: { const: true, type: "boolean" },
|
|
103
|
+
credential_id_sha256: hex64,
|
|
104
|
+
authenticator_aaguid: uuid,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const unsignedProofProperties = {
|
|
108
|
+
v: {
|
|
109
|
+
anyOf: [
|
|
110
|
+
{ const: 1, type: "number" },
|
|
111
|
+
{ const: 2, type: "number" },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
proof_id: ulidString("ggp"),
|
|
115
|
+
tenant_id: ulidString("tnt"),
|
|
116
|
+
approval_id: ulidString("apr"),
|
|
117
|
+
action: { type: "string", pattern: "^[a-z0-9]+(\\.[a-z0-9_]+){1,5}$" },
|
|
118
|
+
subject_ref: { type: "string", minLength: 1 },
|
|
119
|
+
context_sha256: hex64,
|
|
120
|
+
display_sha256: hex64,
|
|
121
|
+
decision: {
|
|
122
|
+
anyOf: [
|
|
123
|
+
{ const: "approved", type: "string" },
|
|
124
|
+
{ const: "denied", type: "string" },
|
|
125
|
+
{ const: "expired", type: "string" },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
human_factor: humanFactorSchema,
|
|
129
|
+
signals: {
|
|
130
|
+
type: "object",
|
|
131
|
+
patternProperties: { "^(.*)$": {} },
|
|
132
|
+
},
|
|
133
|
+
presence: presenceSchema,
|
|
134
|
+
issued_at: rfc3339Utc,
|
|
135
|
+
expires_at: rfc3339Utc,
|
|
136
|
+
};
|
|
137
|
+
const unsignedProofRequired = [
|
|
138
|
+
"v",
|
|
139
|
+
"proof_id",
|
|
140
|
+
"tenant_id",
|
|
141
|
+
"approval_id",
|
|
142
|
+
"action",
|
|
143
|
+
"subject_ref",
|
|
144
|
+
"context_sha256",
|
|
145
|
+
"display_sha256",
|
|
146
|
+
"decision",
|
|
147
|
+
"human_factor",
|
|
148
|
+
"issued_at",
|
|
149
|
+
"expires_at",
|
|
150
|
+
];
|
|
151
|
+
/** Schéma JSON du document signé — contrat de format, immuable sans ADR. */
|
|
152
|
+
export const proofSchema = {
|
|
153
|
+
type: "object",
|
|
154
|
+
required: [...unsignedProofRequired, "sig"],
|
|
155
|
+
additionalProperties: false,
|
|
156
|
+
properties: {
|
|
157
|
+
...unsignedProofProperties,
|
|
158
|
+
sig: {
|
|
159
|
+
type: "object",
|
|
160
|
+
required: ["es256", "mldsa65"],
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
properties: {
|
|
163
|
+
es256: signatureSchema,
|
|
164
|
+
mldsa65: signatureSchema,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
anchor: {
|
|
168
|
+
type: "object",
|
|
169
|
+
required: ["batch_id", "merkle_root", "leaf_index", "path"],
|
|
170
|
+
additionalProperties: false,
|
|
171
|
+
properties: {
|
|
172
|
+
batch_id: { type: "string", minLength: 1 },
|
|
173
|
+
merkle_root: hex64,
|
|
174
|
+
leaf_index: { type: "integer", minimum: 0 },
|
|
175
|
+
path: { type: "array", items: hex64 },
|
|
176
|
+
ots: { type: "string", minLength: 1 },
|
|
177
|
+
bitcoin_block_height: { type: "integer", minimum: 0 },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
/** Partie signée de la preuve (payload JCS sans `sig` ni `anchor`). */
|
|
183
|
+
export const unsignedProofSchema = {
|
|
184
|
+
type: "object",
|
|
185
|
+
required: unsignedProofRequired,
|
|
186
|
+
additionalProperties: false,
|
|
187
|
+
properties: unsignedProofProperties,
|
|
188
|
+
};
|
|
189
|
+
export function isProof(value) {
|
|
190
|
+
return checkSchema(proofSchema, value);
|
|
191
|
+
}
|
|
192
|
+
export function isUnsignedProof(value) {
|
|
193
|
+
return checkSchema(unsignedProofSchema, value);
|
|
194
|
+
}
|
|
195
|
+
/** Canonical JCS bytes of the proof without `sig` and `anchor` (SPEC §3). */
|
|
196
|
+
export function signedPayload(proof) {
|
|
197
|
+
const unsigned = { ...proof };
|
|
198
|
+
delete unsigned.sig;
|
|
199
|
+
delete unsigned.anchor;
|
|
200
|
+
return canonicalBytes(unsigned);
|
|
201
|
+
}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const ML_DSA_CONTEXT: Uint8Array<ArrayBuffer>;
|
|
2
|
+
export type VerifyErrorCode = "SCHEMA_INVALID" | "UNSUPPORTED_VERSION" | "KEYSET_INVALID" | "ES256_KID_UNKNOWN" | "MLDSA_KID_UNKNOWN" | "ES256_SIG_INVALID" | "MLDSA_SIG_INVALID" | "ES256_HIGH_S" | "KEY_NOT_VALID_AT_ISSUANCE" | "PROOF_EXPIRED" | "TIMESTAMPS_INCOHERENT";
|
|
3
|
+
export interface VerifyResult {
|
|
4
|
+
readonly valid: boolean;
|
|
5
|
+
readonly errors: readonly VerifyErrorCode[];
|
|
6
|
+
readonly proof_id?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface VerifyOptions {
|
|
9
|
+
readonly now?: Date;
|
|
10
|
+
}
|
|
11
|
+
export declare function verifyProof(proof: unknown, keyset: unknown, opts?: VerifyOptions): VerifyResult;
|
|
12
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,cAAc,yBAAiD,CAAC;AAQ7E,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,qBAAqB,GACrB,gBAAgB,GAChB,mBAAmB,GACnB,mBAAmB,GACnB,mBAAmB,GACnB,mBAAmB,GACnB,cAAc,GACd,2BAA2B,GAC3B,eAAe,GACf,uBAAuB,CAAC;AAE5B,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;IAC5C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;CACrB;AAcD,wBAAgB,WAAW,CACzB,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,OAAO,EACf,IAAI,CAAC,EAAE,aAAa,GACnB,YAAY,CAuId"}
|