@smartledger/bsv 5.5.3 → 6.0.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +38 -38
  3. package/bin/cli.js +35 -5
  4. package/bsv-covenant.min.js +6 -6
  5. package/bsv-gdaf.min.js +2 -2
  6. package/bsv-ltp.min.js +2 -2
  7. package/bsv-shamir.min.js +1 -1
  8. package/bsv-smartcontract.min.js +6 -6
  9. package/bsv-statuslist.min.js +5 -5
  10. package/bsv-vcjwt.min.js +1 -1
  11. package/bsv.bundle.js +2 -2
  12. package/bsv.d.ts +289 -10
  13. package/bsv.min.js +2 -2
  14. package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
  15. package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
  16. package/docs/getting-started/INSTALLATION.md +23 -23
  17. package/docs/getting-started/QUICK_START.md +7 -7
  18. package/docs/migration/FROM_BSV_1_5_6.md +5 -5
  19. package/index.js +4 -0
  20. package/lib/crypto/ecdsa.js +13 -0
  21. package/lib/crypto/shamir.js +16 -4
  22. package/lib/gdaf/did-resolver.js +3 -1
  23. package/lib/gdaf/smartledger-anchor.js +154 -12
  24. package/lib/ltp/anchor.js +16 -3
  25. package/lib/ltp/obligation.js +56 -8
  26. package/lib/ltp/proof.js +4 -2
  27. package/lib/ltp/right.js +86 -11
  28. package/lib/ordinals/README.md +170 -0
  29. package/lib/ordinals/bsv20.js +185 -0
  30. package/lib/ordinals/index.js +49 -0
  31. package/lib/ordinals/inscription.js +159 -0
  32. package/lib/ordinals/ordlock.js +560 -0
  33. package/lib/smart_contract/builder.js +27 -3
  34. package/lib/smart_contract/covenant.js +37 -10
  35. package/lib/smart_contract/covenant_builder.js +7 -5
  36. package/lib/smart_contract/index.js +28 -15
  37. package/lib/smart_contract/preimage.js +7 -7
  38. package/lib/smart_contract/pushtx.js +47 -7
  39. package/lib/smart_contract/token.js +69 -1
  40. package/lib/spv/headerchain.js +77 -0
  41. package/lib/spv/index.js +16 -0
  42. package/lib/spv/merkleproof.js +108 -0
  43. package/lib/statuslist/index.js +65 -24
  44. package/lib/vcjwt/index.js +6 -0
  45. package/package.json +1 -1
  46. package/test/credentials-test.js +22 -19
  47. package/test/crypto/shamir.js +9 -1
  48. package/test/crypto/shamir_rngcap.js +29 -0
  49. package/test/gdaf/anchor_spv.js +163 -0
  50. package/test/ltp/verify_failclosed.js +134 -0
  51. package/test/ordinals/bsv20.js +126 -0
  52. package/test/ordinals/inscription.js +71 -0
  53. package/test/ordinals/ordlock.js +552 -0
  54. package/test/smart_contract/extract_field.js +61 -0
  55. package/test/smart_contract/nonenforcing_guard.js +44 -0
  56. package/test/smart_contract/ordinal_transfer.js +83 -0
  57. package/test/smart_contract/sighash_marketplace.js +110 -0
  58. package/test/spv/headerchain.js +86 -0
  59. package/test/spv/merkleproof.js +133 -0
  60. package/test/statuslist/failclosed.js +78 -0
@@ -7,6 +7,57 @@
7
7
 
8
8
  var vcjwt = require('../vcjwt')
9
9
  var crypto = require('crypto')
