@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
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it, before, after */
4
+
5
+ // Ordinal-safe transfer (Phase 1). A 1-sat ordinal held in a covenant must be
6
+ // transferred WITHOUT burning the inscribed satoshi. The single-output
7
+ // ownershipToken sets output = input - fee, so it destroys a 1-sat UTXO; these
8
+ // tests prove (a) the single-output path refuses a 1-sat UTXO, and (b)
9
+ // transferOrdinal recreates the token at exactly 1 sat among funding outputs and
10
+ // the covenant verifies.
11
+
12
+ require('chai').should()
13
+ var bsv = require('../..')
14
+ var SC = bsv.SmartContract
15
+ var PrivateKey = bsv.PrivateKey
16
+ var Transaction = bsv.Transaction
17
+
18
+ var Token = SC.Token
19
+ var help = SC.CovenantHelpers
20
+ var fundAndSpend = help.fundAndSpend
21
+ var p2pkhOutput = help.p2pkhOutput
22
+ var verify = help.verify
23
+
24
+ describe('Ordinal-safe transfer (1-sat, SIGHASH_ALL)', function () {
25
+ this.timeout(20000)
26
+ var I = bsv.Script.Interpreter
27
+ var saved
28
+ var alice = PrivateKey.fromRandom()
29
+ var bob = PrivateKey.fromRandom()
30
+
31
+ before(function () {
32
+ saved = { el: I.MAX_SCRIPT_ELEMENT_SIZE, num: I.MAXIMUM_ELEMENT_SIZE, ops: I.MAX_OPS_PER_SCRIPT }
33
+ SC.enableGenesis()
34
+ })
35
+ after(function () {
36
+ I.MAX_SCRIPT_ELEMENT_SIZE = saved.el
37
+ I.MAXIMUM_ELEMENT_SIZE = saved.num
38
+ I.MAX_OPS_PER_SCRIPT = saved.ops
39
+ })
40
+
41
+ it('transferOrdinal recreates the 1-sat token among funding outputs and verifies', function () {
42
+ var lock = Token.ordinalLock(Token.ownerId(alice))
43
+ var bobHash = Token.ownerId(bob)
44
+ var nextToken = Token.ordinalLock(bobHash)
45
+
46
+ // Ordinal output stays exactly 1 sat; a change output carries the funding.
47
+ var tokenOutput = new Transaction.Output({ script: nextToken, satoshis: 1 })
48
+ var change = p2pkhOutput(PrivateKey.fromRandom(), 5000)
49
+
50
+ // token output at index 0 (ordinal FIFO), change after.
51
+ var spend = fundAndSpend(lock, 1, { outputs: [tokenOutput, change] }).spend
52
+ spend.inputs[0].setScript(Token.transferOrdinal(alice, bobHash, spend, 1, lock, { tokenIndex: 0 }))
53
+
54
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: 1 }).ok.should.equal(true)
55
+ // The recreated token output is still exactly 1 sat.
56
+ spend.outputs[0].satoshis.should.equal(1)
57
+ })
58
+
59
+ it('guardrail: single-output unlockTransfer refuses a 1-sat UTXO', function () {
60
+ var lock = Token.ordinalLock(Token.ownerId(alice))
61
+ var bobHash = Token.ownerId(bob)
62
+ var next = Token.ownershipToken(500, bobHash)
63
+ var spend = fundAndSpend(lock, 1, {
64
+ outputs: [new Transaction.Output({ script: next, satoshis: 1 })]
65
+ }).spend
66
+ ;(function () {
67
+ Token.unlockTransfer(alice, bobHash, spend, 1, lock)
68
+ }).should.throw(/burns|ordinal/)
69
+ })
70
+
71
+ it('anti-burn: transferOrdinal rejects a token output not equal to the ordinal value', function () {
72
+ var lock = Token.ordinalLock(Token.ownerId(alice))
73
+ var bobHash = Token.ownerId(bob)
74
+ var nextToken = Token.ordinalLock(bobHash)
75
+ // Wrong: token output is 2 sat, not the ordinal's 1 sat.
76
+ var tokenOutput = new Transaction.Output({ script: nextToken, satoshis: 2 })
77
+ var change = p2pkhOutput(PrivateKey.fromRandom(), 5000)
78
+ var spend = fundAndSpend(lock, 1, { outputs: [tokenOutput, change] }).spend
79
+ ;(function () {
80
+ Token.transferOrdinal(alice, bobHash, spend, 1, lock, { tokenIndex: 0 })
81
+ }).should.throw(/equal the ordinal|burn/)
82
+ })
83
+ })
@@ -0,0 +1,110 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it, before, after */
4
+
5
+ // Configurable SIGHASH core (Phase 2). The OP_PUSH_TX core was hard-wired to
6
+ // SIGHASH_ALL|FORKID (0x41). It can now be built under any flag; these tests prove
7
+ // the marketplace / partially-signed pattern with SIGHASH_SINGLE|ANYONECANPAY|FORKID
8
+ // (0xc3): a seller commits only to its own input and its own output, and a buyer may
9
+ // add funding inputs and change outputs without invalidating the covenant — while
10
+ // the seller's OWN output stays bound.
11
+
12
+ require('chai').should()
13
+ var bsv = require('../..')
14
+ var SC = bsv.SmartContract
15
+ var PrivateKey = bsv.PrivateKey
16
+ var Script = bsv.Script
17
+ var Opcode = bsv.Opcode
18
+ var Transaction = bsv.Transaction
19
+
20
+ var P = SC.PushTx
21
+ var help = SC.CovenantHelpers
22
+ var fundAndSpend = help.fundAndSpend
23
+ var p2pkhOutput = help.p2pkhOutput
24
+ var verify = help.verify
25
+
26
+ var SATS = 100000
27
+
28
+ describe('Configurable SIGHASH — SINGLE|ANYONECANPAY marketplace covenant', function () {
29
+ this.timeout(20000)
30
+ var I = bsv.Script.Interpreter
31
+ var saved
32
+ var flag = P.SIGHASH_SINGLE_ANYONECANPAY_FORKID // 0xc3
33
+
34
+ before(function () {
35
+ saved = { el: I.MAX_SCRIPT_ELEMENT_SIZE, num: I.MAXIMUM_ELEMENT_SIZE, ops: I.MAX_OPS_PER_SCRIPT }
36
+ SC.enableGenesis()
37
+ })
38
+ after(function () {
39
+ I.MAX_SCRIPT_ELEMENT_SIZE = saved.el
40
+ I.MAXIMUM_ELEMENT_SIZE = saved.num
41
+ I.MAX_OPS_PER_SCRIPT = saved.ops
42
+ })
43
+
44
+ // Lock: authenticate the pushed preimage under `flag`, assert the flag, succeed.
45
+ function makeLock () {
46
+ var lock = new Script()
47
+ lock.add(Opcode.OP_DUP)
48
+ P.pushTxCore(lock, { sighashType: flag })
49
+ lock.add(Opcode.OP_VERIFY)
50
+ P.assertSighashType(lock, flag)
51
+ lock.add(Opcode.OP_DROP)
52
+ lock.add(Opcode.OP_TRUE)
53
+ return lock
54
+ }
55
+
56
+ function grindUnlock (spend, lock) {
57
+ var g = P.grind(spend, 0, lock, SATS, { sighashType: flag })
58
+ spend.inputs[0].setScript(new Script().add(g.preimage))
59
+ }
60
+
61
+ it('verifies a SINGLE|ANYONECANPAY covenant spend', function () {
62
+ var lock = makeLock()
63
+ var sellerOut = p2pkhOutput(PrivateKey.fromRandom(), SATS - 500) // committed output (index 0)
64
+ var spend = fundAndSpend(lock, SATS, { outputs: [sellerOut] }).spend
65
+ grindUnlock(spend, lock)
66
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: SATS }).ok.should.equal(true)
67
+ })
68
+
69
+ it('stays valid when a buyer adds a funding input and a change output', function () {
70
+ var lock = makeLock()
71
+ var sellerOut = p2pkhOutput(PrivateKey.fromRandom(), SATS - 500)
72
+ var spend = fundAndSpend(lock, SATS, { outputs: [sellerOut] }).spend
73
+ grindUnlock(spend, lock) // seller signs input0 + output0 only
74
+
75
+ // Buyer adds a funding input and their own change output AFTER the seller signed.
76
+ spend.addInput(new Transaction.Input({
77
+ prevTxId: '11'.repeat(32), outputIndex: 0, script: Script.empty()
78
+ }), p2pkhOutput(PrivateKey.fromRandom(), 50000).script, 50000)
79
+ spend.addOutput(p2pkhOutput(PrivateKey.fromRandom(), 49000)) // buyer change at index 1
80
+
81
+ // The seller's covenant on input 0 is unaffected (ANYONECANPAY ignores the new
82
+ // input; SINGLE ignores the new output).
83
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: SATS }).ok.should.equal(true)
84
+ })
85
+
86
+ it('still binds the seller\'s OWN output (SINGLE commits output at the input index)', function () {
87
+ var lock = makeLock()
88
+ var sellerOut = p2pkhOutput(PrivateKey.fromRandom(), SATS - 500)
89
+ var spend = fundAndSpend(lock, SATS, { outputs: [sellerOut] }).spend
90
+ grindUnlock(spend, lock)
91
+
92
+ // Tamper the committed output (index 0) after signing → must fail.
93
+ spend.outputs[0] = p2pkhOutput(PrivateKey.fromRandom(), 1)
94
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: SATS }).ok.should.equal(false)
95
+ })
96
+
97
+ it('back-compat: default pushTxCore is still SIGHASH_ALL|FORKID', function () {
98
+ var lock = new Script()
99
+ lock.add(Opcode.OP_DUP)
100
+ P.pushTxCore(lock) // no opts → 0x41
101
+ lock.add(Opcode.OP_VERIFY)
102
+ P.assertSighashAll(lock)
103
+ lock.add(Opcode.OP_DROP).add(Opcode.OP_TRUE)
104
+ var out = p2pkhOutput(PrivateKey.fromRandom(), SATS - 500)
105
+ var spend = fundAndSpend(lock, SATS, { outputs: [out] }).spend
106
+ var g = P.grind(spend, 0, lock, SATS) // default sighash
107
+ spend.inputs[0].setScript(new Script().add(g.preimage))
108
+ verify(spend.inputs[0].script, lock, { tx: spend, satoshis: SATS }).ok.should.equal(true)
109
+ })
110
+ })
@@ -0,0 +1,86 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // SPV header-chain verification: linkage + per-header proof-of-work + optional
6
+ // trusted-hash anchoring. Uses the regtest max target (0x207fffff) so a header's
7
+ // PoW passes after trivial nonce grinding (no real mining needed).
8
+
9
+ require('chai').should()
10
+ var bsv = require('../..')
11
+ var SPV = bsv.SPV
12
+
13
+ function rand32 () { return bsv.crypto.Random.getRandomBuffer(32) }
14
+
15
+ // "Mine" a header at regtest difficulty: grind nonce until PoW passes (~1-2 tries).
16
+ function minedHeader (prevHashInternal, merkleInternal) {
17
+ for (var nonce = 0; nonce < 100000; nonce++) {
18
+ var h = new bsv.BlockHeader({
19
+ version: 1,
20
+ prevHash: prevHashInternal,
21
+ merkleRoot: merkleInternal,
22
+ time: 1231006505 + nonce,
23
+ bits: 0x207fffff,
24
+ nonce: nonce
25
+ })
26
+ if (h.validProofOfWork()) return h
27
+ }
28
+ throw new Error('could not mine a regtest header')
29
+ }
30
+
31
+ function chain (n) {
32
+ var hs = []
33
+ var prev = Buffer.alloc(32)
34
+ for (var i = 0; i < n; i++) {
35
+ var h = minedHeader(prev, rand32())
36
+ hs.push(h)
37
+ prev = h._getHash()
38
+ }
39
+ return hs
40
+ }
41
+
42
+ describe('SPV.verifyHeaderChain', function () {
43
+ this.timeout(10000)
44
+
45
+ it('accepts a linked, PoW-valid chain', function () {
46
+ var hs = chain(4)
47
+ var res = SPV.verifyHeaderChain(hs)
48
+ res.valid.should.equal(true)
49
+ res.count.should.equal(4)
50
+ res.anchorHash.should.equal(hs[0].id)
51
+ res.tipHash.should.equal(hs[3].id)
52
+ })
53
+
54
+ it('rejects a broken link', function () {
55
+ var hs = chain(3)
56
+ var res = SPV.verifyHeaderChain([hs[0], hs[2]]) // hs[2] links to hs[1], not hs[0]
57
+ res.valid.should.equal(false)
58
+ res.reason.should.match(/broken link/)
59
+ })
60
+
61
+ it('rejects a header that fails proof-of-work', function () {
62
+ // Mainnet-difficulty bits with nonce 0 — the hash will not meet the tiny target.
63
+ var bad = new bsv.BlockHeader({
64
+ version: 1, prevHash: Buffer.alloc(32), merkleRoot: rand32(),
65
+ time: 1231006505, bits: 0x1d00ffff, nonce: 0
66
+ })
67
+ SPV.verifyHeaderChain([bad]).valid.should.equal(false)
68
+ // ...but skipping PoW, a single header is trivially "valid".
69
+ SPV.verifyHeaderChain([bad], { requirePow: false }).valid.should.equal(true)
70
+ })
71
+
72
+ it('honours a trusted-hash anchor (tip or anchor)', function () {
73
+ var hs = chain(3)
74
+ SPV.verifyHeaderChain(hs, { trustedHash: hs[2].id }).valid.should.equal(true)
75
+ SPV.verifyHeaderChain(hs, { trustedHash: hs[0].id }).valid.should.equal(true)
76
+ var res = SPV.verifyHeaderChain(hs, { trustedHash: 'ff'.repeat(32) })
77
+ res.valid.should.equal(false)
78
+ res.reason.should.match(/trusted hash/)
79
+ })
80
+
81
+ it('accepts hex / buffer headers', function () {
82
+ var hs = chain(2)
83
+ var asHex = hs.map(function (h) { return h.toBuffer().toString('hex') })
84
+ SPV.verifyHeaderChain(asHex).valid.should.equal(true)
85
+ })
86
+ })
@@ -0,0 +1,133 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // SPV Merkle inclusion proofs, cross-checked against the codebase's own Merkle
6
+ // tree algorithm (lib/block/block.js getMerkleTree). For random trees of many
7
+ // shapes (even, odd, duplicate-last), extract the branch for each leaf and assert
8
+ // the SPV verifier recomputes the exact same root — then assert tampering fails.
9
+
10
+ require('chai').should()
11
+ var bsv = require('../..')
12
+ var SPV = bsv.SPV
13
+ var Hash = bsv.crypto.Hash
14
+
15
+ function rev (b) { return Buffer.from(b).reverse() }
16
+
17
+ // Build the flat merkle tree exactly like Block.getMerkleTree (leaves = internal-LE).
18
+ function buildTree (leaves) {
19
+ var tree = leaves.slice()
20
+ var j = 0
21
+ for (var size = leaves.length; size > 1; size = Math.floor((size + 1) / 2)) {
22
+ for (var i = 0; i < size; i += 2) {
23
+ var i2 = Math.min(i + 1, size - 1)
24
+ tree.push(Hash.sha256sha256(Buffer.concat([tree[j + i], tree[j + i2]])))
25
+ }
26
+ j += size
27
+ }
28
+ return tree
29
+ }
30
+ function rootOf (leaves) { var t = buildTree(leaves); return t[t.length - 1] }
31
+
32
+ // Extract the sibling branch (display-order hex) for a leaf, mirroring the tree layout.
33
+ function branchFor (leaves, index) {
34
+ var tree = buildTree(leaves)
35
+ var branch = []
36
+ var j = 0
37
+ var idx = index
38
+ for (var size = leaves.length; size > 1; size = Math.floor((size + 1) / 2)) {
39
+ var sib = idx ^ 1
40
+ if (sib >= size) sib = idx // odd end -> duplicate self
41
+ branch.push(rev(tree[j + sib]).toString('hex'))
42
+ j += size
43
+ idx = Math.floor(idx / 2)
44
+ }
45
+ return branch
46
+ }
47
+
48
+ function randLeaves (n) {
49
+ var a = []
50
+ for (var i = 0; i < n; i++) a.push(bsv.crypto.Random.getRandomBuffer(32))
51
+ return a
52
+ }
53
+
54
+ describe('SPV.verifyMerkleProof (cross-checked vs Block.getMerkleTree)', function () {
55
+ var sizes = [1, 2, 3, 4, 5, 7, 8, 9, 16, 21]
56
+
57
+ it('recomputes the root for every leaf across many tree shapes', function () {
58
+ sizes.forEach(function (n) {
59
+ var leaves = randLeaves(n)
60
+ var root = rev(rootOf(leaves)).toString('hex') // display order
61
+ for (var idx = 0; idx < n; idx++) {
62
+ var txid = rev(leaves[idx]).toString('hex')
63
+ var nodes = branchFor(leaves, idx)
64
+ SPV.merkleRootFromBranch(txid, idx, nodes).should.equal(root,
65
+ 'size ' + n + ' index ' + idx)
66
+ SPV.verifyMerkleProof({ txid: txid, index: idx, nodes: nodes, merkleRoot: root })
67
+ .should.equal(true)
68
+ }
69
+ })
70
+ })
71
+
72
+ it('rejects a tampered branch node', function () {
73
+ var leaves = randLeaves(6)
74
+ var root = rev(rootOf(leaves)).toString('hex')
75
+ var nodes = branchFor(leaves, 2)
76
+ nodes[0] = rev(bsv.crypto.Random.getRandomBuffer(32)).toString('hex') // wrong sibling
77
+ SPV.verifyMerkleProof({ txid: rev(leaves[2]).toString('hex'), index: 2, nodes: nodes, merkleRoot: root })
78
+ .should.equal(false)
79
+ })
80
+
81
+ it('rejects the right proof against the wrong txid', function () {
82
+ var leaves = randLeaves(8)
83
+ var root = rev(rootOf(leaves)).toString('hex')
84
+ var nodes = branchFor(leaves, 3)
85
+ var wrongTxid = rev(bsv.crypto.Random.getRandomBuffer(32)).toString('hex')
86
+ SPV.verifyMerkleProof({ txid: wrongTxid, index: 3, nodes: nodes, merkleRoot: root })
87
+ .should.equal(false)
88
+ })
89
+
90
+ it("accepts '*' as the odd-node duplicate marker", function () {
91
+ // 3 leaves: index 2 is the odd one; its level-0 sibling is itself.
92
+ var leaves = randLeaves(3)
93
+ var root = rev(rootOf(leaves)).toString('hex')
94
+ var nodes = branchFor(leaves, 2)
95
+ nodes[0] = '*' // replace the explicit self-hash with the duplicate marker
96
+ SPV.verifyMerkleProof({ txid: rev(leaves[2]).toString('hex'), index: 2, nodes: nodes, merkleRoot: root })
97
+ .should.equal(true)
98
+ })
99
+
100
+ it('single-tx block: empty branch, root == txid', function () {
101
+ var leaves = randLeaves(1)
102
+ var txid = rev(leaves[0]).toString('hex')
103
+ SPV.merkleRootFromBranch(txid, 0, []).should.equal(txid)
104
+ })
105
+
106
+ it('verifyTxInclusion binds the branch to a header merkle root', function () {
107
+ var leaves = randLeaves(5)
108
+ var rootInternal = rootOf(leaves)
109
+ var header = new bsv.BlockHeader({
110
+ version: 1,
111
+ prevHash: Buffer.alloc(32),
112
+ merkleRoot: rootInternal, // internal-LE buffer
113
+ time: 1231006505,
114
+ bits: 0x1d00ffff,
115
+ nonce: 0
116
+ })
117
+ var res = SPV.verifyTxInclusion({
118
+ txid: rev(leaves[3]).toString('hex'),
119
+ index: 3,
120
+ nodes: branchFor(leaves, 3),
121
+ header: header,
122
+ requirePow: false // synthetic header has no real PoW
123
+ })
124
+ res.rootMatches.should.equal(true)
125
+ res.valid.should.equal(true)
126
+
127
+ // Wrong index -> root mismatch.
128
+ SPV.verifyTxInclusion({
129
+ txid: rev(leaves[3]).toString('hex'), index: 1,
130
+ nodes: branchFor(leaves, 3), header: header, requirePow: false
131
+ }).valid.should.equal(false)
132
+ })
133
+ })
@@ -0,0 +1,78 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // Regression guard for the StatusList2021 revocation-bypass bug. Before the fix,
6
+ // getCredentialStatusEntry / updateStatusList read the revocation bitstring from
7
+ // the JWT payload WITHOUT verifying the signature — so anyone who could substitute
8
+ // the list JWT could flip a revoked credential back to "valid". These tests assert
9
+ // the status-list reads now fail closed.
10
+
11
+ var expect = require('chai').expect
12
+ var didweb = require('../../lib/didweb')
13
+ var statuslist = require('../../lib/statuslist')
14
+ var vcjwt = require('../../lib/vcjwt')
15
+
16
+ function tamperEncodedList (listVcJwt, newEncodedList) {
17
+ var parts = listVcJwt.split('.')
18
+ var payload = JSON.parse(vcjwt.base64UrlDecode(parts[1]).toString())
19
+ payload.vc.credentialSubject.encodedList = newEncodedList
20
+ var tamperedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
21
+ return parts[0] + '.' + tamperedPayload + '.' + parts[2] // keep original signature
22
+ }
23
+
24
+ describe('StatusList2021 fails closed (revocation-bypass regression)', function () {
25
+ var keys, docs, resolver, verifyOpts, listJwt
26
+
27
+ before(async function () {
28
+ keys = await didweb.generateIssuerKeys({ alg: 'ES256' })
29
+ docs = didweb.buildDidWebDocuments({
30
+ domain: 'example.com',
31
+ p256: { jwk: keys.publicJwk, kid: keys.kid }
32
+ })
33
+ resolver = async function () { return { jwks: docs.jwks } }
34
+ verifyOpts = { expectedIssuerDid: docs.did, didResolver: resolver }
35
+ var created = await statuslist.createStatusList({ issuerDid: docs.did, privateJwk: keys.privateJwk })
36
+ listJwt = created.listVcJwt
37
+ })
38
+
39
+ it('reads a genuine, verified status list', async function () {
40
+ var status = await statuslist.getCredentialStatusEntry(Object.assign({ listVcJwt: listJwt, index: 7 }, verifyOpts))
41
+ expect(status).to.equal('valid')
42
+ })
43
+
44
+ it('rejects a list whose bitstring was tampered without re-signing (bypass closed)', async function () {
45
+ // Build a bitstring that marks index 7 as revoked, then splice it in WITHOUT
46
+ // the issuer's signature — exactly the substitution attack the bug allowed.
47
+ var zlib = require('zlib')
48
+ var buf = Buffer.alloc(Math.ceil(100000 / 8), 0)
49
+ buf[0] = 0xff
50
+ var forgedEncoded = zlib.gzipSync(buf).toString('base64')
51
+ var forgedJwt = tamperEncodedList(listJwt, forgedEncoded)
52
+
53
+ var threw = false
54
+ try {
55
+ await statuslist.getCredentialStatusEntry(Object.assign({ listVcJwt: forgedJwt, index: 7 }, verifyOpts))
56
+ } catch (e) { threw = true }
57
+ expect(threw, 'tampered list must be rejected').to.equal(true)
58
+ })
59
+
60
+ it('refuses to read without a pinned expectedIssuerDid', async function () {
61
+ var threw = false
62
+ try {
63
+ await statuslist.getCredentialStatusEntry({ listVcJwt: listJwt, index: 7, didResolver: resolver })
64
+ } catch (e) { threw = true }
65
+ expect(threw).to.equal(true)
66
+ })
67
+
68
+ it('rejects a list signed by a different (attacker) issuer under the victim DID', async function () {
69
+ var attacker = await didweb.generateIssuerKeys({ alg: 'ES256' })
70
+ var attackerList = await statuslist.createStatusList({ issuerDid: docs.did, privateJwk: attacker.privateJwk })
71
+ // Resolver returns the VICTIM's key for docs.did; attacker's signature must fail.
72
+ var threw = false
73
+ try {
74
+ await statuslist.getCredentialStatusEntry(Object.assign({ listVcJwt: attackerList.listVcJwt, index: 7 }, verifyOpts))
75
+ } catch (e) { threw = true }
76
+ expect(threw).to.equal(true)
77
+ })
78
+ })