@smartledger/bsv 8.1.0 β 8.3.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 +166 -0
- package/README.md +38 -38
- package/bsv-ecies.min.js +1 -1
- package/bsv-gdaf.min.js +62 -62
- package/bsv-ltp.min.js +48 -48
- package/bsv-smartcontract.min.js +1 -1
- package/bsv.bundle.js +62 -62
- package/bsv.min.js +62 -62
- package/docs/AUDIT_SCOPE.md +8 -8
- package/docs/BRC220_ENCODING_AMENDMENT.md +100 -0
- package/docs/BRC220_PLAN.md +224 -0
- 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/index.js +5 -0
- package/index.mjs +1 -0
- package/lib/gdaf/attestation-signer.js +54 -2
- package/lib/gdaf/attestation-verifier.js +41 -25
- package/lib/gdaf/zk-prover.js +143 -46
- package/lib/notaryhash/certificate.js +282 -0
- package/lib/notaryhash/encoding.js +150 -0
- package/lib/notaryhash/index.js +334 -0
- package/lib/notaryhash/merkle.js +209 -0
- package/lib/notaryhash/script.js +261 -0
- package/lib/notaryhash/suites.js +156 -0
- package/lib/util/jcs.js +75 -0
- package/package.json +9 -6
- package/test/gdaf/canonicalization.js +106 -0
- package/test/gdaf/zk_prover.js +204 -0
- package/test/notaryhash/certificate.js +249 -0
- package/test/notaryhash/encoding.js +186 -0
- package/test/notaryhash/merkle.js +181 -0
- package/test/notaryhash/script.js +270 -0
- package/test/notaryhash/verify.js +339 -0
- 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
|
@@ -159,17 +159,17 @@ const recovered = bsv.reconstructSecret([shares[0], shares[2], shares[4]]);
|
|
|
159
159
|
### **New Modular Options**
|
|
160
160
|
```html
|
|
161
161
|
<!-- Core compatibility (same size as bsv@1.5.6) -->
|
|
162
|
-
<script src="https://unpkg.com/@smartledger/bsv@8.
|
|
162
|
+
<script src="https://unpkg.com/@smartledger/bsv@8.3.0/bsv.min.js"></script>
|
|
163
163
|
|
|
164
164
|
<!-- Add smart contracts when ready -->
|
|
165
|
-
<script src="https://unpkg.com/@smartledger/bsv@8.
|
|
165
|
+
<script src="https://unpkg.com/@smartledger/bsv@8.3.0/bsv-smartcontract.min.js"></script>
|
|
166
166
|
|
|
167
167
|
<!-- Add advanced features as needed -->
|
|
168
|
-
<script src="https://unpkg.com/@smartledger/bsv@8.
|
|
169
|
-
<script src="https://unpkg.com/@smartledger/bsv@8.
|
|
168
|
+
<script src="https://unpkg.com/@smartledger/bsv@8.3.0/bsv-ltp.min.js"></script>
|
|
169
|
+
<script src="https://unpkg.com/@smartledger/bsv@8.3.0/bsv-gdaf.min.js"></script>
|
|
170
170
|
|
|
171
171
|
<!-- Everything in one file -->
|
|
172
|
-
<script src="https://unpkg.com/@smartledger/bsv@8.
|
|
172
|
+
<script src="https://unpkg.com/@smartledger/bsv@8.3.0/bsv.bundle.js"></script>
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
## π **Testing Your Migration**
|
package/index.js
CHANGED
|
@@ -115,6 +115,11 @@ bsv.Block = require('./lib/block')
|
|
|
115
115
|
bsv.MerkleBlock = require('./lib/block/merkleblock')
|
|
116
116
|
bsv.BlockHeader = require('./lib/block/blockheader')
|
|
117
117
|
bsv.SPV = require('./lib/spv')
|
|
118
|
+
|
|
119
|
+
// BRC-220 NotaryHash. Exposed only now that verification exists: build/parse alone would
|
|
120
|
+
// let a caller construct a record whose proofHash means nothing, with nothing to stop
|
|
121
|
+
// them. See docs/BRC220_PLAN.md.
|
|
122
|
+
bsv.NotaryHash = require('./lib/notaryhash')
|
|
118
123
|
bsv.HDPrivateKey = require('./lib/hdprivatekey.js')
|
|
119
124
|
bsv.HDPublicKey = require('./lib/hdpublickey.js')
|
|
120
125
|
bsv.Networks = require('./lib/networks')
|
package/index.mjs
CHANGED
|
@@ -7,6 +7,7 @@ var Hash = require('../crypto/hash')
|
|
|
7
7
|
var ECDSA = require('../crypto/ecdsa')
|
|
8
8
|
var Signature = require('../crypto/signature')
|
|
9
9
|
var $ = require('../util/preconditions')
|
|
10
|
+
var JCS = require('../util/jcs')
|
|
10
11
|
var _ = require('../util/_')
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -94,16 +95,67 @@ AttestationSigner._canonicalizeJSON = function(obj) {
|
|
|
94
95
|
return JSON.stringify(AttestationSigner._sortValue(obj))
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* RFC 8785 (JCS) canonical JSON.
|
|
100
|
+
*
|
|
101
|
+
* `_canonicalizeJSON` above sorts keys and then REBUILDS an object, which loses the
|
|
102
|
+
* sort: V8 orders integer-like own properties numerically ahead of string keys whatever
|
|
103
|
+
* order they were inserted in. So `{ '10': β¦, '2': β¦ }` serialized as
|
|
104
|
+
*
|
|
105
|
+
* ours {"2":"two","10":"ten"}
|
|
106
|
+
* JCS {"10":"ten","2":"two"} (UTF-16 code-unit order)
|
|
107
|
+
*
|
|
108
|
+
* Within this library that is merely deterministic-but-nonstandard: signing and
|
|
109
|
+
* verification agree, and no forgery follows from it. Across implementations it is a
|
|
110
|
+
* verification failure β GDAF credentials exist to be checked by other parties, and a
|
|
111
|
+
* JCS-conformant verifier in any language computes a different hash and rejects a valid
|
|
112
|
+
* signature. The previous implementation acknowledged this with "do not feed integer-like
|
|
113
|
+
* keys here", which is a constraint the data cannot be relied upon to honour.
|
|
114
|
+
*
|
|
115
|
+
* This serializes directly instead of round-tripping through an object, so the order
|
|
116
|
+
* actually survives. Array.prototype.sort already compares strings by UTF-16 code unit,
|
|
117
|
+
* which is exactly what JCS specifies; the bug was never the sort.
|
|
118
|
+
*
|
|
119
|
+
* JSON.stringify is used for the leaf types on purpose: for finite numbers it produces
|
|
120
|
+
* ECMAScript Number::toString, which is what JCS mandates, and since ES2019 it emits
|
|
121
|
+
* well-formed output for lone surrogates. Both are hard to reproduce by hand and easy to
|
|
122
|
+
* get subtly wrong.
|
|
123
|
+
*
|
|
124
|
+
* @param {*} value - Value to serialize
|
|
125
|
+
* @returns {String} RFC 8785 canonical JSON
|
|
126
|
+
*/
|
|
127
|
+
AttestationSigner._canonicalizeJCS = function (value) {
|
|
128
|
+
return JCS.stringify(value)
|
|
129
|
+
}
|
|
130
|
+
|
|
97
131
|
/**
|
|
98
132
|
* Create hash of credential for signing
|
|
99
133
|
* @param {Object} credential - Credential object
|
|
100
134
|
* @returns {Buffer} SHA256 hash
|
|
101
135
|
*/
|
|
102
|
-
AttestationSigner._hashCredential = function(credential) {
|
|
103
|
-
|
|
136
|
+
AttestationSigner._hashCredential = function (credential, canonicalization) {
|
|
137
|
+
// RFC 8785 by default. `'legacy'` selects the pre-JCS sorted-key form, which is kept
|
|
138
|
+
// ONLY so credentials signed before the change still verify β see
|
|
139
|
+
// AttestationSigner.CANONICALIZATION. New signatures must never use it.
|
|
140
|
+
var canonical = canonicalization === AttestationSigner.CANONICALIZATION.LEGACY
|
|
141
|
+
? AttestationSigner._canonicalizeJSON(credential)
|
|
142
|
+
: AttestationSigner._canonicalizeJCS(credential)
|
|
104
143
|
return Hash.sha256(Buffer.from(canonical, 'utf8'))
|
|
105
144
|
}
|
|
106
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Canonicalization forms understood when hashing a credential.
|
|
148
|
+
*
|
|
149
|
+
* JCS is RFC 8785 and is what everything signs with now. LEGACY is the sorted-key form
|
|
150
|
+
* this library used before, retained so existing credentials keep verifying: it is
|
|
151
|
+
* deterministic but not interoperable, because rebuilding the object let V8 reorder
|
|
152
|
+
* integer-like keys ahead of the sort.
|
|
153
|
+
*/
|
|
154
|
+
AttestationSigner.CANONICALIZATION = {
|
|
155
|
+
JCS: 'jcs',
|
|
156
|
+
LEGACY: 'legacy'
|
|
157
|
+
}
|
|
158
|
+
|
|
107
159
|
/**
|
|
108
160
|
* Create base credential structure
|
|
109
161
|
* @param {Object} credentialSubject - Subject data
|
|
@@ -306,18 +306,23 @@ AttestationVerifier._verifySignature = async function(credential) {
|
|
|
306
306
|
delete credentialCopy.proof
|
|
307
307
|
delete credentialCopy.rootHash
|
|
308
308
|
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
//
|
|
309
|
+
// Signatures are made over RFC 8785 canonical JSON. Credentials signed before that
|
|
310
|
+
// change used the older sorted-key form, so both are tried β JCS first, so a
|
|
311
|
+
// current credential never pays for the legacy attempt. This is a compatibility
|
|
312
|
+
// path, not a weakening: each form is a complete, deterministic serialization and
|
|
313
|
+
// the signature must verify against one of them exactly.
|
|
313
314
|
var signature = AttestationVerifier._parseJWSSignature(proof.jws)
|
|
314
|
-
|
|
315
|
-
var ecdsa = new ECDSA()
|
|
316
|
-
ecdsa.hashbuf = credentialHash
|
|
317
|
-
ecdsa.pubkey = publicKey
|
|
318
|
-
ecdsa.sig = signature
|
|
319
315
|
|
|
320
|
-
var valid =
|
|
316
|
+
var valid = [
|
|
317
|
+
AttestationSigner.CANONICALIZATION.JCS,
|
|
318
|
+
AttestationSigner.CANONICALIZATION.LEGACY
|
|
319
|
+
].some(function (form) {
|
|
320
|
+
var ecdsa = new ECDSA()
|
|
321
|
+
ecdsa.hashbuf = AttestationSigner._hashCredential(credentialCopy, form)
|
|
322
|
+
ecdsa.pubkey = publicKey
|
|
323
|
+
ecdsa.sig = signature
|
|
324
|
+
return ecdsa.verify() === true
|
|
325
|
+
})
|
|
321
326
|
|
|
322
327
|
if (valid) {
|
|
323
328
|
return {
|
|
@@ -369,22 +374,24 @@ AttestationVerifier._verifyPresentationSignature = async function(presentation)
|
|
|
369
374
|
var presentationCopy = JSON.parse(JSON.stringify(presentation))
|
|
370
375
|
delete presentationCopy.proof
|
|
371
376
|
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// Verify signature
|
|
377
|
+
// Both canonicalizations, for the reason given on the credential path above:
|
|
378
|
+
// presentations signed before the move to RFC 8785 must keep verifying.
|
|
376
379
|
var signature = AttestationVerifier._parseJWSSignature(proof.jws)
|
|
377
|
-
|
|
378
|
-
var ecdsa = new ECDSA()
|
|
379
|
-
ecdsa.hashbuf = presentationHash
|
|
380
|
-
ecdsa.pubkey = publicKey
|
|
381
|
-
ecdsa.sig = signature
|
|
382
380
|
|
|
383
|
-
var
|
|
381
|
+
var presentationValid = [
|
|
382
|
+
AttestationSigner.CANONICALIZATION.JCS,
|
|
383
|
+
AttestationSigner.CANONICALIZATION.LEGACY
|
|
384
|
+
].some(function (form) {
|
|
385
|
+
var e = new ECDSA()
|
|
386
|
+
e.hashbuf = AttestationSigner._hashCredential(presentationCopy, form)
|
|
387
|
+
e.pubkey = publicKey
|
|
388
|
+
e.sig = signature
|
|
389
|
+
return e.verify() === true
|
|
390
|
+
})
|
|
384
391
|
|
|
385
392
|
return {
|
|
386
|
-
valid:
|
|
387
|
-
errors:
|
|
393
|
+
valid: presentationValid,
|
|
394
|
+
errors: presentationValid ? [] : ['Presentation signature verification failed']
|
|
388
395
|
}
|
|
389
396
|
|
|
390
397
|
} catch (error) {
|
|
@@ -541,10 +548,19 @@ AttestationVerifier.verifyCredentialHash = function(credential) {
|
|
|
541
548
|
delete credentialCopy.proof
|
|
542
549
|
delete credentialCopy.rootHash
|
|
543
550
|
|
|
544
|
-
|
|
551
|
+
// A rootHash written before the move to RFC 8785 was computed over the legacy
|
|
552
|
+
// form, so both are accepted. Buffer.compare on each is still an exact match β
|
|
553
|
+
// this widens which serialization is recognised, not what counts as equal.
|
|
545
554
|
var storedHash = Buffer.from(credential.rootHash, 'hex')
|
|
546
|
-
|
|
547
|
-
return
|
|
555
|
+
|
|
556
|
+
return [
|
|
557
|
+
AttestationSigner.CANONICALIZATION.JCS,
|
|
558
|
+
AttestationSigner.CANONICALIZATION.LEGACY
|
|
559
|
+
].some(function (form) {
|
|
560
|
+
return Buffer.compare(
|
|
561
|
+
AttestationSigner._hashCredential(credentialCopy, form), storedHash
|
|
562
|
+
) === 0
|
|
563
|
+
})
|
|
548
564
|
} catch (error) {
|
|
549
565
|
return false
|
|
550
566
|
}
|
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
|
}
|