protect-mcp 0.7.3 → 0.7.5
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 +48 -0
- package/README.md +155 -7
- package/dist/{bundle-XTR3YMPO.mjs → bundle-YUUHAG7O.mjs} +1 -1
- package/dist/{chunk-G67NUBML.mjs → chunk-6E2DHBAR.mjs} +21 -6
- package/dist/chunk-JCMDLN5I.mjs +1053 -0
- package/dist/{chunk-LYKNULYU.mjs → chunk-LJQOALYR.mjs} +152 -0
- package/dist/{chunk-5JXFV37Y.mjs → chunk-PM2ZO57M.mjs} +10 -2
- package/dist/{chunk-J6L4XCTE.mjs → chunk-SETXVE2K.mjs} +77 -2
- package/dist/{chunk-OHUTUFTC.mjs → chunk-VTPZ4G5I.mjs} +19 -15
- package/dist/{chunk-546U3A7R.mjs → chunk-WIPWNWMJ.mjs} +93 -2
- package/dist/chunk-WV4DKYE4.mjs +486 -0
- package/dist/cli.js +4609 -546
- package/dist/cli.mjs +2215 -40
- package/dist/demo-server.d.mts +3 -0
- package/dist/demo-server.d.ts +3 -0
- package/dist/demo-server.js +77 -2
- package/dist/demo-server.mjs +1 -1
- package/dist/{ed25519-DZMMNNVE.mjs → ed25519-BSHMMVNX.mjs} +1 -1
- package/dist/hook-server.js +128 -24
- package/dist/hook-server.mjs +2 -2
- package/dist/{http-transport-R5AO7X6D.mjs → http-transport-JBORN27G.mjs} +3 -3
- package/dist/index.d.mts +158 -10
- package/dist/index.d.ts +158 -10
- package/dist/index.js +2704 -108
- package/dist/index.mjs +1208 -261
- package/dist/receipt-registry-6CAOY6RP.mjs +279 -0
- package/dist/signing-committed-QXCW24RF.mjs +15 -0
- package/package.json +2 -2
- package/dist/chunk-ZBKJANP7.mjs +0 -145
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ed25519,
|
|
3
|
+
sha256
|
|
4
|
+
} from "./chunk-LJQOALYR.mjs";
|
|
5
|
+
import {
|
|
6
|
+
Hash,
|
|
7
|
+
abytes,
|
|
8
|
+
aexists,
|
|
9
|
+
ahash,
|
|
10
|
+
bytesToHex,
|
|
11
|
+
clean,
|
|
12
|
+
hexToBytes,
|
|
13
|
+
randomBytes,
|
|
14
|
+
toBytes
|
|
15
|
+
} from "./chunk-D733KAPG.mjs";
|
|
16
|
+
|
|
17
|
+
// node_modules/@noble/hashes/esm/sha256.js
|
|
18
|
+
var sha2562 = sha256;
|
|
19
|
+
|
|
20
|
+
// src/commitments/merkle.ts
|
|
21
|
+
var DOMAIN_LEAF = 0;
|
|
22
|
+
var DOMAIN_INTERNAL = 1;
|
|
23
|
+
function hashLeaf(leafBytes) {
|
|
24
|
+
const buf = new Uint8Array(leafBytes.length + 1);
|
|
25
|
+
buf[0] = DOMAIN_LEAF;
|
|
26
|
+
buf.set(leafBytes, 1);
|
|
27
|
+
return sha2562(buf);
|
|
28
|
+
}
|
|
29
|
+
function hashInternal(left, right) {
|
|
30
|
+
const buf = new Uint8Array(left.length + right.length + 1);
|
|
31
|
+
buf[0] = DOMAIN_INTERNAL;
|
|
32
|
+
buf.set(left, 1);
|
|
33
|
+
buf.set(right, 1 + left.length);
|
|
34
|
+
return sha2562(buf);
|
|
35
|
+
}
|
|
36
|
+
function merkleRoot(leafHashes) {
|
|
37
|
+
if (leafHashes.length === 0) {
|
|
38
|
+
throw new Error("merkleRoot: cannot compute root of empty leaf set");
|
|
39
|
+
}
|
|
40
|
+
if (leafHashes.length === 1) {
|
|
41
|
+
return leafHashes[0];
|
|
42
|
+
}
|
|
43
|
+
const n = leafHashes.length;
|
|
44
|
+
const k = largestPowerOfTwoLessThan(n);
|
|
45
|
+
const left = merkleRoot(leafHashes.slice(0, k));
|
|
46
|
+
const right = merkleRoot(leafHashes.slice(k));
|
|
47
|
+
return hashInternal(left, right);
|
|
48
|
+
}
|
|
49
|
+
function generateProof(leafHashes, index) {
|
|
50
|
+
if (leafHashes.length === 0) {
|
|
51
|
+
throw new Error("generateProof: empty tree");
|
|
52
|
+
}
|
|
53
|
+
if (index < 0 || index >= leafHashes.length) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`generateProof: index ${index} out of range [0, ${leafHashes.length})`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const siblings = [];
|
|
59
|
+
collectPath(leafHashes, index, siblings);
|
|
60
|
+
return {
|
|
61
|
+
index,
|
|
62
|
+
treeSize: leafHashes.length,
|
|
63
|
+
siblings: siblings.map((s) => bytesToHex(s))
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function collectPath(leaves, index, out) {
|
|
67
|
+
if (leaves.length === 1) return;
|
|
68
|
+
const n = leaves.length;
|
|
69
|
+
const k = largestPowerOfTwoLessThan(n);
|
|
70
|
+
if (index < k) {
|
|
71
|
+
collectPath(leaves.slice(0, k), index, out);
|
|
72
|
+
out.push(merkleRoot(leaves.slice(k)));
|
|
73
|
+
} else {
|
|
74
|
+
collectPath(leaves.slice(k), index - k, out);
|
|
75
|
+
out.push(merkleRoot(leaves.slice(0, k)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function verifyProof(expectedRootHex, leafHash, proof) {
|
|
79
|
+
if (proof.index < 0 || proof.index >= proof.treeSize) return false;
|
|
80
|
+
if (proof.treeSize === 1) {
|
|
81
|
+
return proof.siblings.length === 0 && bytesToHex(leafHash).toLowerCase() === expectedRootHex.toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
let result;
|
|
84
|
+
try {
|
|
85
|
+
result = reconstructRoot(
|
|
86
|
+
leafHash,
|
|
87
|
+
proof.index,
|
|
88
|
+
proof.treeSize,
|
|
89
|
+
proof.siblings
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return bytesToHex(result).toLowerCase() === expectedRootHex.toLowerCase();
|
|
95
|
+
}
|
|
96
|
+
function reconstructRoot(leafHash, index, treeSize, siblings) {
|
|
97
|
+
if (treeSize === 1) {
|
|
98
|
+
if (siblings.length !== 0) {
|
|
99
|
+
throw new Error("reconstructRoot: extra siblings at single-leaf level");
|
|
100
|
+
}
|
|
101
|
+
return leafHash;
|
|
102
|
+
}
|
|
103
|
+
if (siblings.length === 0) {
|
|
104
|
+
throw new Error("reconstructRoot: ran out of siblings before single-leaf");
|
|
105
|
+
}
|
|
106
|
+
const k = largestPowerOfTwoLessThan(treeSize);
|
|
107
|
+
const outermostSibling = hexToBytes(siblings[siblings.length - 1]);
|
|
108
|
+
const innerSiblings = siblings.slice(0, -1);
|
|
109
|
+
if (index < k) {
|
|
110
|
+
const leftHash = reconstructRoot(leafHash, index, k, innerSiblings);
|
|
111
|
+
return hashInternal(leftHash, outermostSibling);
|
|
112
|
+
} else {
|
|
113
|
+
const rightHash = reconstructRoot(
|
|
114
|
+
leafHash,
|
|
115
|
+
index - k,
|
|
116
|
+
treeSize - k,
|
|
117
|
+
innerSiblings
|
|
118
|
+
);
|
|
119
|
+
return hashInternal(outermostSibling, rightHash);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function largestPowerOfTwoLessThan(n) {
|
|
123
|
+
if (n < 2) {
|
|
124
|
+
throw new Error(`largestPowerOfTwoLessThan: n must be >= 2 (got ${n})`);
|
|
125
|
+
}
|
|
126
|
+
let k = 1;
|
|
127
|
+
while (k * 2 < n) k *= 2;
|
|
128
|
+
return k;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// node_modules/@noble/hashes/esm/hmac.js
|
|
132
|
+
var HMAC = class extends Hash {
|
|
133
|
+
constructor(hash, _key) {
|
|
134
|
+
super();
|
|
135
|
+
this.finished = false;
|
|
136
|
+
this.destroyed = false;
|
|
137
|
+
ahash(hash);
|
|
138
|
+
const key = toBytes(_key);
|
|
139
|
+
this.iHash = hash.create();
|
|
140
|
+
if (typeof this.iHash.update !== "function")
|
|
141
|
+
throw new Error("Expected instance of class which extends utils.Hash");
|
|
142
|
+
this.blockLen = this.iHash.blockLen;
|
|
143
|
+
this.outputLen = this.iHash.outputLen;
|
|
144
|
+
const blockLen = this.blockLen;
|
|
145
|
+
const pad = new Uint8Array(blockLen);
|
|
146
|
+
pad.set(key.length > blockLen ? hash.create().update(key).digest() : key);
|
|
147
|
+
for (let i = 0; i < pad.length; i++)
|
|
148
|
+
pad[i] ^= 54;
|
|
149
|
+
this.iHash.update(pad);
|
|
150
|
+
this.oHash = hash.create();
|
|
151
|
+
for (let i = 0; i < pad.length; i++)
|
|
152
|
+
pad[i] ^= 54 ^ 92;
|
|
153
|
+
this.oHash.update(pad);
|
|
154
|
+
clean(pad);
|
|
155
|
+
}
|
|
156
|
+
update(buf) {
|
|
157
|
+
aexists(this);
|
|
158
|
+
this.iHash.update(buf);
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
digestInto(out) {
|
|
162
|
+
aexists(this);
|
|
163
|
+
abytes(out, this.outputLen);
|
|
164
|
+
this.finished = true;
|
|
165
|
+
this.iHash.digestInto(out);
|
|
166
|
+
this.oHash.update(out);
|
|
167
|
+
this.oHash.digestInto(out);
|
|
168
|
+
this.destroy();
|
|
169
|
+
}
|
|
170
|
+
digest() {
|
|
171
|
+
const out = new Uint8Array(this.oHash.outputLen);
|
|
172
|
+
this.digestInto(out);
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
_cloneInto(to) {
|
|
176
|
+
to || (to = Object.create(Object.getPrototypeOf(this), {}));
|
|
177
|
+
const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this;
|
|
178
|
+
to = to;
|
|
179
|
+
to.finished = finished;
|
|
180
|
+
to.destroyed = destroyed;
|
|
181
|
+
to.blockLen = blockLen;
|
|
182
|
+
to.outputLen = outputLen;
|
|
183
|
+
to.oHash = oHash._cloneInto(to.oHash);
|
|
184
|
+
to.iHash = iHash._cloneInto(to.iHash);
|
|
185
|
+
return to;
|
|
186
|
+
}
|
|
187
|
+
clone() {
|
|
188
|
+
return this._cloneInto();
|
|
189
|
+
}
|
|
190
|
+
destroy() {
|
|
191
|
+
this.destroyed = true;
|
|
192
|
+
this.oHash.destroy();
|
|
193
|
+
this.iHash.destroy();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var hmac = (hash, key, message) => new HMAC(hash, key).update(message).digest();
|
|
197
|
+
hmac.create = (hash, key) => new HMAC(hash, key);
|
|
198
|
+
|
|
199
|
+
// src/commitments/primitives.ts
|
|
200
|
+
function jcs(value) {
|
|
201
|
+
if (value === null || value === void 0) return "null";
|
|
202
|
+
if (typeof value === "boolean" || typeof value === "number")
|
|
203
|
+
return JSON.stringify(value);
|
|
204
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
205
|
+
if (Array.isArray(value))
|
|
206
|
+
return "[" + value.map(jcs).join(",") + "]";
|
|
207
|
+
const obj = value;
|
|
208
|
+
const keys = Object.keys(obj).sort();
|
|
209
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + jcs(obj[k])).join(",") + "}";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/commitments/leaf.ts
|
|
213
|
+
function base64urlNoPad(bytes) {
|
|
214
|
+
const std = typeof Buffer !== "undefined" ? Buffer.from(bytes).toString("base64") : btoa(String.fromCharCode(...bytes));
|
|
215
|
+
return std.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
216
|
+
}
|
|
217
|
+
function base64urlDecode(s) {
|
|
218
|
+
const std = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
219
|
+
const padded = std + "=".repeat((4 - std.length % 4) % 4);
|
|
220
|
+
if (typeof Buffer !== "undefined") {
|
|
221
|
+
return new Uint8Array(Buffer.from(padded, "base64"));
|
|
222
|
+
}
|
|
223
|
+
const bin = atob(padded);
|
|
224
|
+
const out = new Uint8Array(bin.length);
|
|
225
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
function encodeLeaf(field) {
|
|
229
|
+
const obj = {
|
|
230
|
+
name: field.name,
|
|
231
|
+
salt: base64urlNoPad(field.salt),
|
|
232
|
+
value: field.value
|
|
233
|
+
};
|
|
234
|
+
const canonical = jcs(obj);
|
|
235
|
+
return new TextEncoder().encode(canonical);
|
|
236
|
+
}
|
|
237
|
+
function sortFields(fields) {
|
|
238
|
+
const encoder = new TextEncoder();
|
|
239
|
+
const decorated = fields.map((f) => ({
|
|
240
|
+
field: f,
|
|
241
|
+
nameBytes: encoder.encode(f.name)
|
|
242
|
+
}));
|
|
243
|
+
decorated.sort((a, b) => compareBytes(a.nameBytes, b.nameBytes));
|
|
244
|
+
return decorated.map((d) => d.field);
|
|
245
|
+
}
|
|
246
|
+
function compareBytes(a, b) {
|
|
247
|
+
const len = Math.min(a.length, b.length);
|
|
248
|
+
for (let i = 0; i < len; i++) {
|
|
249
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
250
|
+
}
|
|
251
|
+
return a.length - b.length;
|
|
252
|
+
}
|
|
253
|
+
function leavesFromFields(fields) {
|
|
254
|
+
const sorted = sortFields(fields);
|
|
255
|
+
const leafBytes = sorted.map(encodeLeaf);
|
|
256
|
+
return { sorted, leafBytes };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/signing-committed.ts
|
|
260
|
+
function freshSalt() {
|
|
261
|
+
return randomBytes(32);
|
|
262
|
+
}
|
|
263
|
+
function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey, kid, issuer) {
|
|
264
|
+
const allFields = {
|
|
265
|
+
tool: entry.tool,
|
|
266
|
+
decision: entry.decision,
|
|
267
|
+
reason_code: entry.reason_code,
|
|
268
|
+
policy_digest: entry.policy_digest,
|
|
269
|
+
scope: entry.request_id,
|
|
270
|
+
mode: entry.mode,
|
|
271
|
+
request_id: entry.request_id
|
|
272
|
+
};
|
|
273
|
+
if (entry.tier) allFields.tier = entry.tier;
|
|
274
|
+
if (entry.credential_ref) allFields.credential_ref = entry.credential_ref;
|
|
275
|
+
if (entry.rate_limit_remaining !== void 0) {
|
|
276
|
+
allFields.rate_limit_remaining = entry.rate_limit_remaining;
|
|
277
|
+
}
|
|
278
|
+
if (entry.policy_engine) allFields.policy_engine = entry.policy_engine;
|
|
279
|
+
if (entry.hook_event) allFields.hook_event = entry.hook_event;
|
|
280
|
+
if (entry.sandbox_state) allFields.sandbox_state = entry.sandbox_state;
|
|
281
|
+
if (entry.timing) allFields.timing = entry.timing;
|
|
282
|
+
if (entry.swarm) allFields.swarm = entry.swarm;
|
|
283
|
+
if (entry.payload_digest) allFields.payload_digest = entry.payload_digest;
|
|
284
|
+
if (entry.deny_iteration) allFields.deny_iteration = entry.deny_iteration;
|
|
285
|
+
const committedFields = [];
|
|
286
|
+
const cleartextFields = {};
|
|
287
|
+
const openings = {};
|
|
288
|
+
for (const [name, value] of Object.entries(allFields)) {
|
|
289
|
+
if (committedFieldNames.includes(name)) {
|
|
290
|
+
const salt = freshSalt();
|
|
291
|
+
committedFields.push({ name, salt, value });
|
|
292
|
+
} else {
|
|
293
|
+
cleartextFields[name] = value;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
let committedFieldsRoot = null;
|
|
297
|
+
if (committedFields.length > 0) {
|
|
298
|
+
const { sorted, leafBytes } = leavesFromFields(committedFields);
|
|
299
|
+
const leafHashes = leafBytes.map(hashLeaf);
|
|
300
|
+
const root = merkleRoot(leafHashes);
|
|
301
|
+
committedFieldsRoot = bytesToHex(root);
|
|
302
|
+
sorted.forEach((f, i) => {
|
|
303
|
+
openings[f.name] = { name: f.name, value: f.value, salt: f.salt, index: i };
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const payload = {
|
|
307
|
+
type: "scopeblind.receipt.committed.v1",
|
|
308
|
+
spec: "draft-farley-acta-signed-receipts-01",
|
|
309
|
+
issuer_certification: "self-signed",
|
|
310
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
311
|
+
...cleartextFields
|
|
312
|
+
};
|
|
313
|
+
if (committedFieldsRoot !== null) {
|
|
314
|
+
payload.committed_fields_root = committedFieldsRoot;
|
|
315
|
+
payload.committed_field_names = committedFields.map((f) => f.name);
|
|
316
|
+
}
|
|
317
|
+
const canonical = jcs(payload);
|
|
318
|
+
const messageHash = sha2562(new TextEncoder().encode(canonical));
|
|
319
|
+
const signatureBytes = ed25519.sign(messageHash, hexToBytes(signingKey));
|
|
320
|
+
const signedReceipt = {
|
|
321
|
+
...payload,
|
|
322
|
+
signature: {
|
|
323
|
+
alg: "EdDSA",
|
|
324
|
+
kid,
|
|
325
|
+
issuer,
|
|
326
|
+
sig: base64urlNoPad(signatureBytes),
|
|
327
|
+
public_key: publicKey
|
|
328
|
+
// hex
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
const signedJson = JSON.stringify(signedReceipt);
|
|
332
|
+
const receiptHash = bytesToHex(sha2562(new TextEncoder().encode(jcs(signedReceipt))));
|
|
333
|
+
return {
|
|
334
|
+
signed: signedJson,
|
|
335
|
+
artifact_type: "decision_receipt_committed_v1",
|
|
336
|
+
openings,
|
|
337
|
+
receipt_hash: receiptHash
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function discloseField(receiptHash, fieldName, openings) {
|
|
341
|
+
const o = openings[fieldName];
|
|
342
|
+
if (!o) {
|
|
343
|
+
throw new Error(`disclose: no opening recorded for field "${fieldName}"`);
|
|
344
|
+
}
|
|
345
|
+
const fields = Object.values(openings).map((op) => ({
|
|
346
|
+
name: op.name,
|
|
347
|
+
salt: op.salt,
|
|
348
|
+
value: op.value
|
|
349
|
+
}));
|
|
350
|
+
const { leafBytes } = leavesFromFields(fields);
|
|
351
|
+
const leafHashes = leafBytes.map(hashLeaf);
|
|
352
|
+
const proof = generateProof(leafHashes, o.index);
|
|
353
|
+
return {
|
|
354
|
+
parent_receipt_hash: receiptHash,
|
|
355
|
+
name: fieldName,
|
|
356
|
+
value: o.value,
|
|
357
|
+
salt: base64urlNoPad(o.salt),
|
|
358
|
+
proof
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function createSelectiveDisclosurePackage(receipt, fieldNames, openings) {
|
|
362
|
+
const receiptHash = receiptHashHex(receipt);
|
|
363
|
+
const committedFieldsRoot = typeof receipt.committed_fields_root === "string" ? receipt.committed_fields_root : "";
|
|
364
|
+
if (!committedFieldsRoot) {
|
|
365
|
+
throw new Error("selective disclosure requires a committed receipt with committed_fields_root");
|
|
366
|
+
}
|
|
367
|
+
const committedFieldNames = committedFieldNamesFromReceipt(receipt, openings);
|
|
368
|
+
const uniqueFields = Array.from(new Set(fieldNames));
|
|
369
|
+
for (const fieldName of uniqueFields) {
|
|
370
|
+
if (!committedFieldNames.includes(fieldName)) {
|
|
371
|
+
throw new Error(`selective disclosure: field "${fieldName}" is not committed by this receipt`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const disclosures = uniqueFields.map((fieldName) => discloseField(receiptHash, fieldName, openings));
|
|
375
|
+
const hiddenFields = committedFieldNames.filter((fieldName) => !uniqueFields.includes(fieldName));
|
|
376
|
+
return {
|
|
377
|
+
type: "scopeblind.selective_disclosure.v0",
|
|
378
|
+
version: 0,
|
|
379
|
+
parent_receipt_hash: receiptHash,
|
|
380
|
+
committed_fields_root: committedFieldsRoot,
|
|
381
|
+
disclosed_fields: uniqueFields,
|
|
382
|
+
hidden_fields: hiddenFields,
|
|
383
|
+
disclosures,
|
|
384
|
+
verifier_explanation: {
|
|
385
|
+
summary: "This package opens selected committed receipt fields and leaves the rest hidden.",
|
|
386
|
+
disclosed: uniqueFields.length ? `Disclosed fields: ${uniqueFields.join(", ")}.` : "No fields were disclosed.",
|
|
387
|
+
hidden: hiddenFields.length ? `Hidden committed fields: ${hiddenFields.join(", ")}. Their salted commitments remain bound to the signed receipt root.` : "No committed fields remain hidden.",
|
|
388
|
+
limitation: "Selective Disclosure v0 uses salted SHA-256 commitments and Merkle proofs. It is not a full zero-knowledge proof."
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function verifySelectiveDisclosurePackage(receipt, disclosure) {
|
|
393
|
+
const errors = [];
|
|
394
|
+
if (disclosure.type !== "scopeblind.selective_disclosure.v0") {
|
|
395
|
+
errors.push("disclosure.type is not scopeblind.selective_disclosure.v0");
|
|
396
|
+
}
|
|
397
|
+
const actualReceiptHash = receiptHashHex(receipt);
|
|
398
|
+
const receiptHashValid = disclosure.parent_receipt_hash === actualReceiptHash;
|
|
399
|
+
if (!receiptHashValid) {
|
|
400
|
+
errors.push("parent_receipt_hash does not match the supplied receipt");
|
|
401
|
+
}
|
|
402
|
+
const root = typeof receipt.committed_fields_root === "string" ? receipt.committed_fields_root : "";
|
|
403
|
+
const commitmentRootValid = Boolean(root) && disclosure.committed_fields_root === root;
|
|
404
|
+
if (!commitmentRootValid) {
|
|
405
|
+
errors.push("committed_fields_root does not match the supplied receipt");
|
|
406
|
+
}
|
|
407
|
+
const signatureValid = verifyCommittedReceiptSignature(receipt);
|
|
408
|
+
if (signatureValid === false) {
|
|
409
|
+
errors.push("receipt signature failed verification");
|
|
410
|
+
}
|
|
411
|
+
const committedFieldNames = committedFieldNamesFromReceipt(receipt, {});
|
|
412
|
+
const disclosed = /* @__PURE__ */ new Set();
|
|
413
|
+
for (const item of disclosure.disclosures || []) {
|
|
414
|
+
if (item.parent_receipt_hash !== disclosure.parent_receipt_hash) {
|
|
415
|
+
errors.push(`disclosure for "${item.name}" targets a different receipt hash`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (!committedFieldNames.includes(item.name)) {
|
|
419
|
+
errors.push(`field "${item.name}" is not listed in committed_field_names`);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const leafBytes = encodeLeaf({
|
|
423
|
+
name: item.name,
|
|
424
|
+
salt: base64urlDecode(item.salt),
|
|
425
|
+
value: item.value
|
|
426
|
+
});
|
|
427
|
+
const ok = root ? verifyProof(root, hashLeaf(leafBytes), item.proof) : false;
|
|
428
|
+
if (!ok) {
|
|
429
|
+
errors.push(`field "${item.name}" failed Merkle inclusion verification`);
|
|
430
|
+
} else {
|
|
431
|
+
disclosed.add(item.name);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const disclosedFields = Array.from(disclosed);
|
|
435
|
+
const hiddenFields = committedFieldNames.filter((fieldName) => !disclosed.has(fieldName));
|
|
436
|
+
const valid = errors.length === 0 && receiptHashValid && commitmentRootValid && signatureValid !== false;
|
|
437
|
+
const explanation = [
|
|
438
|
+
valid ? "Selective disclosure verified: the disclosed fields open to the signed receipt commitment root." : "Selective disclosure failed verification.",
|
|
439
|
+
signatureValid === true ? "Receipt signature verified against the embedded Ed25519 public key." : signatureValid === null ? "Receipt signature was not checked because the committed receipt did not carry an embedded Ed25519 signature object." : "Receipt signature did not verify.",
|
|
440
|
+
disclosedFields.length ? `Disclosed fields: ${disclosedFields.join(", ")}.` : "No fields were disclosed.",
|
|
441
|
+
hiddenFields.length ? `Hidden fields: ${hiddenFields.join(", ")}. These remain private but bound to the same commitment root.` : "No committed fields remain hidden.",
|
|
442
|
+
"Limitation: this is salted commitment disclosure, not full zero-knowledge."
|
|
443
|
+
];
|
|
444
|
+
return {
|
|
445
|
+
valid,
|
|
446
|
+
receipt_hash_valid: receiptHashValid,
|
|
447
|
+
signature_valid: signatureValid,
|
|
448
|
+
commitment_root_valid: commitmentRootValid,
|
|
449
|
+
disclosed_fields: disclosedFields,
|
|
450
|
+
hidden_fields: hiddenFields,
|
|
451
|
+
errors,
|
|
452
|
+
explanation
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function committedFieldNamesFromReceipt(receipt, openings) {
|
|
456
|
+
const fromReceipt = Array.isArray(receipt.committed_field_names) ? receipt.committed_field_names.filter((fieldName) => typeof fieldName === "string") : [];
|
|
457
|
+
const names = fromReceipt.length ? fromReceipt : Object.keys(openings);
|
|
458
|
+
return Array.from(new Set(names)).sort();
|
|
459
|
+
}
|
|
460
|
+
function receiptHashHex(receipt) {
|
|
461
|
+
return bytesToHex(sha2562(new TextEncoder().encode(jcs(receipt))));
|
|
462
|
+
}
|
|
463
|
+
function verifyCommittedReceiptSignature(receipt) {
|
|
464
|
+
const signature = receipt.signature;
|
|
465
|
+
if (!signature || typeof signature !== "object") return null;
|
|
466
|
+
const sig = signature;
|
|
467
|
+
if (sig.alg !== "EdDSA" || typeof sig.sig !== "string" || typeof sig.public_key !== "string") {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
const { signature: _signature, ...payloadWithoutSig } = receipt;
|
|
471
|
+
const messageHash = sha2562(new TextEncoder().encode(jcs(payloadWithoutSig)));
|
|
472
|
+
try {
|
|
473
|
+
return ed25519.verify(base64urlDecode(sig.sig), messageHash, hexToBytes(sig.public_key));
|
|
474
|
+
} catch {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export {
|
|
480
|
+
sha2562 as sha256,
|
|
481
|
+
hmac,
|
|
482
|
+
signCommittedDecision,
|
|
483
|
+
discloseField,
|
|
484
|
+
createSelectiveDisclosurePackage,
|
|
485
|
+
verifySelectiveDisclosurePackage
|
|
486
|
+
};
|