@smartledger/bsv 8.0.1 → 8.2.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 +118 -0
- package/README.md +38 -38
- package/bsv-gdaf.min.js +47 -47
- package/bsv-ltp.min.js +1 -1
- package/bsv-smartcontract.min.js +1 -1
- package/bsv.bundle.js +48 -48
- package/bsv.min.js +48 -48
- package/docs/AUDIT_SCOPE.md +13 -13
- package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
- package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
- package/docs/getting-started/INSTALLATION.md +23 -23
- package/docs/getting-started/QUICK_START.md +7 -7
- package/docs/migration/FROM_BSV_1_5_6.md +5 -5
- package/lib/gdaf/attestation-signer.js +90 -2
- package/lib/gdaf/attestation-verifier.js +41 -25
- package/lib/gdaf/zk-prover.js +143 -46
- package/lib/script/interpreter.js +20 -1
- package/package.json +3 -2
- package/test/gdaf/canonicalization.js +106 -0
- package/test/gdaf/zk_prover.js +204 -0
- package/test/script/genesis_limits.js +37 -6
- package/tools/minimal_reproduction.js +119 -0
- package/tools/opcode_map.js +342 -0
- package/tools/server.js +97 -0
- package/tools/simple_real_tx.js +136 -0
- package/tools/sv-sighash-harness.js +123 -0
- package/tools/sv-sighash-report.js +71 -0
- package/tools/sv-tx-harness.js +172 -0
- package/tools/sv-tx-report.js +43 -0
- package/tools/sv-vector-harness.js +304 -0
- package/tools/sv-vector-report.js +88 -0
- package/version.js +1 -1
package/lib/gdaf/zk-prover.js
CHANGED
|
@@ -8,18 +8,26 @@ var $ = require('../util/preconditions')
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* ZKProver
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
11
|
+
*
|
|
12
|
+
* Selective disclosure of credential fields, built on a salted Merkle tree and hash
|
|
13
|
+
* commitments.
|
|
14
|
+
*
|
|
15
|
+
* NOT ZERO-KNOWLEDGE, despite the name this module has carried. There is no
|
|
16
|
+
* zero-knowledge machinery here — no Bulletproofs, no pairing, no circuit. What it
|
|
17
|
+
* provides is:
|
|
18
|
+
*
|
|
19
|
+
* - Selective field disclosure that genuinely withholds the undisclosed fields. Each
|
|
20
|
+
* leaf carries its own salt and only the disclosed leaves' salts travel in the proof,
|
|
21
|
+
* so an undisclosed leaf hash is a commitment under a secret the verifier never sees.
|
|
22
|
+
* - Merkle inclusion proofs binding disclosed fields to a credential root.
|
|
23
|
+
* - Hash commitments for ranges and ages. These can only be verified by OPENING them,
|
|
24
|
+
* which reveals the committed value to the verifier. A real age proof would not; this
|
|
25
|
+
* cannot do that, and callers who need it need a different primitive.
|
|
26
|
+
*
|
|
27
|
+
* The range and age verifiers previously returned a boolean the prover wrote about
|
|
28
|
+
* itself, so any object of the right shape verified. They now require the opening and
|
|
29
|
+
* check the commitment. Keep that distinction in mind before describing anything built
|
|
30
|
+
* on this module as zero-knowledge to a reviewer.
|
|
23
31
|
*/
|
|
24
32
|
|
|
25
33
|
/**
|
|
@@ -43,29 +51,46 @@ function ZKProver(options) {
|
|
|
43
51
|
* @returns {Object} Merkle tree data
|
|
44
52
|
*/
|
|
45
53
|
ZKProver.createMerkleTree = function(credential, salt) {
|
|
46
|
-
salt = salt || Random.getRandomBuffer(32).toString('hex')
|
|
47
|
-
|
|
48
54
|
$.checkArgument(credential && typeof credential === 'object', 'Invalid credential')
|
|
49
|
-
|
|
55
|
+
|
|
50
56
|
// Extract all fields from credential
|
|
51
57
|
var fields = ZKProver._extractFields(credential)
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
|
|
59
|
+
// PER-LEAF salts. A single salt shared by every leaf defeats selective disclosure
|
|
60
|
+
// entirely: the proof has to carry the salt so the verifier can recompute the
|
|
61
|
+
// disclosed leaves, and with that one value an attacker can hash candidate
|
|
62
|
+
// (path, value) pairs against the sibling hashes on the Merkle path and read back
|
|
63
|
+
// exactly the fields the holder withheld. Confirmed against a synthetic credential —
|
|
64
|
+
// disclosing only `credentialSubject.name` leaked `id`, `partyAffiliation` and
|
|
65
|
+
// `eligible`, the last two by brute-forcing their shared parent node.
|
|
66
|
+
//
|
|
67
|
+
// With a salt per leaf, only the disclosed leaves' salts travel in the proof, so an
|
|
68
|
+
// undisclosed leaf hash is a commitment under a secret the verifier never sees.
|
|
69
|
+
//
|
|
70
|
+
// `salt` is accepted only to derive DETERMINISTIC per-leaf salts, so a caller that
|
|
71
|
+
// needs reproducible trees still gets them without sharing one value across leaves.
|
|
72
|
+
var master = salt || Random.getRandomBuffer(32).toString('hex')
|
|
73
|
+
|
|
74
|
+
var leaves = fields.map(function(field, index) {
|
|
75
|
+
var leafSalt = Hash.sha256(
|
|
76
|
+
Buffer.from(master + ':' + index + ':' + field.path, 'utf8')
|
|
77
|
+
).toString('hex')
|
|
78
|
+
var fieldData = field.path + ':' + JSON.stringify(field.value) + ':' + leafSalt
|
|
56
79
|
return {
|
|
57
80
|
path: field.path,
|
|
58
81
|
value: field.value,
|
|
59
82
|
hash: Hash.sha256(Buffer.from(fieldData, 'utf8')).toString('hex'),
|
|
60
|
-
salt:
|
|
83
|
+
salt: leafSalt
|
|
61
84
|
}
|
|
62
85
|
})
|
|
63
|
-
|
|
86
|
+
|
|
64
87
|
// Build Merkle tree
|
|
65
88
|
var tree = ZKProver._buildMerkleTree(leaves.map(l => l.hash))
|
|
66
|
-
|
|
89
|
+
|
|
67
90
|
return {
|
|
68
|
-
salt
|
|
91
|
+
// The MASTER salt. Never put this in a proof — it re-derives every leaf salt and
|
|
92
|
+
// reopens the whole credential. generateSelectiveProof() ships per-leaf salts.
|
|
93
|
+
salt: master,
|
|
69
94
|
leaves: leaves,
|
|
70
95
|
tree: tree,
|
|
71
96
|
root: tree[tree.length - 1][0]
|
|
@@ -122,11 +147,16 @@ ZKProver.generateSelectiveProof = function(credential, disclosePaths, salt) {
|
|
|
122
147
|
return {
|
|
123
148
|
path: leaf.path,
|
|
124
149
|
value: leaf.value,
|
|
125
|
-
hash: leaf.hash
|
|
150
|
+
hash: leaf.hash,
|
|
151
|
+
// The salt for THIS leaf only. Shipping one salt for the whole credential let
|
|
152
|
+
// a verifier — or anyone the proof is shown to — brute-force the withheld
|
|
153
|
+
// fields off the Merkle path.
|
|
154
|
+
salt: leaf.salt
|
|
126
155
|
}
|
|
127
156
|
}),
|
|
128
|
-
merkleProofs: merkleProofs
|
|
129
|
-
|
|
157
|
+
merkleProofs: merkleProofs
|
|
158
|
+
// NOTE: the master salt is deliberately absent. It re-derives every leaf salt,
|
|
159
|
+
// including the undisclosed ones, which would reopen the whole credential.
|
|
130
160
|
}
|
|
131
161
|
}
|
|
132
162
|
|
|
@@ -157,8 +187,13 @@ ZKProver.verifySelectiveProof = function(proof, expectedRoot) {
|
|
|
157
187
|
continue
|
|
158
188
|
}
|
|
159
189
|
|
|
160
|
-
// Verify field hash
|
|
161
|
-
|
|
190
|
+
// Verify field hash, using the salt carried with THIS field. Reading a
|
|
191
|
+
// credential-wide `proof.salt` is what the leak fix removed.
|
|
192
|
+
if (typeof field.salt !== 'string' || !field.salt) {
|
|
193
|
+
result.errors.push('Missing per-field salt for: ' + field.path)
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
var fieldData = field.path + ':' + JSON.stringify(field.value) + ':' + field.salt
|
|
162
197
|
var computedHash = Hash.sha256(Buffer.from(fieldData, 'utf8')).toString('hex')
|
|
163
198
|
|
|
164
199
|
if (computedHash !== field.hash) {
|
|
@@ -235,7 +270,10 @@ ZKProver.generateAgeProof = function(birthDate, minimumAge, salt) {
|
|
|
235
270
|
meetsRequirement: true,
|
|
236
271
|
birthDateCommitment: commitment,
|
|
237
272
|
ageProofHash: ageProofHash,
|
|
238
|
-
challengeResponse: ZKProver._generateAgeChallenge(birthDate, minimumAge, salt)
|
|
273
|
+
challengeResponse: ZKProver._generateAgeChallenge(birthDate, minimumAge, salt),
|
|
274
|
+
// As with the range proof: the commitment can only be checked by opening it, and
|
|
275
|
+
// opening it reveals the birth date. Returned alongside the proof, never inside it.
|
|
276
|
+
opening: { birthDate: birthDateString, salt: salt }
|
|
239
277
|
}
|
|
240
278
|
}
|
|
241
279
|
|
|
@@ -245,7 +283,7 @@ ZKProver.generateAgeProof = function(birthDate, minimumAge, salt) {
|
|
|
245
283
|
* @param {Number} requiredAge - Required minimum age
|
|
246
284
|
* @returns {Boolean} True if proof is valid
|
|
247
285
|
*/
|
|
248
|
-
ZKProver.verifyAgeProof = function(proof, requiredAge) {
|
|
286
|
+
ZKProver.verifyAgeProof = function(proof, requiredAge, opening) {
|
|
249
287
|
try {
|
|
250
288
|
$.checkArgument(proof && typeof proof === 'object', 'Invalid proof')
|
|
251
289
|
$.checkArgument(typeof requiredAge === 'number', 'Required age must be number')
|
|
@@ -258,15 +296,40 @@ ZKProver.verifyAgeProof = function(proof, requiredAge) {
|
|
|
258
296
|
if (proof.minimumAge !== requiredAge) {
|
|
259
297
|
return false
|
|
260
298
|
}
|
|
261
|
-
|
|
262
|
-
|
|
299
|
+
|
|
300
|
+
// Same defect as verifyRangeProof, and the same fix. This used to accept
|
|
301
|
+
// `proof.meetsRequirement` — the prover's own claim — and then check only that
|
|
302
|
+
// `challengeResponse` was a non-empty string, which any forged proof satisfies.
|
|
303
|
+
// `birthDateCommitment` was never opened.
|
|
304
|
+
//
|
|
305
|
+
// The commitment is over the birth date, so verifying it requires the birth date.
|
|
306
|
+
// That reveals it, which is precisely what an age proof is supposed to avoid — the
|
|
307
|
+
// honest reading is that this construction cannot do what its name promises, and a
|
|
308
|
+
// caller who needs real age proofs needs a different primitive.
|
|
309
|
+
if (!opening || typeof opening !== 'object') {
|
|
263
310
|
return false
|
|
264
311
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
312
|
+
var birthDate = opening.birthDate
|
|
313
|
+
if (typeof birthDate === 'string') birthDate = new Date(birthDate)
|
|
314
|
+
if (!(birthDate instanceof Date) || isNaN(birthDate.getTime())) {
|
|
315
|
+
return false
|
|
316
|
+
}
|
|
317
|
+
if (typeof opening.salt !== 'string') {
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
var birthDateString = birthDate.toISOString().split('T')[0]
|
|
322
|
+
var commitment = Hash.sha256(
|
|
323
|
+
Buffer.from(birthDateString + ':' + opening.salt, 'utf8')
|
|
324
|
+
).toString('hex')
|
|
325
|
+
if (commitment !== proof.birthDateCommitment) {
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Recompute the age rather than trusting `meetsRequirement`.
|
|
330
|
+
var ageInYears = Math.floor((Date.now() - birthDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000))
|
|
331
|
+
return ageInYears >= requiredAge
|
|
332
|
+
|
|
270
333
|
} catch (error) {
|
|
271
334
|
return false
|
|
272
335
|
}
|
|
@@ -291,7 +354,8 @@ ZKProver.generateRangeProof = function(value, min, max, salt) {
|
|
|
291
354
|
// Create commitment to value
|
|
292
355
|
var commitment = Hash.sha256(Buffer.from(value.toString() + ':' + salt, 'utf8')).toString('hex')
|
|
293
356
|
|
|
294
|
-
// Generate proof components
|
|
357
|
+
// Generate proof components. NOT a Bulletproof — this is a hash commitment plus a
|
|
358
|
+
// hash over the parameters. It says nothing without the opening returned below.
|
|
295
359
|
var proofData = {
|
|
296
360
|
min: min,
|
|
297
361
|
max: max,
|
|
@@ -309,7 +373,13 @@ ZKProver.generateRangeProof = function(value, min, max, salt) {
|
|
|
309
373
|
range: { min: min, max: max },
|
|
310
374
|
valueCommitment: commitment,
|
|
311
375
|
proofHash: proofHash,
|
|
312
|
-
inRange: true
|
|
376
|
+
inRange: true,
|
|
377
|
+
// The OPENING. verifyRangeProof() cannot check the commitment without it, so the
|
|
378
|
+
// holder must pass it to the verifier out of band. It is returned here rather than
|
|
379
|
+
// embedded in the proof precisely because handing it over reveals the value — that
|
|
380
|
+
// disclosure is the cost of a commitment scheme, and hiding it inside the proof
|
|
381
|
+
// would make every proof self-opening.
|
|
382
|
+
opening: { value: value, salt: salt }
|
|
313
383
|
}
|
|
314
384
|
}
|
|
315
385
|
|
|
@@ -320,20 +390,47 @@ ZKProver.generateRangeProof = function(value, min, max, salt) {
|
|
|
320
390
|
* @param {Number} max - Expected maximum
|
|
321
391
|
* @returns {Boolean} True if proof is valid
|
|
322
392
|
*/
|
|
323
|
-
ZKProver.verifyRangeProof = function(proof, min, max) {
|
|
393
|
+
ZKProver.verifyRangeProof = function(proof, min, max, opening) {
|
|
324
394
|
try {
|
|
325
395
|
$.checkArgument(proof && typeof proof === 'object', 'Invalid proof')
|
|
326
|
-
|
|
396
|
+
|
|
327
397
|
if (proof.type !== 'RangeProof') {
|
|
328
398
|
return false
|
|
329
399
|
}
|
|
330
|
-
|
|
331
|
-
if (proof.range.min !== min || proof.range.max !== max) {
|
|
400
|
+
|
|
401
|
+
if (!proof.range || proof.range.min !== min || proof.range.max !== max) {
|
|
332
402
|
return false
|
|
333
403
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
404
|
+
|
|
405
|
+
// WITHOUT AN OPENING, NOTHING HAS BEEN PROVEN. This used to end at
|
|
406
|
+
// `return proof.inRange === true` — a boolean the prover writes about itself, never
|
|
407
|
+
// checked against `valueCommitment`. Any object of the right shape verified:
|
|
408
|
+
//
|
|
409
|
+
// { type: 'RangeProof', range: { min: 18, max: 120 },
|
|
410
|
+
// valueCommitment: '00…', proofHash: 'de…', inRange: true } -> true
|
|
411
|
+
//
|
|
412
|
+
// These are hash commitments, not zero-knowledge proofs: the commitment can only be
|
|
413
|
+
// checked by opening it, so the holder must supply { value, salt } out of band. That
|
|
414
|
+
// reveals the value to the verifier, which is the honest cost of this construction
|
|
415
|
+
// and the reason it must not be described as zero-knowledge.
|
|
416
|
+
if (!opening || typeof opening !== 'object') {
|
|
417
|
+
return false
|
|
418
|
+
}
|
|
419
|
+
if (typeof opening.value !== 'number' || typeof opening.salt !== 'string') {
|
|
420
|
+
return false
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// The commitment must actually open to the claimed value...
|
|
424
|
+
var commitment = Hash.sha256(
|
|
425
|
+
Buffer.from(opening.value.toString() + ':' + opening.salt, 'utf8')
|
|
426
|
+
).toString('hex')
|
|
427
|
+
if (commitment !== proof.valueCommitment) {
|
|
428
|
+
return false
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ...and that value must genuinely lie in the range, rather than the prover saying so.
|
|
432
|
+
return opening.value >= min && opening.value <= max
|
|
433
|
+
|
|
337
434
|
} catch (error) {
|
|
338
435
|
return false
|
|
339
436
|
}
|
|
@@ -322,9 +322,28 @@ Interpreter.LOCKTIME_THRESHOLD_BN = new BN(Interpreter.LOCKTIME_THRESHOLD)
|
|
|
322
322
|
Interpreter.useGenesisLimits = function (max) {
|
|
323
323
|
max = max || 0x7fffffff
|
|
324
324
|
Interpreter.MAX_SCRIPT_ELEMENT_SIZE = max
|
|
325
|
-
Interpreter.MAXIMUM_ELEMENT_SIZE = max
|
|
326
325
|
Interpreter.MAX_OPS_PER_SCRIPT = max
|
|
327
326
|
Interpreter.MAX_SCRIPT_SIZE = max
|
|
327
|
+
|
|
328
|
+
// MAXIMUM_ELEMENT_SIZE is deliberately NOT raised. It is CScriptNum's `max_length`,
|
|
329
|
+
// and since the limits became era-derived it serves only as the PRE-Genesis fallback
|
|
330
|
+
// for `maxScriptNumLength()` — post-Genesis is 750,000 and post-Chronicle 32,000,000,
|
|
331
|
+
// both reached through the era flags rather than this static.
|
|
332
|
+
//
|
|
333
|
+
// Raising it therefore could not enable post-Genesis arithmetic; it could only corrupt
|
|
334
|
+
// pre-Genesis validation, and it did. Measured, a 5-byte operand under
|
|
335
|
+
// P2SH,STRICTENC:
|
|
336
|
+
//
|
|
337
|
+
// fresh process: SCRIPT_ERR_SCRIPTNUM_OVERFLOW (what the node says)
|
|
338
|
+
// after useGenesisLimits(): ACCEPTED
|
|
339
|
+
//
|
|
340
|
+
// A false accept, in the direction that can cost money, produced by a process-wide
|
|
341
|
+
// side effect on unrelated code later in the same process. The node's own vectors
|
|
342
|
+
// agree: all 22 SCRIPTNUM_OVERFLOW rows are pre-Genesis, and raising this static turns
|
|
343
|
+
// 15 of them into false accepts.
|
|
344
|
+
//
|
|
345
|
+
// For post-Genesis arithmetic, pass the era — `Interpreter.mainnetFlags()`, or simply
|
|
346
|
+
// omit the flags argument, which now defaults to current mainnet.
|
|
328
347
|
return Interpreter
|
|
329
348
|
}
|
|
330
349
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartledger/bsv",
|
|
3
|
-
"version": "8.0
|
|
3
|
+
"version": "8.2.0",
|
|
4
4
|
"description": "🚀 Complete Bitcoin SV development framework with legally-recognizable DID:web + W3C VC-JWT toolkit, Legal Token Protocol (LTP), Global Digital Attestation Framework (GDAF), StatusList2021 revocation, and 16 flexible loading options. Standards-based credentials with ES256/ES256K support, on-chain BSV anchoring, and comprehensive Bitcoin SV API. Perfect for legal tokens, verifiable credentials, DeFi, smart contracts, and secure Bitcoin applications.",
|
|
5
5
|
"author": "SmartLedger Technology <hello@smartledger.technology> (https://smartledger.technology)",
|
|
6
6
|
"homepage": "https://github.com/codenlighten/smartledger-bsv#readme",
|
|
@@ -117,6 +117,7 @@
|
|
|
117
117
|
"message/",
|
|
118
118
|
"mnemonic/",
|
|
119
119
|
"build/",
|
|
120
|
+
"tools/",
|
|
120
121
|
"*-entry.js",
|
|
121
122
|
"bsv.min.js",
|
|
122
123
|
"bsv.bundle.js",
|
|
@@ -187,7 +188,7 @@
|
|
|
187
188
|
"preimage",
|
|
188
189
|
"secret-splitting",
|
|
189
190
|
"did-resolution",
|
|
190
|
-
"
|
|
191
|
+
"selective-disclosure",
|
|
191
192
|
"blockchain-anchoring",
|
|
192
193
|
"attestation-framework",
|
|
193
194
|
"nchain",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* global describe, it */
|
|
4
|
+
|
|
5
|
+
// GDAF signs a hash of canonical JSON, so the canonicalization IS part of the signature
|
|
6
|
+
// scheme. It was sorted-key JSON that rebuilt the object, which loses the sort: V8 orders
|
|
7
|
+
// integer-like own properties numerically, ahead of string keys, whatever order they were
|
|
8
|
+
// inserted in.
|
|
9
|
+
//
|
|
10
|
+
// Within this library that was deterministic — signing and verification agreed, and no
|
|
11
|
+
// forgery followed. Across implementations it was a verification failure, which for
|
|
12
|
+
// credentials that exist to be checked by other parties is the thing that matters.
|
|
13
|
+
|
|
14
|
+
require('chai').should()
|
|
15
|
+
var Signer = require('../../lib/gdaf/attestation-signer')
|
|
16
|
+
|
|
17
|
+
describe('GDAF canonicalization', function () {
|
|
18
|
+
describe('RFC 8785 conformance', function () {
|
|
19
|
+
// The concrete divergence. JCS sorts by UTF-16 code unit, where '10' < '2'.
|
|
20
|
+
it('sorts integer-like keys lexicographically, not numerically', function () {
|
|
21
|
+
var o = {}
|
|
22
|
+
o['10'] = 'ten'
|
|
23
|
+
o['2'] = 'two'
|
|
24
|
+
Signer._canonicalizeJCS(o).should.equal('{"10":"ten","2":"two"}')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('is what the legacy form got wrong, which is why both exist', function () {
|
|
28
|
+
var o = {}
|
|
29
|
+
o['10'] = 'ten'
|
|
30
|
+
o['2'] = 'two'
|
|
31
|
+
Signer._canonicalizeJSON(o).should.equal('{"2":"two","10":"ten"}')
|
|
32
|
+
Signer._canonicalizeJCS(o).should.not.equal(Signer._canonicalizeJSON(o))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('sorts at every depth and leaves arrays in order', function () {
|
|
36
|
+
Signer._canonicalizeJCS({ z: { b: 1, a: 2 }, a: [3, { d: 1, c: 2 }] })
|
|
37
|
+
.should.equal('{"a":[3,{"c":2,"d":1}],"z":{"a":2,"b":1}}')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('is insensitive to key insertion order', function () {
|
|
41
|
+
var x = {}; x.b = 1; x.a = 2
|
|
42
|
+
var y = {}; y.a = 2; y.b = 1
|
|
43
|
+
Signer._canonicalizeJCS(x).should.equal(Signer._canonicalizeJCS(y))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// These are the same IEEE-754 double, so collapsing them is correct rather than a
|
|
47
|
+
// weakness — JavaScript has one number type. Pinned so nobody "fixes" it.
|
|
48
|
+
it('treats 1847, 1847.0 and 1.847e3 as the one number they are', function () {
|
|
49
|
+
Signer._canonicalizeJCS({ a: 1847 }).should.equal('{"a":1847}')
|
|
50
|
+
Signer._canonicalizeJCS({ a: 1847.0 }).should.equal('{"a":1847}')
|
|
51
|
+
Signer._canonicalizeJCS({ a: 1.847e3 }).should.equal('{"a":1847}')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('keeps a number distinct from its string form', function () {
|
|
55
|
+
Signer._canonicalizeJCS({ a: 1847 }).should.not.equal(Signer._canonicalizeJCS({ a: '1847' }))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// JSON.stringify would emit `null` here, silently signing a different document.
|
|
59
|
+
it('refuses non-finite numbers rather than emitting null', function () {
|
|
60
|
+
;(function () { Signer._canonicalizeJCS({ a: NaN }) }).should.throw(/non-finite/)
|
|
61
|
+
;(function () { Signer._canonicalizeJCS({ a: Infinity }) }).should.throw(/non-finite/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('emits unicode and non-BMP characters directly', function () {
|
|
65
|
+
Signer._canonicalizeJCS({ a: 'é' }).should.equal('{"a":"é"}')
|
|
66
|
+
Signer._canonicalizeJCS({ a: '😀' }).should.equal('{"a":"😀"}')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('drops undefined members, which JSON cannot represent', function () {
|
|
70
|
+
Signer._canonicalizeJCS({ a: 1, b: undefined }).should.equal('{"a":1}')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('nulls undefined array elements, as JSON.stringify does', function () {
|
|
74
|
+
Signer._canonicalizeJCS([1, undefined, 2]).should.equal('[1,null,2]')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('hashing and migration', function () {
|
|
79
|
+
var CRED = { id: 'urn:x', credentialSubject: { name: 'Alice', age: 41 } }
|
|
80
|
+
|
|
81
|
+
it('hashes with JCS by default', function () {
|
|
82
|
+
Signer._hashCredential(CRED).toString('hex')
|
|
83
|
+
.should.equal(Signer._hashCredential(CRED, Signer.CANONICALIZATION.JCS).toString('hex'))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// The migration path. A credential whose keys make the two forms differ must hash
|
|
87
|
+
// differently, or the legacy fallback in the verifier would be pointless.
|
|
88
|
+
it('gives a different hash under the legacy form when the forms diverge', function () {
|
|
89
|
+
var withIntKeys = { '10': 'ten', '2': 'two' }
|
|
90
|
+
Signer._hashCredential(withIntKeys, Signer.CANONICALIZATION.JCS).toString('hex')
|
|
91
|
+
.should.not.equal(
|
|
92
|
+
Signer._hashCredential(withIntKeys, Signer.CANONICALIZATION.LEGACY).toString('hex')
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('agrees between the forms when no integer-like keys are present', function () {
|
|
97
|
+
Signer._hashCredential(CRED, Signer.CANONICALIZATION.JCS).toString('hex')
|
|
98
|
+
.should.equal(Signer._hashCredential(CRED, Signer.CANONICALIZATION.LEGACY).toString('hex'))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('exposes both forms by name', function () {
|
|
102
|
+
Signer.CANONICALIZATION.JCS.should.equal('jcs')
|
|
103
|
+
Signer.CANONICALIZATION.LEGACY.should.equal('legacy')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* global describe, it */
|
|
4
|
+
|
|
5
|
+
// This module had ZERO test coverage, and carried three defects that coverage would have
|
|
6
|
+
// caught immediately. Each is pinned below as the attack that worked, not as a paraphrase
|
|
7
|
+
// of the fix, so a regression reproduces the exploit rather than merely failing a check.
|
|
8
|
+
//
|
|
9
|
+
// It is also not zero-knowledge, and the tests say so where it matters: the range and age
|
|
10
|
+
// commitments can only be verified by opening them, which reveals the committed value.
|
|
11
|
+
|
|
12
|
+
require('chai').should()
|
|
13
|
+
var bsv = require('../..')
|
|
14
|
+
var ZK = require('../../lib/gdaf/zk-prover')
|
|
15
|
+
var Hash = require('../../lib/crypto/hash')
|
|
16
|
+
|
|
17
|
+
var CRED = {
|
|
18
|
+
id: 'urn:x',
|
|
19
|
+
credentialSubject: { name: 'Alice', partyAffiliation: 'DEM', eligible: true }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('ZKProver', function () {
|
|
23
|
+
describe('range proofs', function () {
|
|
24
|
+
// The original read `return proof.inRange === true` — a boolean the prover writes
|
|
25
|
+
// about itself. valueCommitment was never consulted, so this forgery verified.
|
|
26
|
+
it('rejects a forged proof that merely claims inRange', function () {
|
|
27
|
+
var forged = {
|
|
28
|
+
type: 'RangeProof',
|
|
29
|
+
range: { min: 18, max: 120 },
|
|
30
|
+
valueCommitment: '00'.repeat(32),
|
|
31
|
+
proofHash: 'de'.repeat(32),
|
|
32
|
+
inRange: true
|
|
33
|
+
}
|
|
34
|
+
ZK.verifyRangeProof(forged, 18, 120).should.equal(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('rejects an honest proof presented without its opening', function () {
|
|
38
|
+
var proof = ZK.generateRangeProof(41, 18, 120)
|
|
39
|
+
ZK.verifyRangeProof(proof, 18, 120).should.equal(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('accepts an honest proof with its opening', function () {
|
|
43
|
+
var proof = ZK.generateRangeProof(41, 18, 120)
|
|
44
|
+
ZK.verifyRangeProof(proof, 18, 120, proof.opening).should.equal(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// The commitment must actually bind the value: a lie about what was committed to
|
|
48
|
+
// has to fail even though the range and structure are right.
|
|
49
|
+
it('rejects an opening that does not match the commitment', function () {
|
|
50
|
+
var proof = ZK.generateRangeProof(41, 18, 120)
|
|
51
|
+
var lie = { value: 12, salt: proof.opening.salt }
|
|
52
|
+
ZK.verifyRangeProof(proof, 18, 120, lie).should.equal(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('rejects a value outside the stated range even if the commitment opens', function () {
|
|
56
|
+
var proof = ZK.generateRangeProof(41, 18, 120)
|
|
57
|
+
ZK.verifyRangeProof(proof, 50, 120, proof.opening).should.equal(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns a strict boolean', function () {
|
|
61
|
+
var proof = ZK.generateRangeProof(41, 18, 120)
|
|
62
|
+
ZK.verifyRangeProof(proof, 18, 120, proof.opening).should.be.a('boolean')
|
|
63
|
+
ZK.verifyRangeProof({}, 18, 120).should.be.a('boolean')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('age proofs', function () {
|
|
68
|
+
var BIRTH = new Date('1985-01-01T00:00:00Z')
|
|
69
|
+
|
|
70
|
+
it('rejects a forged proof that merely claims meetsRequirement', function () {
|
|
71
|
+
var forged = {
|
|
72
|
+
type: 'AgeProof',
|
|
73
|
+
minimumAge: 18,
|
|
74
|
+
meetsRequirement: true,
|
|
75
|
+
birthDateCommitment: '00'.repeat(32),
|
|
76
|
+
challengeResponse: 'anything-non-empty'
|
|
77
|
+
}
|
|
78
|
+
ZK.verifyAgeProof(forged, 18).should.equal(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('rejects an honest proof presented without its opening', function () {
|
|
82
|
+
var proof = ZK.generateAgeProof(BIRTH, 18)
|
|
83
|
+
ZK.verifyAgeProof(proof, 18).should.equal(false)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('accepts an honest proof with its opening', function () {
|
|
87
|
+
var proof = ZK.generateAgeProof(BIRTH, 18)
|
|
88
|
+
ZK.verifyAgeProof(proof, 18, proof.opening).should.equal(true)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('rejects an opening that does not match the commitment', function () {
|
|
92
|
+
var proof = ZK.generateAgeProof(BIRTH, 18)
|
|
93
|
+
ZK.verifyAgeProof(proof, 18, { birthDate: '2015-01-01', salt: proof.opening.salt })
|
|
94
|
+
.should.equal(false)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('selective disclosure', function () {
|
|
99
|
+
// THE LEAK. One salt covered every leaf and travelled in the proof, so the sibling
|
|
100
|
+
// hashes on the Merkle path could be brute-forced back into the withheld fields.
|
|
101
|
+
// Disclosing only the name recovered id, partyAffiliation and eligible.
|
|
102
|
+
it('does not leak the withheld fields to someone holding the proof', function () {
|
|
103
|
+
var proof = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'], 'aa'.repeat(16))
|
|
104
|
+
|
|
105
|
+
// Everything an attacker gets: the proof's salts and the Merkle path.
|
|
106
|
+
var salts = proof.disclosedFields.map(function (f) { return f.salt })
|
|
107
|
+
var siblings = []
|
|
108
|
+
proof.merkleProofs.forEach(function (mp) {
|
|
109
|
+
mp.proof.forEach(function (n) { siblings.push(n.hash) })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
function leaf (p, v, s) {
|
|
113
|
+
return Hash.sha256(Buffer.from(p + ':' + JSON.stringify(v) + ':' + s, 'utf8')).toString('hex')
|
|
114
|
+
}
|
|
115
|
+
function node (a, b) {
|
|
116
|
+
return Hash.sha256(Buffer.from(a + b, 'hex')).toString('hex')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
var paths = ['id', 'credentialSubject.partyAffiliation', 'credentialSubject.eligible']
|
|
120
|
+
var values = ['urn:x', 'DEM', 'REP', 'IND', true, false]
|
|
121
|
+
var recovered = []
|
|
122
|
+
|
|
123
|
+
salts.forEach(function (s) {
|
|
124
|
+
// Direct leaf siblings.
|
|
125
|
+
paths.forEach(function (p) {
|
|
126
|
+
values.forEach(function (v) {
|
|
127
|
+
if (siblings.indexOf(leaf(p, v, s)) !== -1) recovered.push(p)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
// Internal nodes: brute-force the pair beneath them.
|
|
131
|
+
paths.forEach(function (p1) {
|
|
132
|
+
values.forEach(function (v1) {
|
|
133
|
+
paths.forEach(function (p2) {
|
|
134
|
+
values.forEach(function (v2) {
|
|
135
|
+
if (siblings.indexOf(node(leaf(p1, v1, s), leaf(p2, v2, s))) !== -1) {
|
|
136
|
+
recovered.push(p1, p2)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
recovered.should.deep.equal([], 'recovered withheld fields: ' + recovered.join(', '))
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('never ships the master salt, which would reopen every leaf', function () {
|
|
148
|
+
var proof = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'], 'aa'.repeat(16))
|
|
149
|
+
;(proof.salt === undefined).should.equal(true)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// The generated salt used to be dropped on the floor: the proof returned the
|
|
153
|
+
// (undefined) `salt` ARGUMENT rather than the one createMerkleTree produced, so a
|
|
154
|
+
// proof made without an explicit salt could never verify.
|
|
155
|
+
it('verifies a proof generated without an explicit salt', function () {
|
|
156
|
+
var proof = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'])
|
|
157
|
+
var result = ZK.verifySelectiveProof(proof, proof.credentialRoot)
|
|
158
|
+
result.valid.should.equal(true, JSON.stringify(result.errors))
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('verifies a proof generated with an explicit salt', function () {
|
|
162
|
+
var proof = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'], 'aa'.repeat(16))
|
|
163
|
+
ZK.verifySelectiveProof(proof, proof.credentialRoot).valid.should.equal(true)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('rejects a tampered disclosed value', function () {
|
|
167
|
+
var proof = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'], 'aa'.repeat(16))
|
|
168
|
+
proof.disclosedFields[0].value = 'Mallory'
|
|
169
|
+
ZK.verifySelectiveProof(proof, proof.credentialRoot).valid.should.equal(false)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('rejects a proof checked against the wrong root', function () {
|
|
173
|
+
var proof = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'], 'aa'.repeat(16))
|
|
174
|
+
ZK.verifySelectiveProof(proof, '00'.repeat(32)).valid.should.equal(false)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('rejects a field whose salt has been stripped', function () {
|
|
178
|
+
var proof = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'], 'aa'.repeat(16))
|
|
179
|
+
delete proof.disclosedFields[0].salt
|
|
180
|
+
ZK.verifySelectiveProof(proof, proof.credentialRoot).valid.should.equal(false)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Deterministic trees are still available for callers that need them, without the
|
|
184
|
+
// single-salt weakness that made them dangerous.
|
|
185
|
+
it('is deterministic given the same master salt', function () {
|
|
186
|
+
var a = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'], 'bb'.repeat(16))
|
|
187
|
+
var b = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'], 'bb'.repeat(16))
|
|
188
|
+
a.credentialRoot.should.equal(b.credentialRoot)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('gives different roots for different master salts', function () {
|
|
192
|
+
var a = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'], 'bb'.repeat(16))
|
|
193
|
+
var b = ZK.generateSelectiveProof(CRED, ['credentialSubject.name'], 'cc'.repeat(16))
|
|
194
|
+
a.credentialRoot.should.not.equal(b.credentialRoot)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// The module is reachable from the public GDAF surface, so the fixes above are not
|
|
199
|
+
// internal-only.
|
|
200
|
+
it('is exposed on the GDAF instance', function () {
|
|
201
|
+
var g = new bsv.GDAF()
|
|
202
|
+
;(typeof g.zkProver.verifyRangeProof).should.equal('function')
|
|
203
|
+
})
|
|
204
|
+
})
|