10
+ var zlib = require('zlib')
11
+
12
+ // Upper bound on the decompressed bitstring to defuse gzip-bomb DoS: a tiny
13
+ // crafted payload can otherwise inflate to gigabytes. A StatusList2021 bitstring
14
+ // is 1 bit per credential, so 8 MB covers ~64M credentials — far beyond any real
15
+ // list — while refusing a malicious blow-up. Override with maxDecompressedBytes.
16
+ var MAX_DECOMPRESSED_BYTES = 8 * 1024 * 1024
17
+
18
+ function _safeGunzip (encodedList, params) {
19
+ var max = (params && params.maxDecompressedBytes) || MAX_DECOMPRESSED_BYTES
20
+ return zlib.gunzipSync(Buffer.from(encodedList, 'base64'), { maxOutputLength: max })
21
+ }
22
+
23
+ /**
24
+ * Verify the status-list JWT signature and issuer BEFORE trusting any bit it
25
+ * carries, then return the verified payload. Without this, anyone who can
26
+ * substitute the list JWT (MITM, stale cache, swapped URL, hand-crafted token)
27
+ * can flip a revoked credential back to "valid", defeating StatusList2021.
28
+ *
29
+ * Fails closed: throws unless the signature verifies against the PINNED issuer's
30
+ * key. Requires params.expectedIssuerDid plus a key source (params.didResolver,
31
+ * params.issuerJwks, or params.issuerPublicJwk).
32
+ */
33
+ async function _verifyAndDecode (listVcJwt, params) {
34
+ if (!params.expectedIssuerDid) {
35
+ throw new Error('expectedIssuerDid is required to verify a status list (issuer ' +
36
+ 'pinning); refusing to read revocation state from an unpinned/unverified JWT')
37
+ }
38
+ var resolver = params.didResolver
39
+ if (!resolver) {
40
+ var jwks = params.issuerJwks ||
41
+ (params.issuerPublicJwk ? { keys: [params.issuerPublicJwk] } : null)
42
+ if (!jwks) {
43
+ throw new Error('Provide params.didResolver, params.issuerJwks, or ' +
44
+ 'params.issuerPublicJwk to verify the status list signature')
45
+ }
46
+ resolver = async function () { return { jwks: jwks } }
47
+ }
48
+ var result = await vcjwt.verifyVcJwt(listVcJwt, {
49
+ didResolver: resolver,
50
+ expectedIssuerDid: params.expectedIssuerDid,
51
+ allowedAlgs: params.allowedAlgs,
52
+ allowLegacyDER: params.allowLegacyDER,
53
+ clockToleranceSec: params.clockToleranceSec
54
+ })
55
+ if (!result.valid) {
56
+ throw new Error('Status list signature verification failed: ' +
57
+ (result.error || 'invalid signature'))
58
+ }
59
+ return result.payload
60
+ }
10
61
 
11
62
  // Create a new status list
12
63
  async function createStatusList(params) {
@@ -65,19 +116,13 @@ async function updateStatusList(params) {
65
116
  throw new Error('listVcJwt, index, status, and privateJwk are required')
66
117
  }
67
118
 
68
- // Decode the existing status list JWT
69
- var parts = params.listVcJwt.split('.')
70
- if (parts.length !== 3) {
71
- throw new Error('Invalid JWT format')
72
- }
73
-
74
- var payload = JSON.parse(vcjwt.base64UrlDecode(parts[1]).toString())
119
+ // Verify the existing list's signature+issuer before building on top of it —
120
+ // an unverified base list lets an attacker seed forged revocation state.
121
+ var payload = await _verifyAndDecode(params.listVcJwt, params)
75
122
  var encodedList = payload.vc.credentialSubject.encodedList
76
123
 
77
- // Decompress
78
- var zlib = require('zlib')
79
- var compressed = Buffer.from(encodedList, 'base64')
80
- var bitstring = zlib.gunzipSync(compressed)
124
+ // Decompress (bounded to defuse gzip bombs)
125
+ var bitstring = _safeGunzip(encodedList, params)
81
126
 
82
127
  // Update the bit at the given index
83
128
  var byteIndex = Math.floor(params.index / 8)
@@ -124,25 +169,21 @@ async function updateStatusList(params) {
124
169
  }
125
170
  }
126
171
 
