@smartledger/bsv 8.3.0 → 8.3.1

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.
@@ -0,0 +1,140 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // BRC-220 batch mode: what is in a Merkle leaf.
6
+ //
7
+ // The spec writes `leaf = SHA256(0x00 ‖ d)` and never binds `d`. That is RFC 6962's own
8
+ // generic notation for the construction, quoted to identify WHICH tree — domain
9
+ // separation, the power-of-two split, no duplicated last leaf — not to define the leaf's
10
+ // contents. `canonicalBytes` appears once in the whole document, in the proofHash
11
+ // definition, and nowhere in the batch text.
12
+ //
13
+ // This library reads `d` as proofHash. The alternative reading, d = canonicalBytes, is
14
+ // equally sound cryptographically and produces a DIFFERENT root — so the two do not
15
+ // interoperate, and the failure is silent: the fold just does not reach the root.
16
+ //
17
+ // These tests pin the choice, and pin the divergence too. A comment saying "we use
18
+ // proofHash" would not fail if someone changed it; this does.
19
+ //
20
+ // Reasoning and proposed spec text: docs/BRC220_BATCH_LEAF_AMENDMENT.md
21
+
22
+ require('chai').should()
23
+ var NotaryHash = require('../../lib/notaryhash')
24
+ var Merkle = require('../../lib/notaryhash/merkle')
25
+ var Hash = require('../../lib/crypto/hash')
26
+ var NotaryScript = require('../../lib/notaryhash/script')
27
+
28
+ // Four fixed proofHash values, standing in for a four-proof batch.
29
+ var PROOF_HASHES = [
30
+ '00'.repeat(31) + '01',
31
+ '00'.repeat(31) + '02',
32
+ 'ab'.repeat(32),
33
+ 'ff'.repeat(32)
34
+ ].map(function (h) { return Buffer.from(h, 'hex') })
35
+
36
+ function batchCertificate (index, leaves) {
37
+ return {
38
+ mode: NotaryScript.MODE.BATCH,
39
+ proofHash: leaves[index].toString('hex'),
40
+ merkle: {
41
+ root: Merkle.root(leaves).toString('hex'),
42
+ leafIndex: index,
43
+ leafCount: leaves.length,
44
+ path: Merkle.path(leaves, index).map(function (n) { return n.toString('hex') })
45
+ }
46
+ }
47
+ }
48
+
49
+ describe('BRC-220 batch leaf', function () {
50
+ describe('the leaf datum is proofHash', function () {
51
+ it('a leaf is SHA-256(0x00 || proofHash)', function () {
52
+ var proofHash = PROOF_HASHES[0]
53
+ Merkle.hashLeaf(proofHash).toString('hex').should.equal(
54
+ Hash.sha256(Buffer.concat([Buffer.from([0x00]), proofHash])).toString('hex')
55
+ )
56
+ })
57
+
58
+ // Stated the long way round, because this is the identity the spec leaves open.
59
+ it('is therefore SHA-256(0x00 || SHA-256(canonicalBytes))', function () {
60
+ var canonicalBytes = Buffer.from('any canonical proof bytes at all')
61
+ var proofHash = Hash.sha256(canonicalBytes)
62
+ Merkle.hashLeaf(proofHash).toString('hex').should.equal(
63
+ Hash.sha256(Buffer.concat([
64
+ Buffer.from([0x00]), Hash.sha256(canonicalBytes)
65
+ ])).toString('hex')
66
+ )
67
+ })
68
+
69
+ it('verifies inclusion for every leaf of a four-proof batch', function () {
70
+ for (var i = 0; i < PROOF_HASHES.length; i++) {
71
+ var report = NotaryHash.verifyBatchInclusion(batchCertificate(i, PROOF_HASHES))
72
+ report.valid.should.equal(true, 'leaf ' + i + ' should verify')
73
+ report.errors.should.deep.equal([])
74
+ }
75
+ })
76
+ })
77
+
78
+ describe('the two readings of `d` are not interchangeable', function () {
79
+ var canonicalBytesSet = [
80
+ Buffer.from('proof one canonical bytes'),
81
+ Buffer.from('proof two canonical bytes'),
82
+ Buffer.from('proof three canonical bytes'),
83
+ Buffer.from('proof four canonical bytes')
84
+ ]
85
+ var proofHashes = canonicalBytesSet.map(function (b) { return Hash.sha256(b) })
86
+
87
+ it('produces a different root under each reading', function () {
88
+ Merkle.root(proofHashes).toString('hex')
89
+ .should.not.equal(Merkle.root(canonicalBytesSet).toString('hex'))
90
+ })
91
+
92
+ // The interoperability failure, made concrete. This is what a batch built by an
93
+ // implementation using the other reading would look like to this verifier.
94
+ it('REJECTS a certificate whose tree was built over canonicalBytes', function () {
95
+ var certificate = {
96
+ mode: NotaryScript.MODE.BATCH,
97
+ proofHash: proofHashes[1].toString('hex'),
98
+ merkle: {
99
+ // Root and path from the canonicalBytes tree — the other reading.
100
+ root: Merkle.root(canonicalBytesSet).toString('hex'),
101
+ leafIndex: 1,
102
+ leafCount: 4,
103
+ path: Merkle.path(canonicalBytesSet, 1).map(function (n) { return n.toString('hex') })
104
+ }
105
+ }
106
+ var report = NotaryHash.verifyBatchInclusion(certificate)
107
+ report.valid.should.equal(false)
108
+ report.errors.should.deep.equal(['merkle inclusion proof does not fold to the batch root'])
109
+ })
110
+
111
+ // Both are sound; neither is being called wrong. The point is only that a choice had
112
+ // to be made and that it is checkable.
113
+ it('both readings are domain-separated and second-preimage resistant', function () {
114
+ var leafBytes = Buffer.alloc(64, 7)
115
+ Merkle.hashLeaf(leafBytes).toString('hex')
116
+ .should.not.equal(Merkle.hashNode(Buffer.alloc(32, 7), Buffer.alloc(32, 7)).toString('hex'))
117
+ })
118
+ })
119
+
120
+ describe('the verdict is a strict report, not a truthy object', function () {
121
+ it('reports valid:false rather than throwing on a malformed merkle object', function () {
122
+ var report = NotaryHash.verifyBatchInclusion({
123
+ mode: NotaryScript.MODE.BATCH,
124
+ proofHash: PROOF_HASHES[0].toString('hex'),
125
+ merkle: { root: 'not-hex', leafIndex: 0, leafCount: 1, path: [] }
126
+ })
127
+ report.valid.should.equal(false)
128
+ report.errors.length.should.be.above(0)
129
+ })
130
+
131
+ it('rejects a non-batch certificate by mode', function () {
132
+ var report = NotaryHash.verifyBatchInclusion({
133
+ mode: NotaryScript.MODE.FULL,
134
+ proofHash: PROOF_HASHES[0].toString('hex')
135
+ })
136
+ report.valid.should.equal(false)
137
+ report.errors.should.deep.equal(['certificate is not in batch mode'])
138
+ })
139
+ })
140
+ })
@@ -0,0 +1,112 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // BRC-220 signature interoperability.
6
+ //
7
+ // Every other test in this directory signs with `lib/crypto/ecdsa.js` and verifies
8
+ // through `lib/notaryhash/suites.js`, which also uses `lib/crypto/ecdsa.js`. That
9
+ // proves the module agrees with itself. It cannot prove the module agrees with the
10
+ // spec, and in 8.3.0 it did not: the suite set `endian: 'little'` before verifying,
11
+ // so it rejected any signature produced the way BRC-220 describes and accepted only
12
+ // signatures made with bsv's own byte-reversed convention. Twenty-odd passing tests
13
+ // said nothing about it, because they all signed the same wrong way.
14
+ //
15
+ // So this file verifies against `@noble/curves` instead — a separate implementation
16
+ // that shares no verification code with ours. It is the only test here that can fail
17
+ // when our code and our tests are wrong together.
18
+ //
19
+ // One trap worth naming, because it cost real time when this was diagnosed: noble v2
20
+ // PREHASHES by default. `secp256k1.sign(digest, key)` signs `sha256(digest)`, and
21
+ // `secp256k1.verify` prehashes to match, so noble looks self-consistent while
22
+ // disagreeing with everyone. BRC-220 signs the payloadHash itself, so every call
23
+ // below passes `{ prehash: false }`. Omit it and these tests compare the wrong things
24
+ // and "pass" for the wrong reason.
25
+
26
+ require('chai').should()
27
+ var bsv = require('../..')
28
+ var Suites = require('../../lib/notaryhash/suites')
29
+ var Hash = require('../../lib/crypto/hash')
30
+ var { secp256k1 } = require('@noble/curves/secp256k1.js')
31
+
32
+ var PAYLOAD_HASH = Hash.sha256(Buffer.from('the document nobody sees'))
33
+
34
+ // A fixed key, so a failure is reproducible rather than one-in-N flaky.
35
+ var SECRET = Buffer.from('c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee01', 'hex')
36
+ var PUBLIC_KEY = Buffer.from(secp256k1.getPublicKey(SECRET, true))
37
+
38
+ function rawSig (sig) {
39
+ return Buffer.concat([
40
+ sig.r.toArrayLike(Buffer, 'be', 32),
41
+ sig.s.toArrayLike(Buffer, 'be', 32)
42
+ ])
43
+ }
44
+
45
+ describe('BRC-220 signature interop', function () {
46
+ it('agrees with @noble/curves on the public key for a given secret', function () {
47
+ // If this fails nothing below means anything — the two libraries would be
48
+ // signing under different keys.
49
+ var ours = bsv.PrivateKey.fromBuffer(SECRET).toPublicKey().toBuffer()
50
+ ours.toString('hex').should.equal(PUBLIC_KEY.toString('hex'))
51
+ })
52
+
53
+ it('ACCEPTS a signature made the way the spec describes', function () {
54
+ // This is the regression. Under 8.3.0 this assertion fails.
55
+ var conformant = Buffer.from(secp256k1.sign(PAYLOAD_HASH, SECRET, { prehash: false }))
56
+ Suites.verify('ECDSA-secp256k1', PAYLOAD_HASH, conformant, PUBLIC_KEY)
57
+ .should.equal(true, 'a conformant BRC-220 signature must verify')
58
+ })
59
+
60
+ it('emits signatures an independent implementation accepts', function () {
61
+ var key = bsv.PrivateKey.fromBuffer(SECRET)
62
+ var ecdsa = bsv.crypto.ECDSA().set({ hashbuf: PAYLOAD_HASH, privkey: key })
63
+ ecdsa.sign()
64
+ secp256k1.verify(rawSig(ecdsa.sig), PAYLOAD_HASH, PUBLIC_KEY, { prehash: false })
65
+ .should.equal(true, 'our signature must verify under @noble/curves')
66
+ })
67
+
68
+ it('REJECTS a signature over the byte-reversed digest', function () {
69
+ // The 8.3.0 convention. It must now fail: accepting both would mean two valid
70
+ // signatures exist for one signing act, and both are inside proofHash.
71
+ var key = bsv.PrivateKey.fromBuffer(SECRET)
72
+ var ecdsa = bsv.crypto.ECDSA().set({ hashbuf: PAYLOAD_HASH, endian: 'little', privkey: key })
73
+ ecdsa.sign()
74
+ Suites.verify('ECDSA-secp256k1', PAYLOAD_HASH, rawSig(ecdsa.sig), PUBLIC_KEY)
75
+ .should.equal(false, 'the pre-8.3.1 little-endian convention must not verify')
76
+ })
77
+
78
+ it('round-trips both ways over many payloads, not just one', function () {
79
+ // A single vector can pass by coincidence — the reversal of a digest whose bytes
80
+ // happen to be near-palindromic still differs, but a lucky scalar could mask a
81
+ // subtler bug. Twenty payloads with distinct digests makes that implausible.
82
+ var key = bsv.PrivateKey.fromBuffer(SECRET)
83
+ for (var i = 0; i < 20; i++) {
84
+ var h = Hash.sha256(Buffer.from('payload ' + i))
85
+
86
+ var theirs = Buffer.from(secp256k1.sign(h, SECRET, { prehash: false }))
87
+ Suites.verify('ECDSA-secp256k1', h, theirs, PUBLIC_KEY)
88
+ .should.equal(true, 'noble sig ' + i + ' rejected by our suite')
89
+
90
+ var ecdsa = bsv.crypto.ECDSA().set({ hashbuf: h, privkey: key })
91
+ ecdsa.sign()
92
+ secp256k1.verify(rawSig(ecdsa.sig), h, PUBLIC_KEY, { prehash: false })
93
+ .should.equal(true, 'our sig ' + i + ' rejected by noble')
94
+ }
95
+ })
96
+
97
+ it('still rejects a signature over a DIFFERENT payload', function () {
98
+ // Guards against the fix degenerating into "accept anything". A verifier that
99
+ // returns true unconditionally would pass every test above.
100
+ var other = Hash.sha256(Buffer.from('a different document'))
101
+ var sig = Buffer.from(secp256k1.sign(other, SECRET, { prehash: false }))
102
+ Suites.verify('ECDSA-secp256k1', PAYLOAD_HASH, sig, PUBLIC_KEY)
103
+ .should.equal(false)
104
+ })
105
+
106
+ it('still rejects a signature from a DIFFERENT key', function () {
107
+ var otherSecret = Buffer.from('a11ce00a11ce00a11ce00a11ce00a11ce00a11ce00a11ce00a11ce00a11ce001', 'hex')
108
+ var sig = Buffer.from(secp256k1.sign(PAYLOAD_HASH, otherSecret, { prehash: false }))
109
+ Suites.verify('ECDSA-secp256k1', PAYLOAD_HASH, sig, PUBLIC_KEY)
110
+ .should.equal(false)
111
+ })
112
+ })
@@ -41,8 +41,11 @@ describe('BRC-220 verification', function () {
41
41
  publicKey = key.toPublicKey().toBuffer()
42
42
  payloadHash = Hash.sha256(Buffer.from('the document nobody sees'))
43
43
 
44
- // The signer signs the 32-byte payloadHash DIRECTLY — spec §Algorithms.
45
- var ecdsa = bsv.crypto.ECDSA().set({ hashbuf: payloadHash, endian: 'little', privkey: key })
44
+ // The signer signs the 32-byte payloadHash DIRECTLY — spec §Algorithms. No endian
45
+ // option: the digest IS the scalar, big-endian. Until 8.3.1 this line passed
46
+ // `endian: 'little'` while the comment above it said "directly", and the suite
47
+ // reversed to match, so the two agreed with each other and with nothing else.
48
+ var ecdsa = bsv.crypto.ECDSA().set({ hashbuf: payloadHash, privkey: key })
46
49
  ecdsa.sign()
47
50
  signature = rawSig(ecdsa.sig)
48
51
 
package/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  'use strict'
2
2
  // GENERATED by scripts/sync-version.js on `npm version` — do not edit.
3
- module.exports = '8.3.0'
3
+ module.exports = '8.3.1'