@smartledger/bsv 7.0.2 → 7.1.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.
@@ -117,6 +117,79 @@ describe('ECDSA', function () {
117
117
  })
118
118
  })
119
119
 
120
+ describe('nonce reuse', function () {
121
+ // Signing two different messages under one nonce reveals the private key.
122
+ var N = point.getN()
123
+ var h1 = Hash.sha256(Buffer.from('pay alice 1 coin'))
124
+ var h2 = Hash.sha256(Buffer.from('pay bob 2 coins'))
125
+
126
+ var freshSigner = function () {
127
+ return ECDSA().set({
128
+ privkey: new Privkey(BN.fromBuffer(Buffer.from(
129
+ 'fee0a1f7afebf9d2a5a80c0c98a31c709681cce195cbcd06342b517970c0be1e', 'hex')))
130
+ })
131
+ }
132
+
133
+ it('does not reuse k when one instance signs two different messages', function () {
134
+ var e = freshSigner()
135
+ var sig1 = e.set({ hashbuf: h1 }).sign().sig
136
+ var k1 = e.k
137
+ var sig2 = e.set({ hashbuf: h2 }).sign().sig
138
+ var k2 = e.k;
139
+ (k1.cmp(k2) === 0).should.equal(false)
140
+ sig1.r.eq(sig2.r).should.equal(false)
141
+ })
142
+
143
+ it('does not reuse k across signRandomK calls on one instance', function () {
144
+ var e = freshSigner()
145
+ var sig1 = e.set({ hashbuf: h1 }).signRandomK().sig
146
+ var sig2 = e.set({ hashbuf: h2 }).sign().sig
147
+ sig1.r.eq(sig2.r).should.equal(false)
148
+ })
149
+
150
+ it('does not leak the private key from two signatures by one instance', function () {
151
+ var e = freshSigner()
152
+ var d = e.privkey.bn
153
+ var sig1 = e.set({ hashbuf: h1 }).sign().sig
154
+ var sig2 = e.set({ hashbuf: h2 }).sign().sig
155
+
156
+ // toLowS may have flipped either s, so try every sign combination.
157
+ var red = BN.red(N)
158
+ var inv = function (x) { return x.toRed(red).redInvm().fromRed() }
159
+ var mul = function (a, b) { return a.toRed(red).redMul(b.toRed(red)).fromRed() }
160
+ var e1 = BN.fromBuffer(h1)
161
+ var e2 = BN.fromBuffer(h2)
162
+ var recovered = false
163
+ ;[1, -1].forEach(function (f1) {
164
+ ;[1, -1].forEach(function (f2) {
165
+ var S1 = f1 === 1 ? sig1.s : N.sub(sig1.s)
166
+ var S2 = f2 === 1 ? sig2.s : N.sub(sig2.s)
167
+ var diff = S1.sub(S2).umod(N)
168
+ if (diff.isZero()) return
169
+ var k = mul(e1.sub(e2).umod(N), inv(diff))
170
+ var guess = mul(S1.mul(k).umod(N).sub(e1).umod(N), inv(sig1.r))
171
+ if (guess.toString(16) === d.toString(16)) recovered = true
172
+ })
173
+ })
174
+ recovered.should.equal(false)
175
+ })
176
+
177
+ it('still honours an explicitly supplied k for a single signature', function () {
178
+ var k = new BN('114860389168127852803919605627759231199925249596762615988727970217268189974335', 10)
179
+ var a = freshSigner().set({ hashbuf: h1, k: k })
180
+ var b = freshSigner().set({ hashbuf: h1, k: k })
181
+ a.sign().sig.r.eq(b.sign().sig.r).should.equal(true)
182
+ a.k.eq(k).should.equal(true)
183
+ })
184
+
185
+ it('signs the same message deterministically across instances', function () {
186
+ var a = freshSigner().set({ hashbuf: h1 }).sign().sig
187
+ var b = freshSigner().set({ hashbuf: h1 }).sign().sig
188
+ a.r.eq(b.r).should.equal(true)
189
+ a.s.eq(b.s).should.equal(true)
190
+ })
191
+ })
192
+
120
193
  describe('#toPublicKey', function () {
121
194
  it('should calculate the correct public key', function () {
122
195
  ecdsa.k = new BN('114860389168127852803919605627759231199925249596762615988727970217268189974335', 10)
@@ -69,6 +69,52 @@ describe('SmartLedger Security Patches', function () {
69
69
  // change which key/message the signature is valid for.
70
70
  canonical.r.cmp(highS.r).should.equal(0)
71
71
  })
72
+
73
+ it('toCanonical() returns a new signature and leaves the original alone', function () {
74
+ var sig = ECDSA.sign(hash, privateKey)
75
+ var n = Point.getN()
76
+ var lowS = sig.s.lte(n.shrn(1)) ? sig.s : n.sub(sig.s)
77
+ var highS = new Signature({ r: sig.r, s: n.sub(lowS) })
78
+ var before = highS.s.toString()
79
+
80
+ var canonical = highS.toCanonical()
81
+ canonical.should.not.equal(highS)
82
+ highS.s.toString().should.equal(before)
83
+ })
84
+ })
85
+
86
+ describe('applySecurityPatches', function () {
87
+ var highSig = function () {
88
+ var sig = ECDSA.sign(hash, privateKey)
89
+ var n = Point.getN()
90
+ var lowS = sig.s.lte(n.shrn(1)) ? sig.s : n.sub(sig.s)
91
+ return new Signature({ r: sig.r, s: n.sub(lowS) })
92
+ }
93
+
94
+ it('accepts a canonical signature and returns it for chaining', function () {
95
+ var sig = ECDSA.sign(hash, privateKey)
96
+ sig.applySecurityPatches().should.equal(sig)
97
+ })
98
+
99
+ it('rejects a non-canonical (high-S) signature rather than rewriting it', function () {
100
+ // Rewriting s to n-s cannot protect against malleability: the rewritten
101
+ // signature verifies exactly as the original, so silently normalizing only
102
+ // hides from the caller that the signature was malleated.
103
+ var highS = highSig()
104
+ var before = highS.s.toString()
105
+
106
+ expect(function () { highS.applySecurityPatches() }).to.throw(/non-canonical/)
107
+ highS.s.toString().should.equal(before)
108
+ })
109
+
110
+ it('rejects zero and out-of-range components', function () {
111
+ expect(function () {
112
+ new Signature({ r: new BN(0), s: new BN(1) }).applySecurityPatches()
113
+ }).to.throw(/zero r or s/)
114
+ expect(function () {
115
+ new Signature({ r: new BN(1), s: Point.getN() }).applySecurityPatches()
116
+ }).to.throw(/out of range/)
117
+ })
72
118
  })
73
119
 
74
120
  describe('SmartVerify Enhanced Validation', function () {
@@ -77,12 +123,15 @@ describe('SmartLedger Security Patches', function () {
77
123
  SmartVerify.smartVerify(hash, sig, publicKey).should.equal(true)
78
124
  })
79
125
 
80
- it('accepts a malleated (high-S) signature as valid but canonicalizes it', function () {
126
+ it('rejects a malleated (high-S) signature', function () {
81
127
  var sig = ECDSA.sign(hash, privateKey)
82
128
  var n = Point.getN()
83
129
  var lowS = sig.s.lte(n.shrn(1)) ? sig.s : n.sub(sig.s)
84
130
  var highS = new Signature({ r: sig.r, s: n.sub(lowS) })
85
- SmartVerify.smartVerify(hash, highS, publicKey).should.equal(true)
131
+ SmartVerify.isCanonical(highS).should.equal(false)
132
+ SmartVerify.smartVerify(hash, highS, publicKey).should.equal(false)
133
+ // ...while the canonical form of the same signature still verifies.
134
+ SmartVerify.smartVerify(hash, new Signature({ r: sig.r, s: lowS }), publicKey).should.equal(true)
86
135
  })
87
136
 
88
137
  it('throws on an invalid (non-32-byte) message hash', function () {
@@ -2,9 +2,13 @@
2
2
 
3
3
  var _ = require('../../lib/util/_')
4
4
  var should = require('chai').should()
5
+ var expect = require('chai').expect
5
6
  var bsv = require('../..')
6
7
  var BN = bsv.crypto.BN
7
8
  var Signature = bsv.crypto.Signature
9
+ var ECDSA = bsv.crypto.ECDSA
10
+ var Hash = bsv.crypto.Hash
11
+ var PrivateKey = bsv.PrivateKey
8
12
  var JSUtil = bsv.util.js
9
13
  var Interpreter = bsv.Script.Interpreter
10
14
 
@@ -189,6 +193,83 @@ describe('Signature', function () {
189
193
  var parsed = Signature.parseDER(sig, false)
190
194
  should.exist(parsed)
191
195
  })
196
+
197
+ it('reports sneg for a padded s', function () {
198
+ // 02 21 00ab.. — s carries the pad that clears its high bit.
199
+ var sighex = '304502203e4516da7253cf068effec6b95c41221c0cf3a8e6ccb8cbf1725b562e9afde2c' +
200
+ '022100ab1e3da73d67e32045a20e0b999e049978ea8d6ee5480d485fcf2ce0d03b2ef0'
201
+ var parsed = Signature.parseDER(Buffer.from(sighex, 'hex'))
202
+ parsed.sbuf[0].should.equal(0x00)
203
+ parsed.sneg.should.equal(true)
204
+ parsed.rneg.should.equal(false)
205
+ })
206
+ })
207
+
208
+ describe('#parseDER canonicality', function () {
209
+ // A non-canonical INTEGER is a second encoding of the same number, i.e. a
210
+ // malleable signature. Strict (the default, used by fromDER/fromString and
211
+ // so by credential/proof verification) rejects them; non-strict must keep
212
+ // accepting them, because signatures already on chain are not canonical and
213
+ // fromTxFormat has to parse them.
214
+ var cases = [
215
+ { name: 'r negative (high bit set, unpadded)', hex: '3006' + '0201ff' + '020101', err: /Value of r is negative/ },
216
+ { name: 's negative (high bit set, unpadded)', hex: '3006' + '020101' + '0201ff', err: /Value of s is negative/ },
217
+ { name: 'r excessively padded', hex: '3007' + '02020011' + '020101', err: /Value of r is excessively padded/ },
218
+ { name: 's excessively padded', hex: '3007' + '020101' + '02020011', err: /Value of s is excessively padded/ },
219
+ { name: 'r zero-length', hex: '3005' + '0200' + '020101', err: /Length of r is zero/ },
220
+ { name: 's zero-length', hex: '3005' + '020101' + '0200', err: /Length of s is zero/ }
221
+ ]
222
+
223
+ cases.forEach(function (c) {
224
+ it('strict rejects ' + c.name, function () {
225
+ expect(function () {
226
+ Signature.parseDER(Buffer.from(c.hex, 'hex'))
227
+ }).to.throw(c.err)
228
+ })
229
+
230
+ it('non-strict still accepts ' + c.name + ' (chain compatibility)', function () {
231
+ should.exist(Signature.parseDER(Buffer.from(c.hex, 'hex'), false))
232
+ })
233
+ })
234
+
235
+ it('strict accepts a minimally padded integer', function () {
236
+ // 0x00ff is canonical: the pad is required to clear the high bit.
237
+ should.exist(Signature.parseDER(Buffer.from('3008020200ff020200ff', 'hex')))
238
+ })
239
+
240
+ it('strict accepts a canonical zero integer', function () {
241
+ should.exist(Signature.parseDER(Buffer.from('3006020100020100', 'hex')))
242
+ })
243
+
244
+ it('rejects a malleated re-encoding while the original still verifies', function () {
245
+ var privkey = new PrivateKey()
246
+ var hashbuf = Hash.sha256(Buffer.from('der canonicality'))
247
+ var sig = ECDSA.sign(hashbuf, privkey)
248
+ var der = sig.toDER()
249
+
250
+ ECDSA.verify(hashbuf, Signature.fromDER(der), privkey.toPublicKey()).should.equal(true)
251
+
252
+ // Re-encode r with one pad byte more than DER allows: same r, different
253
+ // bytes. The extra zero goes on top of the canonical encoding, so this is
254
+ // excess padding whether or not r needed a pad to begin with.
255
+ var canonical = function (nbuf) {
256
+ return (nbuf[0] & 0x80) ? Buffer.concat([Buffer.from([0x00]), nbuf]) : nbuf
257
+ }
258
+ var padded = Buffer.concat([Buffer.from([0x00]), canonical(sig.r.toBuffer())])
259
+ var sEnc = canonical(sig.s.toBuffer())
260
+ var body = Buffer.concat([
261
+ Buffer.from([0x02, padded.length]), padded,
262
+ Buffer.from([0x02, sEnc.length]), sEnc
263
+ ])
264
+ var malleated = Buffer.concat([Buffer.from([0x30, body.length]), body])
265
+
266
+ malleated.toString('hex').should.not.equal(der.toString('hex'))
267
+ expect(function () {
268
+ Signature.fromDER(malleated)
269
+ }).to.throw(/excessively padded/)
270
+ // The same bytes remain parseable on the non-strict (chain) path.
271
+ Signature.parseDER(malleated, false).r.eq(sig.r).should.equal(true)
272
+ })
192
273
  })
193
274
 
194
275
  describe('#toDER', function () {
@@ -0,0 +1,74 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ require('chai').should()
6
+ var expect = require('chai').expect
7
+
8
+ var bsv = require('../../index.js')
9
+
10
+ var UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
11
+
12
+ // A token id is covered by the credential's signature, keys the registry's
13
+ // registration/revocation maps, and feeds proof material — so it must be
14
+ // unpredictable, not merely distinct.
15
+ describe('LTP identifiers', function () {
16
+ var generators = [
17
+ { name: 'Right._generateUUID', fn: function () { return bsv.LTP.Right._generateUUID() }, format: UUID_V4 },
18
+ { name: 'Obligation._generateUUID', fn: function () { return bsv.LTP.Obligation._generateUUID() }, format: UUID_V4 },
19
+ { name: 'Registry._generateRegistryId', fn: function () { return bsv.LTP.Registry._generateRegistryId() }, format: /^reg_[0-9a-f]{16}$/ },
20
+ { name: 'Registry._generateAuditId', fn: function () { return bsv.LTP.Registry._generateAuditId() }, format: /^audit_[0-9a-f]{32}$/ },
21
+ { name: 'Claim._generateBatchId', fn: function () { return bsv.LTP.Claim._generateBatchId() }, format: /^[0-9a-f]{16}$/ }
22
+ ]
23
+
24
+ var withFrozenMathRandom = function (fn) {
25
+ var original = Math.random
26
+ Math.random = function () { return 0.5 }
27
+ try { return fn() } finally { Math.random = original }
28
+ }
29
+
30
+ generators.forEach(function (g) {
31
+ describe(g.name, function () {
32
+ it('keeps its documented format', function () {
33
+ g.fn().should.match(g.format)
34
+ })
35
+
36
+ it('does not repeat', function () {
37
+ var seen = {}
38
+ for (var i = 0; i < 500; i++) {
39
+ var id = g.fn()
40
+ expect(seen[id], 'duplicate id: ' + id).to.equal(undefined)
41
+ seen[id] = true
42
+ }
43
+ })
44
+
45
+ it('does not draw from the engine PRNG', function () {
46
+ withFrozenMathRandom(function () {
47
+ var seen = {}
48
+ for (var i = 0; i < 100; i++) {
49
+ var id = g.fn()
50
+ expect(seen[id], 'id is predictable from the engine PRNG: ' + id).to.equal(undefined)
51
+ seen[id] = true
52
+ }
53
+ })
54
+ })
55
+ })
56
+ })
57
+
58
+ it('issues unpredictable ids on real tokens', function () {
59
+ var type = Object.values(bsv.LTP.Right.RightTypes)[0]
60
+ var ids = {}
61
+ for (var i = 0; i < 25; i++) {
62
+ var token = bsv.LTP.Right.create(
63
+ type,
64
+ 'did:example:issuer',
65
+ 'did:example:subject',
66
+ { description: 'test right' }
67
+ )
68
+ token.id.should.match(/^urn:uuid:/)
69
+ token.id.replace('urn:uuid:', '').should.match(UUID_V4)
70
+ expect(ids[token.id], 'duplicate token id').to.equal(undefined)
71
+ ids[token.id] = true
72
+ }
73
+ })
74
+ })
@@ -1237,8 +1237,10 @@ describe('Script', function () {
1237
1237
  Script(s2).getSignatureOperationsCount(false).should.equal(20)
1238
1238
  })
1239
1239
  it('should handle P2SH-multisig-in scripts from utility', function () {
1240
- // create a well-formed signature, does not need to match pubkeys
1241
- var signature = bsv.crypto.Signature.fromString('30060201FF0201FF')
1240
+ // create a well-formed signature, does not need to match pubkeys.
1241
+ // r and s are 0x00ff: the pad is required, as a bare 0xff would have its
1242
+ // high bit set and so would not be a canonical DER integer.
1243
+ var signature = bsv.crypto.Signature.fromString('3008020200FF020200FF')
1242
1244
  var signatures = [ signature.toBuffer() ]
1243
1245
  var p2sh = Script.buildP2SHMultisigIn(pubKeyHexes, 1, signatures, {})
1244
1246
  p2sh.getSignatureOperationsCount(true).should.equal(0)
@@ -0,0 +1,72 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ require('chai').should()
6
+ var expect = require('chai').expect
7
+
8
+ var Id = require('../../lib/util/id')
9
+
10
+ var UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
11
+
12
+ describe('util/id', function () {
13
+ describe('#uuid4', function () {
14
+ it('produces a well-formed RFC 4122 v4 uuid', function () {
15
+ Id.uuid4().should.match(UUID_V4)
16
+ })
17
+
18
+ it('sets the version and variant bits on every draw', function () {
19
+ for (var i = 0; i < 200; i++) {
20
+ Id.uuid4().should.match(UUID_V4)
21
+ }
22
+ })
23
+
24
+ it('does not repeat', function () {
25
+ var seen = {}
26
+ for (var i = 0; i < 2000; i++) {
27
+ var id = Id.uuid4()
28
+ expect(seen[id], 'duplicate uuid: ' + id).to.equal(undefined)
29
+ seen[id] = true
30
+ }
31
+ })
32
+ })
33
+
34
+ describe('#randomHex', function () {
35
+ it('returns the requested number of hex characters', function () {
36
+ Id.randomHex(16).should.match(/^[0-9a-f]{16}$/)
37
+ Id.randomHex(12).should.match(/^[0-9a-f]{12}$/)
38
+ Id.randomHex(1).should.match(/^[0-9a-f]$/)
39
+ })
40
+
41
+ it('does not repeat', function () {
42
+ var seen = {}
43
+ for (var i = 0; i < 2000; i++) {
44
+ var id = Id.randomHex(16)
45
+ expect(seen[id], 'duplicate id: ' + id).to.equal(undefined)
46
+ seen[id] = true
47
+ }
48
+ })
49
+ })
50
+
51
+ describe('entropy source', function () {
52
+ // Identifiers must come from the CSPRNG. Freezing the engine PRNG must not
53
+ // affect them; if it does, they are predictable from observed output.
54
+ var withFrozenMathRandom = function (fn) {
55
+ var original = Math.random
56
+ Math.random = function () { return 0.5 }
57
+ try { return fn() } finally { Math.random = original }
58
+ }
59
+
60
+ it('uuid4 is unaffected by the engine PRNG', function () {
61
+ withFrozenMathRandom(function () {
62
+ Id.uuid4().should.not.equal(Id.uuid4())
63
+ })
64
+ })
65
+
66
+ it('randomHex is unaffected by the engine PRNG', function () {
67
+ withFrozenMathRandom(function () {
68
+ Id.randomHex(16).should.not.equal(Id.randomHex(16))
69
+ })
70
+ })
71
+ })
72
+ })
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 = '7.0.2'
3
+ module.exports = '7.1.0'