127
- // Get credential status entry
128
- function getCredentialStatusEntry(params) {
172
+ // Get credential status entry.
173
+ // ASYNC + fail-closed: the status list JWT signature and issuer are verified
174
+ // before any bit is read. Reading revocation state from an unverified JWT is a
175
+ // revocation bypass (a forged/substituted list can un-revoke a credential).
176
+ async function getCredentialStatusEntry(params) {
129
177
  if (!params.listVcJwt || params.index === undefined) {
130
178
  throw new Error('listVcJwt and index are required')
131
179
  }
132
180
 
133
- // Decode the status list JWT
134
- var parts = params.listVcJwt.split('.')
135
- if (parts.length !== 3) {
136
- throw new Error('Invalid JWT format')
137
- }
138
-
139
- var payload = JSON.parse(vcjwt.base64UrlDecode(parts[1]).toString())
181
+ // Verify signature + pinned issuer, then read from the VERIFIED payload.
182
+ var payload = await _verifyAndDecode(params.listVcJwt, params)
140
183
  var encodedList = payload.vc.credentialSubject.encodedList
141
184
 
142
- // Decompress
143
- var zlib = require('zlib')
144
- var compressed = Buffer.from(encodedList, 'base64')
145
- var bitstring = zlib.gunzipSync(compressed)
185
+ // Decompress (bounded to defuse gzip bombs)
186
+ var bitstring = _safeGunzip(encodedList, params)
146
187
 
147
188
  // Check the bit at the given index
148
189
  var byteIndex = Math.floor(params.index / 8)
@@ -151,6 +151,12 @@ async function verifyVcJwt (jwt, opts) {
151
151
  var now = Math.floor(Date.now() / 1000)
152
152
  var clockTolerance = opts.clockToleranceSec || 60
153
153
 
154
+ // A token with no `exp` never expires. Callers that require bounded-lifetime
155
+ // credentials should pass requireExpiration:true to reject exp-less tokens.
156
+ if (opts.requireExpiration && !payload.exp) {
157
+ return { valid: false, error: 'JWT missing required exp claim', header: header, payload: payload }
158
+ }
159
+
154
160
  if (payload.exp && payload.exp < (now - clockTolerance)) {
155
161
  return { valid: false, error: 'JWT expired', header: header, payload: payload }
156
162
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartledger/bsv",
3
- "version": "5.5.3",
3
+ "version": "6.0.1",
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",
@@ -141,35 +141,37 @@ async function testStatusListCreation(issuerDid, keys) {
141
141
  }
142
142
 
143
143
  // Test 7: StatusList2021 Update (Revocation)
144
- async function testStatusListRevocation(listJwt, keys) {
144
+ async function testStatusListRevocation(listJwt, keys, issuerDid, jwks) {
145
145
  console.log('\n7️⃣ Testing credential revocation...')
146
-
146
+
147
147
  var index = 42
148
-
148
+ var didResolver = async function() { return { jwks: jwks } }
149
+ var verifyOpts = { expectedIssuerDid: issuerDid, didResolver: didResolver }
150
+
149
151
  // Check initial status
150
- var initialStatus = statuslist.getCredentialStatusEntry({
152
+ var initialStatus = await statuslist.getCredentialStatusEntry(Object.assign({
151
153
  listVcJwt: listJwt,
152
154
  index: index
153
- })
154
-
155
+ }, verifyOpts))
156
+
155
157
  assert.strictEqual(initialStatus, 'valid', 'Initial status should be valid')
156
158
  console.log(' ✅ Initial status at index', index + ':', initialStatus)
157
-
159
+
158
160
  // Revoke credential
159
- var result = await statuslist.updateStatusList({
161
+ var result = await statuslist.updateStatusList(Object.assign({
160
162
  listVcJwt: listJwt,
161
163
  index: index,
162
164
  status: 'revoked',
163
165
  privateJwk: keys.privateJwk
164
- })
165
-
166
+ }, verifyOpts))
167
+
166
168
  assert(result.listVcJwt, 'Updated list should be returned')
167
-
169
+
168
170
  // Check updated status
169
- var updatedStatus = statuslist.getCredentialStatusEntry({
171
+ var updatedStatus = await statuslist.getCredentialStatusEntry(Object.assign({
170
172
  listVcJwt: result.listVcJwt,
171
173
  index: index
172
- })
174
+ }, verifyOpts))
173
175
 
174
176
  assert.strictEqual(updatedStatus, 'revoked', 'Status should be revoked')
175
177
  console.log(' ✅ Updated status at index', index + ':', updatedStatus)
@@ -276,19 +278,20 @@ async function testEndToEndWorkflow() {
276
278
  })
277
279
 
278
280
  // Revoke credential
279
- var updatedList = await statuslist.updateStatusList({
281
+ var statusVerifyOpts = { expectedIssuerDid: docs.did, didResolver: didResolver }
282
+ var updatedList = await statuslist.updateStatusList(Object.assign({
280
283
  listVcJwt: statusListResult.listVcJwt,
281
284
  index: 10,
282
285
  status: 'revoked',
283
286
  privateJwk: keys.privateJwk
284
- })
285
-
287
+ }, statusVerifyOpts))
288
+
286
289
  // Verify credential is now revoked
287
290
  var verification2 = await vcjwt.verifyVcJwt(jwt, { didResolver: didResolver })
288
- var revokedStatus = statuslist.getCredentialStatusEntry({
291
+ var revokedStatus = await statuslist.getCredentialStatusEntry(Object.assign({
289
292
  listVcJwt: updatedList.listVcJwt,
290
293
  index: 10
291
- })
294
+ }, statusVerifyOpts))
292
295
 
293
296
  assert.strictEqual(verification2.valid, true, 'JWT signature still valid')
294
297
  assert.strictEqual(revokedStatus, 'revoked', 'But credential is revoked')
@@ -306,7 +309,7 @@ async function runTests() {
306
309
  var jwt = await testVCIssuanceES256(docs.did, p256Keys)
307
310
  var verification = await testVCVerification(jwt, docs.jwks)
308
311
  var statusListJwt = await testStatusListCreation(docs.did, p256Keys)
309
- var revokedListJwt = await testStatusListRevocation(statusListJwt, p256Keys)
312
+ var revokedListJwt = await testStatusListRevocation(statusListJwt, p256Keys, docs.did, docs.jwks)
310
313
  var hash = testAnchorHashing()
311
314
  var anchorPayload = testAnchorPayload(hash, docs.did)
312
315
  await testEndToEndWorkflow()
@@ -77,7 +77,9 @@ describe('Shamir Secret Sharing', function () {
77
77
 
78
78
  describe('integrity and isolation', function () {
79
79
  it('detects a tampered share at combine time', function () {
80
- var shares = Shamir.split(crypto.randomBytes(16), 3, 5)
80
+ // checksum is opt-in (off by default to avoid leaking a hash of the secret);
81
+ // this test exercises the checksum-based detection, so enable it explicitly.
82
+ var shares = Shamir.split(crypto.randomBytes(16), 3, 5, { checksum: true })
81
83
  // flip a hex nibble in one share's data
82
84
  var s = shares[1]
83
85
  var ch = s.share[s.share.length - 1]
@@ -95,6 +97,12 @@ describe('Shamir Secret Sharing', function () {
95
97
  assert.throws(function () { Shamir.combine([a[0], b[1]]) }, /different splits|splitId/)
96
98
  })
97
99
 
100
+ it('does not embed a secret checksum by default (no leak to sub-threshold holders)', function () {
101
+ var shares = Shamir.split('low-entropy-pin-1234', 2, 3)
102
+ assert.strictEqual(shares[0].checksum, null)
103
+ assert.strictEqual(Shamir.combine(shares.slice(0, 2)).toString('utf8'), 'low-entropy-pin-1234')
104
+ })
105
+
98
106
  it('checksum can be disabled', function () {
99
107
  var shares = Shamir.split('no-checksum', 2, 3, { checksum: false })
100
108
  assert.strictEqual(shares[0].checksum, null)
@@ -0,0 +1,29 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it, afterEach */
4
+
5
+ // Regression: the secrets.js RNG callback re-draws on an all-zero draw. A broken
6
+ // or stubbed CSPRNG that returns all-zero bytes forever (e.g. a bundle mocking
7
+ // node crypto to Buffer.alloc) previously made that loop spin forever. The bounded
8
+ // loop now throws a clear error instead of hanging.
9
+
10
+ var assert = require('assert')
11
+ var Shamir = require('../../lib/crypto/shamir')
12
+ var Random = require('../../lib/crypto/random')
13
+
14
+ describe('Shamir RNG degenerate-source cap', function () {
15
+ var orig = Random.getRandomBuffer
16
+ afterEach(function () { Random.getRandomBuffer = orig })
17
+
18
+ it('throws (does not hang) when the CSPRNG is stuck returning all zeros', function () {
19
+ Random.getRandomBuffer = function (n) { return Buffer.alloc(n, 0) }
20
+ assert.throws(function () {
21
+ Shamir.split('secret', 2, 3)
22
+ }, /all-zero|degenerate/)
23
+ })
24
+
25
+ it('still splits/combines normally with a healthy CSPRNG', function () {
26
+ var shares = Shamir.split('round-trip', 2, 3)
27
+ assert.strictEqual(Shamir.combine(shares.slice(0, 2)).toString('utf8'), 'round-trip')
28
+ })
29
+ })
@@ -0,0 +1,163 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // Trustless SPV anchor verification end-to-end: build a real anchoring tx with an
6
+ // OP_RETURN commitment, place it in a block, prove its inclusion with a Merkle
7
+ // branch + header, and verify through SmartLedgerAnchor.verifyAnchor with NO trusted
8
+ // provider. Replaces the old {verified:true} stub / trust-the-caller-txData path.
9
+
10
+ require('chai').should()
11
+ var bsv = require('../..')
12
+ var Anchor = require('../../lib/gdaf/smartledger-anchor')
13
+ var Hash = bsv.crypto.Hash
14
+
15
+ function rev (b) { return Buffer.from(b).reverse() }
16
+ function buildTree (leaves) {
17
+ var tree = leaves.slice(); var j = 0
18
+ for (var size = leaves.length; size > 1; size = Math.floor((size + 1) / 2)) {
19
+ for (var i = 0; i < size; i += 2) {
20
+ var i2 = Math.min(i + 1, size - 1)
21
+ tree.push(Hash.sha256sha256(Buffer.concat([tree[j + i], tree[j + i2]])))
22
+ }
23
+ j += size
24
+ }
25
+ return tree
26
+ }
27
+ function rootOf (l) { var t = buildTree(l); return t[t.length - 1] }
28
+ function branchFor (leaves, index) {
29
+ var tree = buildTree(leaves); var branch = []; var j = 0; var idx = index
30
+ for (var size = leaves.length; size > 1; size = Math.floor((size + 1) / 2)) {
31
+ var sib = idx ^ 1; if (sib >= size) sib = idx
32
+ branch.push(rev(tree[j + sib]).toString('hex'))
33
+ j += size; idx = Math.floor(idx / 2)
34
+ }
35
+ return branch
36
+ }
37
+
38
+ function anchorTxFor (hashHex) {
39
+ var tx = new bsv.Transaction()
40
+ tx.addOutput(new bsv.Transaction.Output({
41
+ script: bsv.Script.buildDataOut(Buffer.from(hashHex, 'hex')),
42
+ satoshis: 0
43
+ }))
44
+ return tx
45
+ }
46
+
47
+ function blockOf (rootInternal) {
48
+ return new bsv.BlockHeader({
49
+ version: 1, prevHash: Buffer.alloc(32), merkleRoot: rootInternal,
50
+ time: 1231006505, bits: 0x1d00ffff, nonce: 0
51
+ })
52
+ }
53
+
54
+ // "Mine" a regtest-difficulty header (PoW passes after trivial nonce grinding).
55
+ function minedHeader (prevHashInternal, merkleInternal) {
56
+ for (var nonce = 0; nonce < 100000; nonce++) {
57
+ var h = new bsv.BlockHeader({
58
+ version: 1, prevHash: prevHashInternal, merkleRoot: merkleInternal,
59
+ time: 1231006505 + nonce, bits: 0x207fffff, nonce: nonce
60
+ })
61
+ if (h.validProofOfWork()) return h
62
+ }
63
+ throw new Error('could not mine header')
64
+ }
65
+
66
+ describe('SPV anchor verification (trustless)', function () {
67
+ var anchorHash = Hash.sha256(Buffer.from('anchor me')).toString('hex')
68
+ var atx = anchorTxFor(anchorHash)
69
+ var rawTx = atx.uncheckedSerialize()
70
+ var txid = atx.hash
71
+ // Place the anchor tx at index 2 among 5 leaves.
72
+ var leaves = [
73
+ bsv.crypto.Random.getRandomBuffer(32),
74
+ bsv.crypto.Random.getRandomBuffer(32),
75
+ atx._getHash(),
76
+ bsv.crypto.Random.getRandomBuffer(32),
77
+ bsv.crypto.Random.getRandomBuffer(32)
78
+ ]
79
+ var header = blockOf(rootOf(leaves))
80
+ var spvProof = { index: 2, nodes: branchFor(leaves, 2) }
81
+
82
+ it('verifies inclusion + commitment with a valid proof', async function () {
83
+ var res = await Anchor.verifyAnchor(txid, anchorHash, {
84
+ spvProof: spvProof, header: header, rawTx: rawTx, requirePow: false
85
+ })
86
+ res.verified.should.equal(true)
87
+ res.chainVerified.should.equal(true)
88
+ res.committed.should.equal(true)
89
+ res.txidBound.should.equal(true)
90
+ res.proof.type.should.equal('spv_merkle_inclusion')
91
+ })
92
+
93
+ it('rejects when the tx does not commit to the expected hash', async function () {
94
+ var otherHash = Hash.sha256(Buffer.from('something else')).toString('hex')
95
+ var res = await Anchor.verifyAnchor(txid, otherHash, {
96
+ spvProof: spvProof, header: header, rawTx: rawTx, requirePow: false
97
+ })
98
+ res.committed.should.equal(false)
99
+ res.verified.should.equal(false)
100
+ })
101
+
102
+ it('rejects a tampered Merkle branch (inclusion fails)', async function () {
103
+ var bad = { index: 2, nodes: branchFor(leaves, 2).slice() }
104
+ bad.nodes[0] = rev(bsv.crypto.Random.getRandomBuffer(32)).toString('hex')
105
+ var res = await Anchor.verifyAnchor(txid, anchorHash, {
106
+ spvProof: bad, header: header, rawTx: rawTx, requirePow: false
107
+ })
108
+ res.chainVerified.should.equal(false)
109
+ res.verified.should.equal(false)
110
+ })
111
+
112
+ it('rejects rawTx that does not hash to txid', async function () {
113
+ var otherRaw = anchorTxFor(Hash.sha256(Buffer.from('decoy')).toString('hex')).uncheckedSerialize()
114
+ var res = await Anchor.verifyAnchor(txid, anchorHash, {
115
+ spvProof: spvProof, header: header, rawTx: otherRaw, requirePow: false
116
+ })
117
+ res.txidBound.should.equal(false)
118
+ res.verified.should.equal(false)
119
+ })
120
+
121
+ it('still refuses to fabricate a result with neither an SPV proof nor a provider', async function () {
122
+ var threw = false
123
+ try { await Anchor.verifyAnchor(txid, anchorHash, {}) } catch (e) { threw = true }
124
+ threw.should.equal(true)
125
+ })
126
+
127
+ describe('with a header chain (confirmations under real PoW)', function () {
128
+ this.timeout(10000)
129
+ // Block containing the tx must carry the real merkle root; then descendants.
130
+ var h0 = minedHeader(Buffer.alloc(32), rootOf(leaves))
131
+ var h1 = minedHeader(h0._getHash(), bsv.crypto.Random.getRandomBuffer(32))
132
+ var h2 = minedHeader(h1._getHash(), bsv.crypto.Random.getRandomBuffer(32))
133
+ var headerChain = [h0, h1, h2]
134
+
135
+ it('verifies inclusion + 3 confirmations under real proof-of-work', async function () {
136
+ var res = await Anchor.verifyAnchor(txid, anchorHash, {
137
+ spvProof: spvProof, headerChain: headerChain, rawTx: rawTx, minConfirmations: 3
138
+ })
139
+ res.verified.should.equal(true)
140
+ res.confirmations.should.equal(3)
141
+ res.headerChainValid.should.equal(true)
142
+ res.powValid.should.equal(true)
143
+ res.tipHash.should.equal(h2.id)
144
+ res.proof.type.should.equal('spv_merkle_inclusion_with_confirmations')
145
+ })
146
+
147
+ it('rejects when confirmations are below the required minimum', async function () {
148
+ var res = await Anchor.verifyAnchor(txid, anchorHash, {
149
+ spvProof: spvProof, headerChain: headerChain, rawTx: rawTx, minConfirmations: 6
150
+ })
151
+ res.confirmations.should.equal(3)
152
+ res.verified.should.equal(false)
153
+ })
154
+
155
+ it('rejects a header chain with a broken link', async function () {
156
+ var res = await Anchor.verifyAnchor(txid, anchorHash, {
157
+ spvProof: spvProof, headerChain: [h0, h2], rawTx: rawTx, minConfirmations: 1
158
+ })
159
+ res.headerChainValid.should.equal(false)
160
+ res.verified.should.equal(false)
161
+ })
162
+ })
163
+ })
@@ -0,0 +1,134 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // Regression guard for the C1–C4 fail-open verification bugs. Before the fix,
6
+ // LTP signature verification returned the ECDSA instance (always truthy) or
7
+ // skipped the JWS check entirely (a `// TODO`), so a forged token verified as
8
+ // valid. These tests feed known-bad signatures/issuers and assert valid===false.
9
+
10
+ var expect = require('chai').expect
11
+ var bsv = require('../../')
12
+
13
+ var LTP = bsv.LTP
14
+ var RightToken = LTP.Right
15
+ var Obligation = LTP.Obligation
16
+
17
+ function newDid () {
18
+ var key = bsv.PrivateKey.fromRandom()
19
+ return { key: key, did: 'did:key:' + key.toPublicKey().toString() }
20
+ }
21
+
22
+ describe('LTP verification fails closed (C1–C4 regression)', function () {
23
+ it('a genuinely signed right token still verifies as valid', function () {
24
+ var issuer = newDid()
25
+ var result = RightToken.prepareRightToken(
26
+ 'EquityShare', issuer.did, issuer.did,
27
+ { spv: 'spv-001', units: 1000 }, issuer.key, {}
28
+ )
29
+ expect(result.success).to.equal(true)
30
+ var v = RightToken.prepareRightTokenVerification(result.rightToken, {})
31
+ expect(v.verification.valid, JSON.stringify(v.verification.errors)).to.equal(true)
32
+ })
33
+
34
+ it('a tampered JWS signature is rejected (C4)', function () {
35
+ var issuer = newDid()
36
+ var result = RightToken.prepareRightToken(
37
+ 'EquityShare', issuer.did, issuer.did, { spv: 'x' }, issuer.key, {}
38
+ )
39
+ var token = result.rightToken
40
+ // Flip the signature: replace it with a signature over different data by a
41
+ // different key, re-encoded as a valid-looking JWS.
42
+ var attacker = newDid()
43
+ var parts = token.proof.jws.split('.')
44
+ var bogusSig = bsv.crypto.ECDSA.sign(
45
+ bsv.crypto.Hash.sha256(Buffer.from('not the token')), attacker.key
46
+ )
47
+ token.proof.jws = parts[0] + '.' + parts[1] + '.' + bogusSig.toDER().toString('base64url')
48
+
49
+ var v = RightToken.prepareRightTokenVerification(token, {})
50
+ expect(v.verification.valid).to.equal(false)
51
+ })
52
+
53
+ it('a token re-signed by an attacker but attributed to the victim issuer is rejected (C4 binding)', function () {
54
+ var victim = newDid()
55
+ var attacker = newDid()
56
+ var result = RightToken.prepareRightToken(
57
+ 'PropertyTitle', victim.did, victim.did, { parcel: '1 Main' }, victim.key, {}
58
+ )
59
+ var token = result.rightToken
60
+ // Attacker re-signs the exact token hash with THEIR key, but leaves issuer =
61
+ // victim. Verification must bind to the victim issuer key and reject.
62
+ var sig = bsv.crypto.ECDSA.sign(
63
+ bsv.crypto.Hash.sha256(Buffer.from(RightToken._canonicalizeToken(token))), attacker.key
64
+ )
65
+ var parts = token.proof.jws.split('.')
66
+ token.proof.jws = parts[0] + '.' + parts[1] + '.' + sig.toDER().toString('base64url')
67
+
68
+ var v = RightToken.prepareRightTokenVerification(token, {})
69
+ expect(v.verification.valid).to.equal(false)
70
+ })
71
+
72
+ it('prepareRightTokenTransfer rejects a transfer signed by a non-owner (ownership binding)', function () {
73
+ var owner = newDid()
74
+ var attacker = newDid()
75
+ var res = RightToken.prepareRightToken(
76
+ 'EquityShare', owner.did, owner.did, { spv: 't-1' }, owner.key, {}
77
+ )
78
+ var token = res.rightToken
79
+ // Attacker tries to transfer a token they do not own → must be refused.
80
+ var bad = RightToken.prepareRightTokenTransfer(token, attacker.did, attacker.key, {})
81
+ expect(bad.success).to.equal(false)
82
+ // The genuine owner can transfer.
83
+ var good = RightToken.prepareRightTokenTransfer(token, attacker.did, owner.key, {})
84
+ expect(good.success, JSON.stringify(good.error)).to.equal(true)
85
+ })
86
+
87
+ it('verifyOwnership rejects a signature by the wrong key (C2)', function () {
88
+ var owner = newDid()
89
+ var attacker = newDid()
90
+ var Resolver = require('../../lib/gdaf/did-resolver')
91
+ var did = 'did:smartledger:' + owner.key.toPublicKey().toString()
92
+ var msg = 'transfer authorization'
93
+ // Signature by attacker, checked against owner's DID → must be false.
94
+ var badSig = bsv.crypto.ECDSA.sign(bsv.crypto.Hash.sha256(Buffer.from(msg)), attacker.key)
95
+ expect(Resolver.verifyOwnership(did, badSig.toString(), msg)).to.equal(false)
96
+ })
97
+
98
+ it('verifyAnchor refuses to fabricate success without a chain provider (C3)', async function () {
99
+ var Anchor = require('../../lib/gdaf/smartledger-anchor')
100
+ var threw = false
101
+ try {
102
+ await Anchor.verifyAnchor('deadbeef', 'cafe')
103
+ } catch (e) {
104
+ threw = true
105
+ }
106
+ expect(threw).to.equal(true)
107
+ })
108
+
109
+ it('a genuinely signed obligation token still verifies as valid', function () {
110
+ var issuer = newDid()
111
+ var ob = Obligation.prepareObligationToken(
112
+ 'PaymentObligation', issuer.did, issuer.did, { amount: 1 }, issuer.key, {}
113
+ )
114
+ expect(ob.success).to.equal(true)
115
+ var v = Obligation.prepareObligationVerification(ob.obligationToken, {})
116
+ expect(v.verification.valid, JSON.stringify(v.verification && v.verification.errors)).to.equal(true)
117
+ })
118
+
119
+ it('a tampered obligation signature is rejected (C4)', function () {
120
+ var issuer = newDid()
121
+ var attacker = newDid()
122
+ var ob = Obligation.prepareObligationToken(
123
+ 'PaymentObligation', issuer.did, issuer.did, { amount: 1 }, issuer.key, {}
124
+ )
125
+ var tok = ob.obligationToken
126
+ var sig = bsv.crypto.ECDSA.sign(
127
+ bsv.crypto.Hash.sha256(Buffer.from('forged')), attacker.key
128
+ )
129
+ var parts = tok.proof.jws.split('.')
130
+ tok.proof.jws = parts[0] + '.' + parts[1] + '.' + sig.toDER().toString('base64url')
131
+ var v = Obligation.prepareObligationVerification(tok, {})
132
+ expect(v.verification.valid).to.equal(false)
133
+ })
134
+ })