@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.
- package/CHANGELOG.md +142 -0
- package/README.md +38 -38
- package/bin/cli.js +35 -5
- package/bsv-covenant.min.js +6 -6
- package/bsv-gdaf.min.js +2 -2
- package/bsv-ltp.min.js +2 -2
- package/bsv-shamir.min.js +1 -1
- package/bsv-smartcontract.min.js +6 -6
- package/bsv-statuslist.min.js +5 -5
- package/bsv-vcjwt.min.js +1 -1
- package/bsv.bundle.js +2 -2
- package/bsv.d.ts +289 -10
- package/bsv.min.js +2 -2
- 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 +4 -0
- package/lib/crypto/ecdsa.js +13 -0
- package/lib/crypto/shamir.js +16 -4
- package/lib/gdaf/did-resolver.js +3 -1
- package/lib/gdaf/smartledger-anchor.js +154 -12
- package/lib/ltp/anchor.js +16 -3
- package/lib/ltp/obligation.js +56 -8
- package/lib/ltp/proof.js +4 -2
- package/lib/ltp/right.js +86 -11
- package/lib/ordinals/README.md +170 -0
- package/lib/ordinals/bsv20.js +185 -0
- package/lib/ordinals/index.js +49 -0
- package/lib/ordinals/inscription.js +159 -0
- package/lib/ordinals/ordlock.js +560 -0
- package/lib/smart_contract/builder.js +27 -3
- package/lib/smart_contract/covenant.js +37 -10
- package/lib/smart_contract/covenant_builder.js +7 -5
- package/lib/smart_contract/index.js +28 -15
- package/lib/smart_contract/preimage.js +7 -7
- package/lib/smart_contract/pushtx.js +47 -7
- package/lib/smart_contract/token.js +69 -1
- package/lib/spv/headerchain.js +77 -0
- package/lib/spv/index.js +16 -0
- package/lib/spv/merkleproof.js +108 -0
- package/lib/statuslist/index.js +65 -24
- package/lib/vcjwt/index.js +6 -0
- package/package.json +1 -1
- package/test/credentials-test.js +22 -19
- package/test/crypto/shamir.js +9 -1
- package/test/crypto/shamir_rngcap.js +29 -0
- package/test/gdaf/anchor_spv.js +163 -0
- package/test/ltp/verify_failclosed.js +134 -0
- package/test/ordinals/bsv20.js +126 -0
- package/test/ordinals/inscription.js +71 -0
- package/test/ordinals/ordlock.js +552 -0
- package/test/smart_contract/extract_field.js +61 -0
- package/test/smart_contract/nonenforcing_guard.js +44 -0
- package/test/smart_contract/ordinal_transfer.js +83 -0
- package/test/smart_contract/sighash_marketplace.js +110 -0
- package/test/spv/headerchain.js +86 -0
- package/test/spv/merkleproof.js +133 -0
- package/test/statuslist/failclosed.js +78 -0
package/lib/statuslist/index.js
CHANGED
|
@@ -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
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
134
|
-
var
|
|
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
|
|
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)
|
package/lib/vcjwt/index.js
CHANGED
|
@@ -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": "
|
|
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",
|
package/test/credentials-test.js
CHANGED
|
@@ -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
|
|
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()
|
package/test/crypto/shamir.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
})
|