@zanii/blackbox 0.2.0 → 0.4.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 +26 -1
- package/dist/a2a/index.d.ts +77 -0
- package/dist/a2a/index.js +305 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/analysis/accuracy.d.ts +24 -0
- package/dist/analysis/accuracy.js +45 -0
- package/dist/analysis/credential.d.ts +101 -0
- package/dist/analysis/credential.js +142 -0
- package/dist/analysis/faults.js +115 -0
- package/dist/analysis/grounding.d.ts +122 -0
- package/dist/analysis/grounding.js +445 -0
- package/dist/analysis/hallucination.d.ts +32 -0
- package/dist/analysis/hallucination.js +357 -0
- package/dist/analysis/index.d.ts +23 -0
- package/dist/analysis/index.js +93 -0
- package/dist/analysis/memory.d.ts +8 -0
- package/dist/analysis/memory.js +35 -8
- package/dist/analysis/reference.d.ts +49 -0
- package/dist/analysis/reference.js +164 -0
- package/dist/analysis/taxonomy.d.ts +18 -0
- package/dist/analysis/taxonomy.js +66 -0
- package/dist/approvals/index.d.ts +23 -0
- package/dist/approvals/index.js +48 -0
- package/dist/archive/index.d.ts +39 -0
- package/dist/archive/index.js +96 -0
- package/dist/archive/parquet.d.ts +2 -0
- package/dist/archive/parquet.js +185 -0
- package/dist/badge/index.d.ts +16 -0
- package/dist/badge/index.js +48 -0
- package/dist/bom/index.d.ts +14 -0
- package/dist/bom/index.js +152 -0
- package/dist/cli.js +114 -10
- package/dist/compliance/art12.d.ts +35 -0
- package/dist/compliance/art12.js +190 -0
- package/dist/compliance/index.d.ts +36 -2
- package/dist/compliance/index.js +78 -11
- package/dist/compliance/zanii.d.ts +29 -0
- package/dist/compliance/zanii.js +84 -0
- package/dist/constitution/index.d.ts +57 -0
- package/dist/constitution/index.js +131 -0
- package/dist/cv/index.d.ts +39 -0
- package/dist/cv/index.js +108 -0
- package/dist/disclosure/index.d.ts +31 -0
- package/dist/disclosure/index.js +113 -0
- package/dist/encryption/index.d.ts +9 -0
- package/dist/encryption/index.js +31 -0
- package/dist/evidence/index.d.ts +60 -0
- package/dist/evidence/index.js +151 -0
- package/dist/federation/index.d.ts +35 -0
- package/dist/federation/index.js +102 -0
- package/dist/finance/index.d.ts +126 -0
- package/dist/finance/index.js +320 -0
- package/dist/fleet/index.js +9 -0
- package/dist/gov/index.d.ts +108 -0
- package/dist/gov/index.js +225 -0
- package/dist/health/index.d.ts +120 -0
- package/dist/health/index.js +233 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.js +34 -5
- package/dist/memory/index.d.ts +36 -0
- package/dist/memory/index.js +85 -0
- package/dist/occurrence/index.d.ts +11 -0
- package/dist/occurrence/index.js +18 -0
- package/dist/ocsf/index.d.ts +1 -1
- package/dist/ocsf/index.js +36 -3
- package/dist/otlp/index.d.ts +8 -1
- package/dist/otlp/index.js +258 -1
- package/dist/packs/index.js +44 -4
- package/dist/policy/delta.js +7 -1
- package/dist/policy/index.d.ts +40 -6
- package/dist/policy/index.js +186 -8
- package/dist/policy/zanii.d.ts +31 -0
- package/dist/policy/zanii.js +87 -0
- package/dist/pq/index.d.ts +23 -0
- package/dist/pq/index.js +104 -0
- package/dist/search/index.d.ts +23 -0
- package/dist/search/index.js +69 -0
- package/dist/session/index.d.ts +107 -1
- package/dist/session/index.js +189 -11
- package/dist/sla/index.d.ts +61 -0
- package/dist/sla/index.js +197 -0
- package/dist/succession/index.d.ts +50 -0
- package/dist/succession/index.js +123 -0
- package/dist/timestamp/index.d.ts +24 -0
- package/dist/timestamp/index.js +274 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.js +46 -0
- package/dist/transparency/index.d.ts +188 -0
- package/dist/transparency/index.js +712 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walls/index.d.ts +31 -0
- package/dist/walls/index.js +119 -0
- package/package.json +1 -1
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
// Transparency (spec/transparency.md): deterministic CBOR, COSE_Sign1 Signed Statements (RFC 9052,
|
|
2
|
+
// SCITT RFC 9943), RFC 9162 Merkle trees and proofs, COSE Receipts (RFC 9942), and C2SP signed notes,
|
|
3
|
+
// checkpoints and witness cosignatures. Pure functions with node:crypto only, pinned with the Python
|
|
4
|
+
// SDK by spec/vectors/transparency.json. Mirrors zanii_blackbox/transparency.py.
|
|
5
|
+
import { createHash, createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
|
|
6
|
+
import { canonicalBytes } from "@zanii/core";
|
|
7
|
+
import { verifyTimestamp } from "../timestamp/index.js";
|
|
8
|
+
import { hashLine } from "../verify/envelope.js";
|
|
9
|
+
export const tagged = (tag, value) => ({ tag, value });
|
|
10
|
+
const head = (major, n) => {
|
|
11
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
12
|
+
throw new RangeError("cbor: argument out of range");
|
|
13
|
+
const m = major << 5;
|
|
14
|
+
if (n < 24)
|
|
15
|
+
return Buffer.from([m | n]);
|
|
16
|
+
if (n < 0x100)
|
|
17
|
+
return Buffer.from([m | 24, n]);
|
|
18
|
+
if (n < 0x10000)
|
|
19
|
+
return Buffer.from([m | 25, n >> 8, n & 0xff]);
|
|
20
|
+
if (n < 0x100000000) {
|
|
21
|
+
const b = Buffer.alloc(5);
|
|
22
|
+
b[0] = m | 26;
|
|
23
|
+
b.writeUInt32BE(n, 1);
|
|
24
|
+
return b;
|
|
25
|
+
}
|
|
26
|
+
const b = Buffer.alloc(9);
|
|
27
|
+
b[0] = m | 27;
|
|
28
|
+
b.writeBigUInt64BE(BigInt(n), 1);
|
|
29
|
+
return b;
|
|
30
|
+
};
|
|
31
|
+
export function cborEncode(v) {
|
|
32
|
+
if (typeof v === "number")
|
|
33
|
+
return v >= 0 ? head(0, v) : head(1, -1 - v);
|
|
34
|
+
if (typeof v === "string") {
|
|
35
|
+
const b = Buffer.from(v, "utf8");
|
|
36
|
+
return Buffer.concat([head(3, b.length), b]);
|
|
37
|
+
}
|
|
38
|
+
if (v instanceof Uint8Array)
|
|
39
|
+
return Buffer.concat([head(2, v.length), Buffer.from(v)]);
|
|
40
|
+
if (v === false)
|
|
41
|
+
return Buffer.from([0xf4]);
|
|
42
|
+
if (v === true)
|
|
43
|
+
return Buffer.from([0xf5]);
|
|
44
|
+
if (v === null)
|
|
45
|
+
return Buffer.from([0xf6]);
|
|
46
|
+
if (Array.isArray(v))
|
|
47
|
+
return Buffer.concat([head(4, v.length), ...v.map(cborEncode)]);
|
|
48
|
+
if (v instanceof Map) {
|
|
49
|
+
const entries = [...v.entries()].map(([k, x]) => [cborEncode(k), cborEncode(x)]);
|
|
50
|
+
entries.sort((a, b) => Buffer.compare(a[0], b[0]));
|
|
51
|
+
for (let i = 1; i < entries.length; i++)
|
|
52
|
+
if (Buffer.compare(entries[i - 1][0], entries[i][0]) === 0)
|
|
53
|
+
throw new Error("cbor: duplicate map key");
|
|
54
|
+
return Buffer.concat([head(5, entries.length), ...entries.flat()]);
|
|
55
|
+
}
|
|
56
|
+
return Buffer.concat([head(6, v.tag), cborEncode(v.value)]);
|
|
57
|
+
}
|
|
58
|
+
/** Decodes one deterministic CBOR item; throws on anything else (indefinite lengths, floats, extra bytes). */
|
|
59
|
+
export function cborDecode(bytes) {
|
|
60
|
+
const buf = Buffer.from(bytes);
|
|
61
|
+
let at = 0;
|
|
62
|
+
const arg = (ai) => {
|
|
63
|
+
if (ai < 24)
|
|
64
|
+
return ai;
|
|
65
|
+
const n = ai === 24 ? 1 : ai === 25 ? 2 : ai === 26 ? 4 : ai === 27 ? 8 : 0;
|
|
66
|
+
if (n === 0 || at + n > buf.length)
|
|
67
|
+
throw new Error("cbor: bad argument");
|
|
68
|
+
const v = n === 8 ? Number(buf.readBigUInt64BE(at)) : buf.readUIntBE(at, n);
|
|
69
|
+
at += n;
|
|
70
|
+
if (!Number.isSafeInteger(v))
|
|
71
|
+
throw new Error("cbor: integer too large");
|
|
72
|
+
const min = n === 1 ? 24 : n === 2 ? 0x100 : n === 4 ? 0x10000 : 0x100000000;
|
|
73
|
+
if (v < min)
|
|
74
|
+
throw new Error("cbor: not the shortest form");
|
|
75
|
+
return v;
|
|
76
|
+
};
|
|
77
|
+
const item = (depth) => {
|
|
78
|
+
if (depth > 32 || at >= buf.length)
|
|
79
|
+
throw new Error("cbor: truncated");
|
|
80
|
+
const ib = buf[at++];
|
|
81
|
+
const major = ib >> 5;
|
|
82
|
+
const ai = ib & 31;
|
|
83
|
+
if (major === 7) {
|
|
84
|
+
if (ib === 0xf4)
|
|
85
|
+
return false;
|
|
86
|
+
if (ib === 0xf5)
|
|
87
|
+
return true;
|
|
88
|
+
if (ib === 0xf6)
|
|
89
|
+
return null;
|
|
90
|
+
throw new Error("cbor: unsupported simple value or float");
|
|
91
|
+
}
|
|
92
|
+
const n = arg(ai);
|
|
93
|
+
if (major === 0)
|
|
94
|
+
return n;
|
|
95
|
+
if (major === 1)
|
|
96
|
+
return -1 - n;
|
|
97
|
+
if (major === 2 || major === 3) {
|
|
98
|
+
if (at + n > buf.length)
|
|
99
|
+
throw new Error("cbor: truncated");
|
|
100
|
+
const s = buf.subarray(at, at + n);
|
|
101
|
+
at += n;
|
|
102
|
+
return major === 2 ? new Uint8Array(s) : s.toString("utf8");
|
|
103
|
+
}
|
|
104
|
+
if (major === 4)
|
|
105
|
+
return Array.from({ length: n }, () => item(depth + 1));
|
|
106
|
+
if (major === 5) {
|
|
107
|
+
const m = new Map();
|
|
108
|
+
let last = null;
|
|
109
|
+
for (let i = 0; i < n; i++) {
|
|
110
|
+
const start = at;
|
|
111
|
+
const k = item(depth + 1);
|
|
112
|
+
const kb = buf.subarray(start, at);
|
|
113
|
+
if (last && Buffer.compare(last, kb) >= 0)
|
|
114
|
+
throw new Error("cbor: map keys not in order");
|
|
115
|
+
last = kb;
|
|
116
|
+
m.set(k, item(depth + 1));
|
|
117
|
+
}
|
|
118
|
+
return m;
|
|
119
|
+
}
|
|
120
|
+
return { tag: n, value: item(depth + 1) };
|
|
121
|
+
};
|
|
122
|
+
const v = item(0);
|
|
123
|
+
if (at !== buf.length)
|
|
124
|
+
throw new Error("cbor: extra bytes");
|
|
125
|
+
return v;
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------- Ed25519 (raw 32-byte keys)
|
|
128
|
+
const PKCS8 = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
129
|
+
const SPKI = Buffer.from("302a300506032b6570032100", "hex");
|
|
130
|
+
const priv = (seed) => createPrivateKey({
|
|
131
|
+
key: Buffer.concat([PKCS8, Buffer.from(seed)]),
|
|
132
|
+
format: "der",
|
|
133
|
+
type: "pkcs8",
|
|
134
|
+
});
|
|
135
|
+
const pub = (key) => createPublicKey({ key: Buffer.concat([SPKI, Buffer.from(key)]), format: "der", type: "spki" });
|
|
136
|
+
export const ed25519Sign = (seed, msg) => new Uint8Array(sign(null, msg, priv(seed)));
|
|
137
|
+
export function ed25519Verify(publicKey, msg, sig) {
|
|
138
|
+
try {
|
|
139
|
+
return publicKey.length === 32 && sig.length === 64 && verify(null, msg, pub(publicKey), sig);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export const ed25519PublicKey = (seed) => new Uint8Array(createPublicKey(priv(seed)).export({ format: "der", type: "spki" }).subarray(-32));
|
|
146
|
+
const sha256 = (...parts) => {
|
|
147
|
+
const h = createHash("sha256");
|
|
148
|
+
for (const p of parts)
|
|
149
|
+
h.update(p);
|
|
150
|
+
return new Uint8Array(h.digest());
|
|
151
|
+
};
|
|
152
|
+
// ---------------------------------------------------------------- COSE_Sign1 (RFC 9052, RFC 9864)
|
|
153
|
+
export const ALG_ED25519 = -19;
|
|
154
|
+
const ALG_EDDSA = -8; // deprecated; accepted when verifying, never made
|
|
155
|
+
const SIG1 = (protectedBytes, payload) => cborEncode(["Signature1", protectedBytes, new Uint8Array(0), payload]);
|
|
156
|
+
/** A SCITT Signed Statement: tagged COSE_Sign1, Ed25519 (alg -19), empty unprotected header, payload attached. */
|
|
157
|
+
export function signStatement(input, seed) {
|
|
158
|
+
const protectedBytes = cborEncode(new Map([
|
|
159
|
+
[1, ALG_ED25519],
|
|
160
|
+
[3, input.contentType],
|
|
161
|
+
[4, input.kid],
|
|
162
|
+
[
|
|
163
|
+
15,
|
|
164
|
+
new Map([
|
|
165
|
+
[1, input.iss],
|
|
166
|
+
[2, input.sub],
|
|
167
|
+
[6, input.iat],
|
|
168
|
+
]),
|
|
169
|
+
],
|
|
170
|
+
]));
|
|
171
|
+
const sig = ed25519Sign(seed, SIG1(protectedBytes, input.payload));
|
|
172
|
+
return new Uint8Array(cborEncode(tagged(18, [protectedBytes, new Map(), input.payload, sig])));
|
|
173
|
+
}
|
|
174
|
+
/** Parses a tagged COSE_Sign1; throws if it isn't one. */
|
|
175
|
+
export function parseCoseSign1(bytes) {
|
|
176
|
+
const v = cborDecode(bytes);
|
|
177
|
+
const arr = v?.tag === 18 ? v.value : null;
|
|
178
|
+
if (!Array.isArray(arr) || arr.length !== 4)
|
|
179
|
+
throw new Error("not a tagged COSE_Sign1");
|
|
180
|
+
const [p, u, payload, signature] = arr;
|
|
181
|
+
if (!(p instanceof Uint8Array) || !(u instanceof Map) || !(signature instanceof Uint8Array))
|
|
182
|
+
throw new Error("not a COSE_Sign1");
|
|
183
|
+
if (payload !== null && !(payload instanceof Uint8Array))
|
|
184
|
+
throw new Error("bad COSE payload");
|
|
185
|
+
const prot = p.length === 0 ? new Map() : cborDecode(p);
|
|
186
|
+
if (!(prot instanceof Map))
|
|
187
|
+
throw new Error("bad protected header");
|
|
188
|
+
return { protectedBytes: p, protected: prot, unprotected: u, payload, signature };
|
|
189
|
+
}
|
|
190
|
+
/** Does a COSE_Sign1 verify with this Ed25519 key? `detached`: the payload, when it isn't attached. */
|
|
191
|
+
export function verifyCoseSign1(bytes, publicKey, detached) {
|
|
192
|
+
try {
|
|
193
|
+
const c = parseCoseSign1(bytes);
|
|
194
|
+
const alg = c.protected.get(1);
|
|
195
|
+
if (alg !== ALG_ED25519 && alg !== ALG_EDDSA)
|
|
196
|
+
return false;
|
|
197
|
+
const payload = c.payload ?? detached;
|
|
198
|
+
if (!payload)
|
|
199
|
+
return false;
|
|
200
|
+
return ed25519Verify(publicKey, SIG1(c.protectedBytes, payload), c.signature);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** The bytes a log entry is: the statement with its unprotected header emptied (RFC 9943 §6.3). */
|
|
207
|
+
export function statementEntry(statement) {
|
|
208
|
+
const c = parseCoseSign1(statement);
|
|
209
|
+
return new Uint8Array(cborEncode(tagged(18, [c.protectedBytes, new Map(), c.payload, c.signature])));
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------- RFC 9162 Merkle trees
|
|
212
|
+
export const leafHash = (entry) => sha256(Uint8Array.of(0), entry);
|
|
213
|
+
const nodeHash = (l, r) => sha256(Uint8Array.of(1), l, r);
|
|
214
|
+
const split = (n) => {
|
|
215
|
+
let k = 1;
|
|
216
|
+
while (k * 2 < n)
|
|
217
|
+
k *= 2;
|
|
218
|
+
return k;
|
|
219
|
+
};
|
|
220
|
+
/** MTH over leaf hashes (already `leafHash`ed). */
|
|
221
|
+
export function merkleRoot(leaves) {
|
|
222
|
+
if (leaves.length === 0)
|
|
223
|
+
return sha256();
|
|
224
|
+
if (leaves.length === 1)
|
|
225
|
+
return leaves[0];
|
|
226
|
+
const k = split(leaves.length);
|
|
227
|
+
return nodeHash(merkleRoot(leaves.slice(0, k)), merkleRoot(leaves.slice(k)));
|
|
228
|
+
}
|
|
229
|
+
/** The audit path for leaf `m` in a tree of these leaf hashes, leaf to root. */
|
|
230
|
+
export function inclusionProof(m, leaves) {
|
|
231
|
+
if (leaves.length <= 1)
|
|
232
|
+
return [];
|
|
233
|
+
const k = split(leaves.length);
|
|
234
|
+
return m < k
|
|
235
|
+
? [...inclusionProof(m, leaves.slice(0, k)), merkleRoot(leaves.slice(k))]
|
|
236
|
+
: [...inclusionProof(m - k, leaves.slice(k)), merkleRoot(leaves.slice(0, k))];
|
|
237
|
+
}
|
|
238
|
+
const eq = (a, b) => a.length === b.length && Buffer.compare(a, b) === 0;
|
|
239
|
+
/** RFC 9162 §2.1.3.2: does `path` prove leaf `index` (its hash) in a tree of `size` with `root`? */
|
|
240
|
+
export function verifyInclusion(leaf, index, size, path, root) {
|
|
241
|
+
return eq(rootFromInclusion(leaf, index, size, path) ?? new Uint8Array(), root);
|
|
242
|
+
}
|
|
243
|
+
function rootFromInclusion(leaf, index, size, path) {
|
|
244
|
+
if (index >= size || index < 0)
|
|
245
|
+
return null;
|
|
246
|
+
let fn = index;
|
|
247
|
+
let sn = size - 1;
|
|
248
|
+
let r = leaf;
|
|
249
|
+
for (const p of path) {
|
|
250
|
+
if (sn === 0)
|
|
251
|
+
return null;
|
|
252
|
+
if (fn & 1 || fn === sn) {
|
|
253
|
+
r = nodeHash(p, r);
|
|
254
|
+
if (!(fn & 1))
|
|
255
|
+
while (!(fn & 1) && fn !== 0) {
|
|
256
|
+
fn >>= 1;
|
|
257
|
+
sn >>= 1;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else
|
|
261
|
+
r = nodeHash(r, p);
|
|
262
|
+
fn >>= 1;
|
|
263
|
+
sn >>= 1;
|
|
264
|
+
}
|
|
265
|
+
return sn === 0 ? r : null;
|
|
266
|
+
}
|
|
267
|
+
/** RFC 9162 §2.1.4.1: proof that the tree of the first `m` leaves is a prefix of all of them. */
|
|
268
|
+
export function consistencyProof(m, leaves) {
|
|
269
|
+
const sub = (mm, d, b) => {
|
|
270
|
+
if (mm === d.length)
|
|
271
|
+
return b ? [] : [merkleRoot(d)];
|
|
272
|
+
const k = split(d.length);
|
|
273
|
+
return mm <= k
|
|
274
|
+
? [...sub(mm, d.slice(0, k), b), merkleRoot(d.slice(k))]
|
|
275
|
+
: [...sub(mm - k, d.slice(k), false), merkleRoot(d.slice(0, k))];
|
|
276
|
+
};
|
|
277
|
+
return m === 0 || m === leaves.length ? [] : sub(m, leaves, true);
|
|
278
|
+
}
|
|
279
|
+
/** RFC 9162 §2.1.4.2, with the trivial cases: an empty first tree, and equal sizes. */
|
|
280
|
+
export function verifyConsistency(first, second, firstRoot, secondRoot, proof) {
|
|
281
|
+
if (first === 0)
|
|
282
|
+
return proof.length === 0;
|
|
283
|
+
if (first === second)
|
|
284
|
+
return proof.length === 0 && eq(firstRoot, secondRoot);
|
|
285
|
+
if (first > second || proof.length === 0)
|
|
286
|
+
return false;
|
|
287
|
+
const path = (first & (first - 1)) === 0 ? [firstRoot, ...proof] : [...proof];
|
|
288
|
+
let fn = first - 1;
|
|
289
|
+
let sn = second - 1;
|
|
290
|
+
while (fn & 1) {
|
|
291
|
+
fn >>= 1;
|
|
292
|
+
sn >>= 1;
|
|
293
|
+
}
|
|
294
|
+
let fr = path[0];
|
|
295
|
+
let sr = path[0];
|
|
296
|
+
for (const c of path.slice(1)) {
|
|
297
|
+
if (sn === 0)
|
|
298
|
+
return false;
|
|
299
|
+
if (fn & 1 || fn === sn) {
|
|
300
|
+
fr = nodeHash(c, fr);
|
|
301
|
+
sr = nodeHash(c, sr);
|
|
302
|
+
if (!(fn & 1))
|
|
303
|
+
while (!(fn & 1) && fn !== 0) {
|
|
304
|
+
fn >>= 1;
|
|
305
|
+
sn >>= 1;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else
|
|
309
|
+
sr = nodeHash(sr, c);
|
|
310
|
+
fn >>= 1;
|
|
311
|
+
sn >>= 1;
|
|
312
|
+
}
|
|
313
|
+
return eq(fr, firstRoot) && eq(sr, secondRoot) && sn === 0;
|
|
314
|
+
}
|
|
315
|
+
/** A receipt of inclusion (vds RFC9162_SHA256): signed over the root, payload detached. */
|
|
316
|
+
export function makeReceipt(proof, signer) {
|
|
317
|
+
const protectedBytes = cborEncode(new Map([
|
|
318
|
+
[1, ALG_ED25519],
|
|
319
|
+
[4, signer.kid],
|
|
320
|
+
[
|
|
321
|
+
15,
|
|
322
|
+
new Map([
|
|
323
|
+
[1, signer.iss],
|
|
324
|
+
[2, signer.sub],
|
|
325
|
+
]),
|
|
326
|
+
],
|
|
327
|
+
[395, 1],
|
|
328
|
+
]));
|
|
329
|
+
const inclusion = cborEncode([proof.size, proof.index, [...proof.path]]);
|
|
330
|
+
const sig = ed25519Sign(signer.seed, SIG1(protectedBytes, proof.root));
|
|
331
|
+
const unprotected = new Map([[396, new Map([[-1, [inclusion]]])]]);
|
|
332
|
+
return new Uint8Array(cborEncode(tagged(18, [protectedBytes, unprotected, null, sig])));
|
|
333
|
+
}
|
|
334
|
+
/** RFC 9942 §5.2.1: does the receipt prove this entry (the statement, unprotected emptied) under this key?
|
|
335
|
+
* Returns the tree size and leaf index it proves, or null. Accepts a size-1 tree's empty path. */
|
|
336
|
+
export function verifyReceipt(receipt, entry, publicKey) {
|
|
337
|
+
try {
|
|
338
|
+
const c = parseCoseSign1(receipt);
|
|
339
|
+
if (c.protected.get(395) !== 1)
|
|
340
|
+
return null;
|
|
341
|
+
const vdp = c.unprotected.get(396);
|
|
342
|
+
const proofs = vdp instanceof Map ? vdp.get(-1) : undefined;
|
|
343
|
+
if (!Array.isArray(proofs) || proofs.length < 1)
|
|
344
|
+
return null;
|
|
345
|
+
for (const bytes of proofs) {
|
|
346
|
+
if (!(bytes instanceof Uint8Array))
|
|
347
|
+
continue;
|
|
348
|
+
const p = cborDecode(bytes);
|
|
349
|
+
if (!Array.isArray(p) || p.length !== 3)
|
|
350
|
+
continue;
|
|
351
|
+
const [size, index, path] = p;
|
|
352
|
+
if (typeof size !== "number" || typeof index !== "number" || !Array.isArray(path))
|
|
353
|
+
continue;
|
|
354
|
+
if (!path.every((x) => x instanceof Uint8Array && x.length === 32))
|
|
355
|
+
continue;
|
|
356
|
+
const root = rootFromInclusion(leafHash(entry), index, size, path);
|
|
357
|
+
if (root && verifyCoseSign1(receipt, publicKey, root))
|
|
358
|
+
return { size, index };
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/** A Transparent Statement: the statement with its receipts in unprotected header 394. */
|
|
367
|
+
export function transparentStatement(statement, receipts) {
|
|
368
|
+
const c = parseCoseSign1(statement);
|
|
369
|
+
const unprotected = new Map([[394, [...receipts]]]);
|
|
370
|
+
return new Uint8Array(cborEncode(tagged(18, [c.protectedBytes, unprotected, c.payload, c.signature])));
|
|
371
|
+
}
|
|
372
|
+
// ---------------------------------------------------------------- C2SP signed notes, checkpoints, cosignatures
|
|
373
|
+
const EM = "—";
|
|
374
|
+
const b64 = (b) => Buffer.from(b).toString("base64");
|
|
375
|
+
const unb64 = (s) => new Uint8Array(Buffer.from(s, "base64"));
|
|
376
|
+
/** c2sp.org/signed-note: SHA-256(name || "\n" || type || public key)[:4]. Type 0x01 note, 0x04 cosignature. */
|
|
377
|
+
export const noteKeyId = (name, type, publicKey) => sha256(Buffer.from(`${name}\n`, "utf8"), Uint8Array.of(type), publicKey).subarray(0, 4);
|
|
378
|
+
/** A verifier key string: `name+hex(keyID)+base64(type || public key)`. */
|
|
379
|
+
export const noteVkey = (name, type, publicKey) => `${name}+${Buffer.from(noteKeyId(name, type, publicKey)).toString("hex")}+${b64(Buffer.concat([Uint8Array.of(type), publicKey]))}`;
|
|
380
|
+
export function parseVkey(vkey) {
|
|
381
|
+
// name+keyid+key: the base64 key may itself hold "+", so only the first two separate
|
|
382
|
+
const a = vkey.indexOf("+");
|
|
383
|
+
const b = vkey.indexOf("+", a + 1);
|
|
384
|
+
if (a < 1 || b < 0)
|
|
385
|
+
return null;
|
|
386
|
+
const name = vkey.slice(0, a);
|
|
387
|
+
const hexId = vkey.slice(a + 1, b);
|
|
388
|
+
const key = vkey.slice(b + 1);
|
|
389
|
+
const raw = unb64(key);
|
|
390
|
+
const type = raw[0];
|
|
391
|
+
const size = type === 6 ? 1 + MLDSA44_PUBLIC : 33;
|
|
392
|
+
if (!name || !/^[0-9a-f]{8}$/.test(hexId) || raw.length !== size)
|
|
393
|
+
return null;
|
|
394
|
+
const publicKey = raw.subarray(1);
|
|
395
|
+
if (Buffer.from(noteKeyId(name, type, publicKey)).toString("hex") !== hexId)
|
|
396
|
+
return null;
|
|
397
|
+
return { name, type, keyId: noteKeyId(name, type, publicKey), publicKey };
|
|
398
|
+
}
|
|
399
|
+
/** Signs a note's text (which ends in "\n") as `name`; returns the note with its signature line. */
|
|
400
|
+
export function signNote(text, name, seed) {
|
|
401
|
+
if (!text.endsWith("\n"))
|
|
402
|
+
throw new Error("a note's text ends in a newline");
|
|
403
|
+
const id = noteKeyId(name, 1, ed25519PublicKey(seed));
|
|
404
|
+
const sig = ed25519Sign(seed, Buffer.from(text, "utf8"));
|
|
405
|
+
return `${text}\n${EM} ${name} ${b64(Buffer.concat([id, sig]))}\n`;
|
|
406
|
+
}
|
|
407
|
+
/** Splits a signed note into its text and signature lines. */
|
|
408
|
+
export function splitNote(note) {
|
|
409
|
+
const i = note.lastIndexOf("\n\n");
|
|
410
|
+
if (i < 0)
|
|
411
|
+
return null;
|
|
412
|
+
const text = note.slice(0, i + 1);
|
|
413
|
+
const signatures = [];
|
|
414
|
+
for (const line of note.slice(i + 2).split("\n")) {
|
|
415
|
+
if (line === "")
|
|
416
|
+
continue;
|
|
417
|
+
const m = /^— (\S+) (\S+)$/.exec(line);
|
|
418
|
+
if (!m)
|
|
419
|
+
return null;
|
|
420
|
+
const raw = unb64(m[2]);
|
|
421
|
+
if (raw.length < 5)
|
|
422
|
+
return null;
|
|
423
|
+
signatures.push({ name: m[1], keyId: raw.subarray(0, 4), sig: raw.subarray(4) });
|
|
424
|
+
}
|
|
425
|
+
return { text, signatures };
|
|
426
|
+
}
|
|
427
|
+
/** Is the note signed (type 0x01) by this vkey? */
|
|
428
|
+
export function verifyNote(note, vkey) {
|
|
429
|
+
const k = parseVkey(vkey);
|
|
430
|
+
const n = splitNote(note);
|
|
431
|
+
if (!k || !n || k.type !== 1)
|
|
432
|
+
return false;
|
|
433
|
+
return n.signatures.some((s) => s.name === k.name &&
|
|
434
|
+
eq(s.keyId, k.keyId) &&
|
|
435
|
+
ed25519Verify(k.publicKey, Buffer.from(n.text, "utf8"), s.sig));
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* §9: a log key rotation, as a note signed by the old key and the new one: the new key's vkey, the
|
|
439
|
+
* tree size it takes over at, and when. A verifier that pinned the old key follows it to the new.
|
|
440
|
+
*/
|
|
441
|
+
export function keyRotationNote(old, next, size, time) {
|
|
442
|
+
const oldVkey = noteVkey(old.origin, 1, ed25519PublicKey(old.seed));
|
|
443
|
+
const newVkey = noteVkey(next.origin, 1, ed25519PublicKey(next.seed));
|
|
444
|
+
const text = `zanii-blackbox/log-key-rotation/v1\nold ${oldVkey}\nnew ${newVkey}\nsize ${size}\ntime ${time}\n`;
|
|
445
|
+
const signed = signNote(text, old.origin, old.seed);
|
|
446
|
+
return signed + signNote(text, next.origin, next.seed).slice(text.length + 1);
|
|
447
|
+
}
|
|
448
|
+
/** §9: the new key, size and time a rotation note names, if the old key and the new one both signed it. */
|
|
449
|
+
export function verifyKeyRotation(note, oldVkey) {
|
|
450
|
+
const n = splitNote(note);
|
|
451
|
+
const m = n
|
|
452
|
+
? /^zanii-blackbox\/log-key-rotation\/v1\nold (\S+)\nnew (\S+)\nsize (0|[1-9][0-9]{0,15})\ntime (0|[1-9][0-9]{0,15})\n$/.exec(n.text)
|
|
453
|
+
: null;
|
|
454
|
+
if (!m || m[1] !== oldVkey)
|
|
455
|
+
return null;
|
|
456
|
+
const newVkey = m[2];
|
|
457
|
+
if (parseVkey(newVkey)?.type !== 1 || !verifyNote(note, oldVkey) || !verifyNote(note, newVkey))
|
|
458
|
+
return null;
|
|
459
|
+
return { new_vkey: newVkey, size: Number(m[3]), time: Number(m[4]) };
|
|
460
|
+
}
|
|
461
|
+
/** c2sp.org/tlog-checkpoint: origin, size, base64 root, each on its own line. */
|
|
462
|
+
export const checkpointText = (origin, size, root) => `${origin}\n${size}\n${b64(root)}\n`;
|
|
463
|
+
export function parseCheckpoint(text) {
|
|
464
|
+
const lines = text.split("\n");
|
|
465
|
+
if (lines.length < 4 || lines.at(-1) !== "")
|
|
466
|
+
return null;
|
|
467
|
+
const [origin, size, root] = lines;
|
|
468
|
+
if (!origin || !/^(0|[1-9][0-9]{0,15})$/.test(size))
|
|
469
|
+
return null;
|
|
470
|
+
const r = unb64(root);
|
|
471
|
+
if (r.length !== 32 || b64(r) !== root)
|
|
472
|
+
return null;
|
|
473
|
+
return { origin, size: Number(size), root: r };
|
|
474
|
+
}
|
|
475
|
+
/** c2sp.org/tlog-cosignature: the witness's timestamped Ed25519 cosignature (type 0x04) on a checkpoint. */
|
|
476
|
+
export function cosign(checkpoint, name, seed, time) {
|
|
477
|
+
const id = noteKeyId(name, 4, ed25519PublicKey(seed));
|
|
478
|
+
const ts = Buffer.alloc(8);
|
|
479
|
+
ts.writeBigUInt64BE(BigInt(time));
|
|
480
|
+
const sig = ed25519Sign(seed, Buffer.from(`cosignature/v1\ntime ${time}\n${checkpoint}`, "utf8"));
|
|
481
|
+
return `${EM} ${name} ${b64(Buffer.concat([id, ts, sig]))}\n`;
|
|
482
|
+
}
|
|
483
|
+
// ---------------------------------------------------------------- ML-DSA-44 (FIPS 204, pure, empty context)
|
|
484
|
+
const MLDSA44_PUBLIC = 1312;
|
|
485
|
+
const MLDSA44_SIG = 2420;
|
|
486
|
+
const MLDSA44_PKCS8 = Buffer.from("3034020100300b060960864801650304031104228020", "hex");
|
|
487
|
+
const MLDSA44_SPKI = Buffer.from("30820532300b06096086480165030403110382052100", "hex");
|
|
488
|
+
const mldsaKey = (seed) => {
|
|
489
|
+
if (seed.length !== 32)
|
|
490
|
+
throw new Error("an ML-DSA-44 seed is 32 bytes");
|
|
491
|
+
return createPrivateKey({
|
|
492
|
+
key: Buffer.concat([MLDSA44_PKCS8, seed]),
|
|
493
|
+
format: "der",
|
|
494
|
+
type: "pkcs8",
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
/** The raw ML-DSA-44 public key (1312 bytes) for a 32-byte seed. Needs Node ≥ 24.6. */
|
|
498
|
+
export const mldsaPublicKey = (seed) => new Uint8Array(createPublicKey(mldsaKey(seed))
|
|
499
|
+
.export({ format: "der", type: "spki" })
|
|
500
|
+
.subarray(MLDSA44_SPKI.length));
|
|
501
|
+
/** Whether this runtime has ML-DSA (Node ≥ 24.6 with OpenSSL ≥ 3.5). */
|
|
502
|
+
export function mldsaAvailable() {
|
|
503
|
+
try {
|
|
504
|
+
mldsaPublicKey(new Uint8Array(32));
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const mldsaVerify = (publicKey, msg, sig) => {
|
|
512
|
+
try {
|
|
513
|
+
const key = createPublicKey({
|
|
514
|
+
key: Buffer.concat([MLDSA44_SPKI, publicKey]),
|
|
515
|
+
format: "der",
|
|
516
|
+
type: "spki",
|
|
517
|
+
});
|
|
518
|
+
return verify(null, msg, key, sig);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
/** c2sp.org/tlog-cosignature v1.1 (type 0x06): what an ML-DSA-44 subtree cosignature signs. */
|
|
525
|
+
export function subtreeMessage(name, time, origin, start, end, hash) {
|
|
526
|
+
const n = Buffer.from(name, "utf8");
|
|
527
|
+
const o = Buffer.from(origin, "utf8");
|
|
528
|
+
if (n.length > 255 || o.length > 255)
|
|
529
|
+
throw new Error("names and origins are at most 255 bytes");
|
|
530
|
+
const u64 = (v) => {
|
|
531
|
+
const b = Buffer.alloc(8);
|
|
532
|
+
b.writeBigUInt64BE(BigInt(v));
|
|
533
|
+
return b;
|
|
534
|
+
};
|
|
535
|
+
return new Uint8Array(Buffer.concat([
|
|
536
|
+
Buffer.from("subtree/v1\n\0", "latin1"),
|
|
537
|
+
Uint8Array.of(n.length),
|
|
538
|
+
n,
|
|
539
|
+
u64(time),
|
|
540
|
+
Uint8Array.of(o.length),
|
|
541
|
+
o,
|
|
542
|
+
u64(start),
|
|
543
|
+
u64(end),
|
|
544
|
+
hash,
|
|
545
|
+
]));
|
|
546
|
+
}
|
|
547
|
+
/** An ML-DSA-44 cosignature line (type 0x06) on a checkpoint: what a log or witness adds for post-quantum. */
|
|
548
|
+
export function cosignMldsa(checkpoint, name, seed, time) {
|
|
549
|
+
const cp = parseCheckpoint(checkpoint);
|
|
550
|
+
if (!cp)
|
|
551
|
+
throw new Error("not a checkpoint");
|
|
552
|
+
const id = noteKeyId(name, 6, mldsaPublicKey(seed));
|
|
553
|
+
const ts = Buffer.alloc(8);
|
|
554
|
+
ts.writeBigUInt64BE(BigInt(time));
|
|
555
|
+
const sig = sign(null, subtreeMessage(name, time, cp.origin, 0, cp.size, cp.root), mldsaKey(seed));
|
|
556
|
+
return `${EM} ${name} ${b64(Buffer.concat([id, ts, sig]))}\n`;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* The time a cosignature (in a note's signature lines) says it saw the checkpoint, or null. Takes a
|
|
560
|
+
* witness's Ed25519 vkey (type 0x04) or an ML-DSA-44 one (type 0x06).
|
|
561
|
+
*/
|
|
562
|
+
export function verifyCosignature(note, vkey) {
|
|
563
|
+
const k = parseVkey(vkey);
|
|
564
|
+
const n = splitNote(note);
|
|
565
|
+
if (!k || !n || (k.type !== 4 && k.type !== 6))
|
|
566
|
+
return null;
|
|
567
|
+
if (k.type === 6) {
|
|
568
|
+
const cp = parseCheckpoint(n.text);
|
|
569
|
+
if (!cp)
|
|
570
|
+
return null;
|
|
571
|
+
for (const s of n.signatures) {
|
|
572
|
+
if (s.name !== k.name || !eq(s.keyId, k.keyId) || s.sig.length !== 8 + MLDSA44_SIG)
|
|
573
|
+
continue;
|
|
574
|
+
const time = Number(Buffer.from(s.sig.subarray(0, 8)).readBigUInt64BE());
|
|
575
|
+
const msg = subtreeMessage(k.name, time, cp.origin, 0, cp.size, cp.root);
|
|
576
|
+
if (mldsaVerify(k.publicKey, msg, s.sig.subarray(8)))
|
|
577
|
+
return time;
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
for (const s of n.signatures) {
|
|
582
|
+
if (s.name !== k.name || !eq(s.keyId, k.keyId) || s.sig.length !== 72)
|
|
583
|
+
continue;
|
|
584
|
+
const time = Number(Buffer.from(s.sig.subarray(0, 8)).readBigUInt64BE());
|
|
585
|
+
const msg = Buffer.from(`cosignature/v1\ntime ${time}\n${n.text}`, "utf8");
|
|
586
|
+
if (ed25519Verify(k.publicKey, msg, s.sig.subarray(8)))
|
|
587
|
+
return time;
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
/** c2sp.org/tlog-witness: the body of `POST add-checkpoint`. */
|
|
592
|
+
export const addCheckpointBody = (old, proof, signedCheckpoint) => `old ${old}\n${proof.map((p) => `${b64(p)}\n`).join("")}\n${signedCheckpoint}`;
|
|
593
|
+
export function parseAddCheckpoint(body) {
|
|
594
|
+
const i = body.indexOf("\n\n");
|
|
595
|
+
if (i < 0)
|
|
596
|
+
return null;
|
|
597
|
+
const lines = body.slice(0, i).split("\n");
|
|
598
|
+
const m = /^old (0|[1-9][0-9]{0,15})$/.exec(lines[0] ?? "");
|
|
599
|
+
if (!m || lines.length - 1 > 63)
|
|
600
|
+
return null;
|
|
601
|
+
const proof = lines.slice(1).map(unb64);
|
|
602
|
+
if (proof.some((p) => p.length !== 32))
|
|
603
|
+
return null;
|
|
604
|
+
return { old: Number(m[1]), proof, note: body.slice(i + 2) };
|
|
605
|
+
}
|
|
606
|
+
// ---------------------------------------------------------------- the gateway's log (spec/transparency.md)
|
|
607
|
+
export const SESSION_CONTENT_TYPE = "application/vnd.zanii.blackbox.session+json";
|
|
608
|
+
export const RECEIPT_SUB = "transparency-log";
|
|
609
|
+
/** spec/transparency.md §1: the first 8 bytes of SHA-256(public key). */
|
|
610
|
+
export const logKid = (publicKey) => sha256(publicKey).subarray(0, 8);
|
|
611
|
+
/** spec/transparency.md §2: the Signed Statement a closed session gets. `iat` is `closed_at` in seconds. */
|
|
612
|
+
export function sessionStatement(head, origin, seed) {
|
|
613
|
+
const payload = canonicalBytes({ v: 1, ...head });
|
|
614
|
+
return signStatement({
|
|
615
|
+
contentType: SESSION_CONTENT_TYPE,
|
|
616
|
+
payload: new Uint8Array(payload),
|
|
617
|
+
iss: origin,
|
|
618
|
+
sub: head.session_id,
|
|
619
|
+
iat: Math.floor(Date.parse(head.closed_at) / 1000),
|
|
620
|
+
kid: logKid(ed25519PublicKey(seed)),
|
|
621
|
+
}, seed);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* spec/transparency.md §6: checks a bundle's `transparency` offline, against a log key the verifier
|
|
625
|
+
* pins (a C2SP vkey, type 0x01) and the witnesses it trusts (vkeys, type 0x04).
|
|
626
|
+
*/
|
|
627
|
+
export function verifyTransparency(bundle, options) {
|
|
628
|
+
const report = {
|
|
629
|
+
ok: false,
|
|
630
|
+
statement: false,
|
|
631
|
+
receipt: null,
|
|
632
|
+
checkpoint: null,
|
|
633
|
+
witnessed: [],
|
|
634
|
+
timestamp: null,
|
|
635
|
+
pq: null,
|
|
636
|
+
problems: [],
|
|
637
|
+
};
|
|
638
|
+
const t = bundle.transparency;
|
|
639
|
+
const key = parseVkey(options.logKey);
|
|
640
|
+
if (!key || key.type !== 1)
|
|
641
|
+
return { ...report, problems: ["the log key isn't a C2SP note key (type 0x01)"] };
|
|
642
|
+
if (!t || typeof t.statement !== "string")
|
|
643
|
+
return { ...report, problems: ["the bundle has no transparency statement"] };
|
|
644
|
+
const statement = unb64(t.statement);
|
|
645
|
+
if (!verifyCoseSign1(statement, key.publicKey))
|
|
646
|
+
report.problems.push("the statement doesn't verify under the log key");
|
|
647
|
+
else {
|
|
648
|
+
const payload = JSON.parse(Buffer.from(parseCoseSign1(statement).payload ?? new Uint8Array()).toString("utf8"));
|
|
649
|
+
const last = bundle.lines.at(-1);
|
|
650
|
+
if (payload.session_id !== bundle.session.session_id)
|
|
651
|
+
report.problems.push("the statement is about another session");
|
|
652
|
+
else if (payload.count !== bundle.lines.length ||
|
|
653
|
+
last === undefined ||
|
|
654
|
+
payload.head !== hashLine(last))
|
|
655
|
+
report.problems.push("the statement's count and head aren't this record's");
|
|
656
|
+
else
|
|
657
|
+
report.statement = true;
|
|
658
|
+
}
|
|
659
|
+
if (typeof t.checkpoint === "string") {
|
|
660
|
+
const text = splitNote(t.checkpoint)?.text;
|
|
661
|
+
const cp = text ? parseCheckpoint(text) : null;
|
|
662
|
+
if (!cp || !verifyNote(t.checkpoint, options.logKey))
|
|
663
|
+
report.problems.push("the checkpoint isn't signed by the log key");
|
|
664
|
+
else {
|
|
665
|
+
report.checkpoint = { origin: cp.origin, size: cp.size };
|
|
666
|
+
report.witnessed = (options.witnesses ?? []).filter((w) => verifyCosignature(t.checkpoint, w) !== null);
|
|
667
|
+
if (options.logPqKey !== undefined) {
|
|
668
|
+
report.pq = verifyCosignature(t.checkpoint, options.logPqKey) !== null;
|
|
669
|
+
if (!report.pq)
|
|
670
|
+
report.problems.push("the checkpoint has no ML-DSA-44 signature from the log");
|
|
671
|
+
}
|
|
672
|
+
if (typeof t.timestamp === "string") {
|
|
673
|
+
const digest = new Uint8Array(createHash("sha256")
|
|
674
|
+
.update(text)
|
|
675
|
+
.digest());
|
|
676
|
+
const ts = verifyTimestamp(unb64(t.timestamp), {
|
|
677
|
+
digest,
|
|
678
|
+
...(options.tsaRoots ? { roots: options.tsaRoots } : {}),
|
|
679
|
+
});
|
|
680
|
+
if (ts.ok)
|
|
681
|
+
report.timestamp = ts.gen_time;
|
|
682
|
+
else
|
|
683
|
+
report.problems.push(`the checkpoint's timestamp doesn't verify: ${ts.problems.join("; ")}`);
|
|
684
|
+
}
|
|
685
|
+
if (typeof t.receipt === "string") {
|
|
686
|
+
const proved = verifyReceipt(unb64(t.receipt), statementEntry(statement), key.publicKey);
|
|
687
|
+
const root = proved ? receiptRoot(unb64(t.receipt), statementEntry(statement)) : null;
|
|
688
|
+
if (!proved)
|
|
689
|
+
report.problems.push("the receipt doesn't prove this statement");
|
|
690
|
+
else if (proved.size !== cp.size || !root || !eq(root, cp.root))
|
|
691
|
+
report.problems.push("the receipt isn't for the checkpoint's tree");
|
|
692
|
+
else
|
|
693
|
+
report.receipt = proved;
|
|
694
|
+
}
|
|
695
|
+
else
|
|
696
|
+
report.problems.push("the bundle has no receipt yet");
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else
|
|
700
|
+
report.problems.push("the bundle has no checkpoint yet");
|
|
701
|
+
report.ok = report.problems.length === 0;
|
|
702
|
+
return report;
|
|
703
|
+
}
|
|
704
|
+
/** The root a receipt's first inclusion proof gives for this entry. */
|
|
705
|
+
function receiptRoot(receipt, entry) {
|
|
706
|
+
const vdp = parseCoseSign1(receipt).unprotected.get(396);
|
|
707
|
+
const first = vdp instanceof Map ? vdp.get(-1)?.[0] : undefined;
|
|
708
|
+
if (!(first instanceof Uint8Array))
|
|
709
|
+
return null;
|
|
710
|
+
const [size, index, path] = cborDecode(first);
|
|
711
|
+
return rootFromInclusion(leafHash(entry), index, size, path);
|
|
712
|
+
}
|