@smartledger/bsv 7.11.0 → 7.13.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.
- package/CHANGELOG.md +135 -0
- package/README.md +38 -38
- package/bsv-anchor.min.js +11 -17
- package/bsv-didweb.min.js +11 -17
- package/bsv-gdaf.min.js +69 -75
- package/bsv-ltp.min.js +55 -61
- package/bsv-mnemonic.min.js +11 -17
- package/bsv-shamir.min.js +11 -17
- package/bsv-smartcontract.min.js +1 -1
- package/bsv-statuslist.min.js +53 -59
- package/bsv-vcjwt.min.js +11 -17
- package/bsv.bundle.js +69 -75
- package/bsv.min.js +69 -75
- package/build/esbuild/crypto-shim.js +35 -0
- package/build/esbuild.js +7 -3
- package/docs/AUDIT_SCOPE.md +198 -0
- 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 +10 -2
- package/lib/crypto/hash.browser.js +13 -5
- package/lib/crypto/hash.js +58 -2
- package/lib/crypto/random.js +72 -15
- package/lib/didweb/index.js +88 -9
- package/lib/encoding/base58.js +67 -3
- package/lib/mnemonic/mnemonic.js +19 -5
- package/lib/mnemonic/pbkdf2.js +35 -2
- package/lib/script/interpreter.js +21 -2
- package/lib/transaction/input/multisig.js +1 -1
- package/lib/transaction/input/multisigscripthash.js +1 -1
- package/lib/transaction/input/publickey.js +1 -1
- package/lib/transaction/input/publickeyhash.js +1 -1
- package/lib/transaction/signature.js +1 -1
- package/lib/util/inherits.js +37 -0
- package/package.json +16 -9
- package/test/build/bundle_crypto_shim.js +104 -0
- package/test/build/bundle_externals.js +6 -1
- package/test/crypto/backend_selection.js +126 -0
- package/test/crypto/random.js +73 -0
- package/test/didweb/relationships.js +144 -0
- package/test/transaction/unspentoutput.js +6 -1
- package/version.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartledger/bsv",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.13.0",
|
|
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",
|
|
@@ -231,19 +231,15 @@
|
|
|
231
231
|
"@noble/curves": "2.2.0",
|
|
232
232
|
"@noble/hashes": "2.2.0",
|
|
233
233
|
"bn.js": "=4.12.5",
|
|
234
|
-
"
|
|
235
|
-
"clone-deep": "^4.0.1",
|
|
236
|
-
"hash.js": "^1.1.7",
|
|
237
|
-
"inherits": "2.0.4",
|
|
238
|
-
"secrets.js-grempe": "^2.0.0",
|
|
239
|
-
"unorm": "1.6.0"
|
|
234
|
+
"secrets.js-grempe": "^2.0.0"
|
|
240
235
|
},
|
|
241
236
|
"devDependencies": {
|
|
242
237
|
"assert": "^2.1.0",
|
|
243
238
|
"browserify-zlib": "^0.2.0",
|
|
244
239
|
"buffer": "^6.0.3",
|
|
245
240
|
"chai": "6.2.2",
|
|
246
|
-
"
|
|
241
|
+
"create-hash": "^1.2.0",
|
|
242
|
+
"create-hmac": "^1.1.7",
|
|
247
243
|
"esbuild": "^0.28.1",
|
|
248
244
|
"events": "^3.3.0",
|
|
249
245
|
"jose": "^5.10.0",
|
|
@@ -251,6 +247,7 @@
|
|
|
251
247
|
"nyc": "^18.0.0",
|
|
252
248
|
"path-browserify": "^1.0.1",
|
|
253
249
|
"process": "^0.11.10",
|
|
250
|
+
"randombytes": "^2.1.0",
|
|
254
251
|
"sinon": "22.1.0",
|
|
255
252
|
"standard": "12.0.1",
|
|
256
253
|
"stream-browserify": "^3.0.0",
|
|
@@ -258,6 +255,15 @@
|
|
|
258
255
|
"util": "^0.12.5",
|
|
259
256
|
"vm-browserify": "^1.1.2"
|
|
260
257
|
},
|
|
258
|
+
"overrides": {
|
|
259
|
+
"js-yaml@^3": "^3.15.1",
|
|
260
|
+
"js-yaml@^4": "^4.3.1",
|
|
261
|
+
"serialize-javascript": "^7.0.5",
|
|
262
|
+
"tmp": "^0.2.7",
|
|
263
|
+
"mocha": {
|
|
264
|
+
"diff": "^8.0.3"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
261
267
|
"license": "MIT",
|
|
262
268
|
"standard": {
|
|
263
269
|
"globals": [
|
|
@@ -266,7 +272,8 @@
|
|
|
266
272
|
"before",
|
|
267
273
|
"beforeEach",
|
|
268
274
|
"describe",
|
|
269
|
-
"it"
|
|
275
|
+
"it",
|
|
276
|
+
"globalThis"
|
|
270
277
|
],
|
|
271
278
|
"ignore": [
|
|
272
279
|
"dist/**",
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* global describe, it, before */
|
|
4
|
+
|
|
5
|
+
// The browser `crypto` polyfill is our own three-function shim, not crypto-browserify.
|
|
6
|
+
//
|
|
7
|
+
// Two things have to hold, and neither is visible to a test that stays inside lib/.
|
|
8
|
+
//
|
|
9
|
+
// 1. `elliptic` must not re-enter the bundles. It carries an advisory with no patched
|
|
10
|
+
// version, and it is reachable from crypto-browserify via browserify-sign and
|
|
11
|
+
// create-ecdh. build/esbuild.js used to keep it out with a hand-written onResolve
|
|
12
|
+
// stub; removing crypto-browserify removed the need for the stub, and this file
|
|
13
|
+
// replaces it. A stub is silent when it stops matching — an assertion is not.
|
|
14
|
+
//
|
|
15
|
+
// 2. The shim must keep implementing what the bundles actually call. If someone adds a
|
|
16
|
+
// `crypto.pbkdf2` call to a bundled module, resolution still succeeds and the member
|
|
17
|
+
// is simply `undefined`, so the failure surfaces far from its cause.
|
|
18
|
+
//
|
|
19
|
+
// The known-answer test exists because "the bundle builds" and "the bundle hashes
|
|
20
|
+
// correctly" are different claims, and only the second one matters to a caller.
|
|
21
|
+
|
|
22
|
+
require('chai').should()
|
|
23
|
+
var crypto = require('crypto')
|
|
24
|
+
var esbuild = require('../../build/esbuild.js')
|
|
25
|
+
var shim = require('../../build/esbuild/crypto-shim.js')
|
|
26
|
+
|
|
27
|
+
// Reachable from crypto-browserify; `elliptic` is the one with no fix available.
|
|
28
|
+
var BANNED = ['elliptic', 'browserify-sign', 'create-ecdh', 'crypto-browserify']
|
|
29
|
+
|
|
30
|
+
describe('browser crypto shim', function () {
|
|
31
|
+
this.timeout(300000)
|
|
32
|
+
|
|
33
|
+
var pkgsByBundle = {}
|
|
34
|
+
|
|
35
|
+
before(function () {
|
|
36
|
+
return Promise.all(esbuild.BUNDLES.map(function (cfg) {
|
|
37
|
+
return esbuild.buildOne(cfg, { metafile: true, write: false, logLevel: 'silent' })
|
|
38
|
+
.then(function (result) {
|
|
39
|
+
pkgsByBundle[cfg.file] = Object.keys(result.metafile.inputs)
|
|
40
|
+
.map(function (p) {
|
|
41
|
+
var m = /node_modules\/((?:@[^/]+\/)?[^/]+)\//.exec(p)
|
|
42
|
+
return m && m[1]
|
|
43
|
+
})
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
})
|
|
46
|
+
}))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('built every bundle', function () {
|
|
50
|
+
esbuild.BUNDLES.length.should.be.above(0)
|
|
51
|
+
Object.keys(pkgsByBundle).length.should.equal(esbuild.BUNDLES.length)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Guard against the graph being empty for an unrelated reason, which would make the
|
|
55
|
+
// "no banned packages" assertion below pass without checking anything.
|
|
56
|
+
it('resolved a non-trivial dependency graph', function () {
|
|
57
|
+
Object.keys(pkgsByBundle).forEach(function (file) {
|
|
58
|
+
pkgsByBundle[file].length.should.be.above(0, file + ' has no node_modules inputs')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('pulls no part of the elliptic chain into any bundle', function () {
|
|
63
|
+
var offenders = []
|
|
64
|
+
Object.keys(pkgsByBundle).forEach(function (file) {
|
|
65
|
+
BANNED.forEach(function (pkg) {
|
|
66
|
+
if (pkgsByBundle[file].indexOf(pkg) !== -1) offenders.push(file + ' → ' + pkg)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
offenders.should.deep.equal([])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('exports exactly the members the bundles call', function () {
|
|
73
|
+
Object.keys(shim).sort().should.deep.equal(['createHash', 'createHmac', 'randomBytes'])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Deliberately absent: secrets.js-grempe prefers getRandomValues over randomBytes when
|
|
77
|
+
// it is present, so exporting it would silently move the Shamir CSPRNG to another code
|
|
78
|
+
// path. crypto-browserify did not expose it either.
|
|
79
|
+
it('does not expose getRandomValues', function () {
|
|
80
|
+
;(shim.getRandomValues === undefined).should.equal(true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('hashes identically to node crypto', function () {
|
|
84
|
+
['', 'abc', 'The quick brown fox jumps over the lazy dog'].forEach(function (input) {
|
|
85
|
+
;['sha1', 'sha256', 'sha512', 'ripemd160'].forEach(function (alg) {
|
|
86
|
+
shim.createHash(alg).update(input).digest('hex')
|
|
87
|
+
.should.equal(crypto.createHash(alg).update(input).digest('hex'), alg + ' of ' + JSON.stringify(input))
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('hmacs identically to node crypto', function () {
|
|
93
|
+
shim.createHmac('sha512', Buffer.from('key')).update('message').digest('hex')
|
|
94
|
+
.should.equal(crypto.createHmac('sha512', Buffer.from('key')).update('message').digest('hex'))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('draws distinct random bytes of the requested length', function () {
|
|
98
|
+
var a = shim.randomBytes(32)
|
|
99
|
+
var b = shim.randomBytes(32)
|
|
100
|
+
a.length.should.equal(32)
|
|
101
|
+
b.length.should.equal(32)
|
|
102
|
+
a.toString('hex').should.not.equal(b.toString('hex'))
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -74,7 +74,12 @@ describe('feature bundles externalise the shared primitives', function () {
|
|
|
74
74
|
// Named FILES, not the directory. `lib/util/` as a prefix exempts anything added
|
|
75
75
|
// under it later, including something stateful — which is the property this test
|
|
76
76
|
// exists to prevent. Listing them means a new util file has to be considered.
|
|
77
|
-
|
|
77
|
+
//
|
|
78
|
+
// And listing ONLY what is actually embedded, for the same reason: naming a file
|
|
79
|
+
// that no bundle pulls in pre-approves it, so the consideration never happens.
|
|
80
|
+
// Measured — `lib/util/_.js` is the only one today. If a bundle starts needing
|
|
81
|
+
// another, this line should change in the same commit that causes it.
|
|
82
|
+
var SHARED = ['lib/util/_.js']
|
|
78
83
|
var EXPECTED = {
|
|
79
84
|
'bsv-ecies.min.js': ['lib/ecies/'],
|
|
80
85
|
'bsv-message.min.js': ['lib/message/'],
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* global describe, it */
|
|
4
|
+
|
|
5
|
+
// The hashing and PBKDF2 backends are chosen by CAPABILITY, not by `process.browser`.
|
|
6
|
+
//
|
|
7
|
+
// `process.browser` is a Browserify-era convention, undefined in React Native, Deno,
|
|
8
|
+
// Cloudflare Workers and Bun. Every one of those took the node branch and
|
|
9
|
+
// require('crypto')'d, which fails outright in React Native or — worse — resolves to a
|
|
10
|
+
// partial shim. rn-nodeify and several RN starter templates register exactly such a
|
|
11
|
+
// shim, so this is a reported failure mode, not a hypothetical one.
|
|
12
|
+
//
|
|
13
|
+
// Each case runs in a child process because it has to intercept module resolution
|
|
14
|
+
// before lib/ is loaded, which cannot be undone safely in-process.
|
|
15
|
+
|
|
16
|
+
require('chai').should()
|
|
17
|
+
var path = require('path')
|
|
18
|
+
var spawnSync = require('child_process').spawnSync
|
|
19
|
+
|
|
20
|
+
var ROOT = path.resolve(__dirname, '../..')
|
|
21
|
+
var SHA256_ABC = 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
|
|
22
|
+
|
|
23
|
+
// Run `body` with Module._load intercepted by `stub`, and print the result of `report`.
|
|
24
|
+
function inChild (stub, report) {
|
|
25
|
+
var src = [
|
|
26
|
+
'var Module = require("module"), orig = Module._load',
|
|
27
|
+
'var real = orig.call(Module, "crypto", module, false)',
|
|
28
|
+
'Module._load = function (r) {',
|
|
29
|
+
' if (r === "crypto") { ' + stub + ' }',
|
|
30
|
+
' return orig.apply(this, arguments)',
|
|
31
|
+
'}',
|
|
32
|
+
// Only the pure-JS backend is loaded for comparison. hash.node.js require()s
|
|
33
|
+
// 'crypto' at module scope, so it is not even loadable when the stub removes it —
|
|
34
|
+
// which is precisely why hash.js must not reach for it unconditionally.
|
|
35
|
+
'var HASH = require(' + JSON.stringify(path.join(ROOT, 'lib/crypto/hash.js')) + ')',
|
|
36
|
+
'var PURE = require(' + JSON.stringify(path.join(ROOT, 'lib/crypto/hash.browser.js')) + ')',
|
|
37
|
+
'console.log(' + report + ')'
|
|
38
|
+
].join('\n')
|
|
39
|
+
var out = spawnSync(process.execPath, ['-e', src], { encoding: 'utf8' })
|
|
40
|
+
if (out.status !== 0) throw new Error('child failed: ' + out.stderr)
|
|
41
|
+
return out.stdout.trim()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('crypto backend selection', function () {
|
|
45
|
+
this.timeout(30000)
|
|
46
|
+
|
|
47
|
+
it('uses node crypto when it is present and correct', function () {
|
|
48
|
+
// Not the pure-JS module => the node backend was selected.
|
|
49
|
+
inChild('return real', '(HASH !== PURE) + " " + HASH.sha256(Buffer.from("abc")).toString("hex")')
|
|
50
|
+
.should.equal('true ' + SHA256_ABC)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('falls back to the pure-JS backend when there is no crypto module', function () {
|
|
54
|
+
inChild('var e = new Error("no crypto"); e.code = "MODULE_NOT_FOUND"; throw e',
|
|
55
|
+
'(HASH === PURE) + " " + HASH.sha256(Buffer.from("abc")).toString("hex")')
|
|
56
|
+
.should.equal('true ' + SHA256_ABC)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// The important one. A shim that EXISTS but computes the wrong digest must be
|
|
60
|
+
// rejected — trusting it would silently corrupt every hash in the library, and a
|
|
61
|
+
// wrong hash is invisible until it matters.
|
|
62
|
+
it('rejects a crypto shim that returns incorrect digests', function () {
|
|
63
|
+
inChild(
|
|
64
|
+
'return { createHash: function () { return { update: function () { return this }, ' +
|
|
65
|
+
'digest: function () { return "deadbeef" } } } }',
|
|
66
|
+
'(HASH === PURE) + " " + HASH.sha256(Buffer.from("abc")).toString("hex")')
|
|
67
|
+
.should.equal('true ' + SHA256_ABC)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// OpenSSL 3 moved ripemd160 to the legacy provider, so some Node builds have sha256
|
|
71
|
+
// but throw on ripemd160 — and ripemd160 is what addresses are built from.
|
|
72
|
+
it('rejects a crypto build without ripemd160', function () {
|
|
73
|
+
inChild(
|
|
74
|
+
'return Object.assign({}, real, { createHash: function (a) { ' +
|
|
75
|
+
'if (a === "ripemd160") throw new Error("Digest method not supported"); ' +
|
|
76
|
+
'return real.createHash(a) } })',
|
|
77
|
+
'(HASH === PURE) + " " + HASH.sha256ripemd160(Buffer.from("abc")).toString("hex").slice(0, 16)')
|
|
78
|
+
.should.equal('true bb1be98c142444d7')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('does not consult process.browser', function () {
|
|
82
|
+
var src = [
|
|
83
|
+
'process.browser = undefined',
|
|
84
|
+
'var a = require(' + JSON.stringify(path.join(ROOT, 'lib/crypto/hash.js')) + ')',
|
|
85
|
+
'console.log(a.sha256(Buffer.from("abc")).toString("hex"))'
|
|
86
|
+
].join('\n')
|
|
87
|
+
var withoutFlag = spawnSync(process.execPath, ['-e', src], { encoding: 'utf8' })
|
|
88
|
+
var withFlag = spawnSync(process.execPath, ['-e', 'process.browser = true\n' + src.split('\n').slice(1).join('\n')], { encoding: 'utf8' })
|
|
89
|
+
withoutFlag.stdout.trim().should.equal(SHA256_ABC)
|
|
90
|
+
withFlag.stdout.trim().should.equal(SHA256_ABC)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('pbkdf2', function () {
|
|
94
|
+
// BIP-39 English test vector; the seed must not depend on which backend ran.
|
|
95
|
+
var PHRASE = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
|
96
|
+
var SEED = 'c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04'
|
|
97
|
+
|
|
98
|
+
function seedInChild (stub) {
|
|
99
|
+
var src = [
|
|
100
|
+
'var Module = require("module"), orig = Module._load',
|
|
101
|
+
'var real = orig.call(Module, "crypto", module, false)',
|
|
102
|
+
'Module._load = function (r) { if (r === "crypto") { ' + stub + ' } return orig.apply(this, arguments) }',
|
|
103
|
+
'var bsv = require(' + JSON.stringify(ROOT) + ')',
|
|
104
|
+
'console.log(new bsv.Mnemonic(' + JSON.stringify(PHRASE) + ').toSeed("TREZOR").toString("hex"))'
|
|
105
|
+
].join('\n')
|
|
106
|
+
var out = spawnSync(process.execPath, ['-e', src], { encoding: 'utf8' })
|
|
107
|
+
if (out.status !== 0) throw new Error('child failed: ' + out.stderr)
|
|
108
|
+
return out.stdout.trim()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
it('derives the BIP-39 vector with node crypto', function () {
|
|
112
|
+
seedInChild('return real').should.equal(SEED)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('derives the same seed with no crypto module', function () {
|
|
116
|
+
seedInChild('var e = new Error("no crypto"); e.code = "MODULE_NOT_FOUND"; throw e').should.equal(SEED)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('rejects a shim whose HMAC is wrong and still derives the right seed', function () {
|
|
120
|
+
seedInChild(
|
|
121
|
+
'return Object.assign({}, real, { createHmac: function () { return { ' +
|
|
122
|
+
'update: function () { return this }, digest: function () { return Buffer.alloc(64) } } } })'
|
|
123
|
+
).should.equal(SEED)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
})
|
package/test/crypto/random.js
CHANGED
|
@@ -28,5 +28,78 @@ describe('Random', function () {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
})
|
|
31
|
+
|
|
32
|
+
// WebCrypto refuses more than 65,536 bytes per call. Before chunking, a request
|
|
33
|
+
// above that limit threw; a naive implementation that ignored the return size
|
|
34
|
+
// instead would hand back a zero-filled tail, which is far worse than throwing.
|
|
35
|
+
it('fills requests larger than the 65536-byte getRandomValues limit', function () {
|
|
36
|
+
var buf = Random.getRandomBuffer(70000)
|
|
37
|
+
buf.length.should.equal(70000)
|
|
38
|
+
// The tail comes from a second call — assert it is real entropy, not zero padding.
|
|
39
|
+
var tail = buf.subarray(65536)
|
|
40
|
+
tail.some(function (b) { return b !== 0 }).should.equal(true)
|
|
41
|
+
buf.subarray(0, 32).toString('hex').should.not.equal(tail.subarray(0, 32).toString('hex'))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('handles the exact limit and one past it', function () {
|
|
45
|
+
Random.getRandomBuffer(65536).length.should.equal(65536)
|
|
46
|
+
Random.getRandomBuffer(65537).length.should.equal(65537)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// The backend is chosen by feature detection. This used to branch on
|
|
50
|
+
// `process.browser`, which is undefined in React Native, Deno, Workers and Bun —
|
|
51
|
+
// all of which then fell through to require('crypto').randomBytes. Where a partial
|
|
52
|
+
// crypto shim supplies a weak randomBytes, that produced guessable keys silently.
|
|
53
|
+
it('does not depend on process.browser', function () {
|
|
54
|
+
var saved = process.browser
|
|
55
|
+
try {
|
|
56
|
+
process.browser = undefined
|
|
57
|
+
Random.getRandomBuffer(16).length.should.equal(16)
|
|
58
|
+
process.browser = true
|
|
59
|
+
Random.getRandomBuffer(16).length.should.equal(16)
|
|
60
|
+
} finally {
|
|
61
|
+
process.browser = saved
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('prefers globalThis.crypto.getRandomValues when present', function () {
|
|
66
|
+
var calls = 0
|
|
67
|
+
var real = Object.getOwnPropertyDescriptor(globalThis, 'crypto')
|
|
68
|
+
try {
|
|
69
|
+
Object.defineProperty(globalThis, 'crypto', {
|
|
70
|
+
configurable: true,
|
|
71
|
+
value: {
|
|
72
|
+
getRandomValues: function (arr) {
|
|
73
|
+
calls++
|
|
74
|
+
for (var i = 0; i < arr.length; i++) arr[i] = (i * 7 + 3) & 0xff
|
|
75
|
+
return arr
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
var buf = Random.getRandomBuffer(4)
|
|
80
|
+
calls.should.equal(1)
|
|
81
|
+
buf.toString('hex').should.equal('030a1118')
|
|
82
|
+
} finally {
|
|
83
|
+
if (real) Object.defineProperty(globalThis, 'crypto', real)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// The throw matters as much as the detection: a key library must never return bytes
|
|
88
|
+
// it cannot vouch for. Verified in a child process because it has to remove both
|
|
89
|
+
// backends, which cannot be undone safely in-process.
|
|
90
|
+
it('throws rather than returning bytes when no CSPRNG exists', function () {
|
|
91
|
+
var out = require('child_process').spawnSync(process.execPath, ['-e', [
|
|
92
|
+
'var Module = require("module"), orig = Module._load',
|
|
93
|
+
'delete globalThis.crypto',
|
|
94
|
+
'Module._load = function (r) {',
|
|
95
|
+
' if (r === "crypto") { var e = new Error("no crypto"); e.code = "MODULE_NOT_FOUND"; throw e }',
|
|
96
|
+
' return orig.apply(this, arguments)',
|
|
97
|
+
'}',
|
|
98
|
+
'var R = require(' + JSON.stringify(require.resolve('../../lib/crypto/random.js')) + ')',
|
|
99
|
+
'try { R.getRandomBuffer(32); console.log("RETURNED") }',
|
|
100
|
+
'catch (e) { console.log("THREW") }'
|
|
101
|
+
].join('\n')], { encoding: 'utf8' })
|
|
102
|
+
out.stdout.trim().should.equal('THREW')
|
|
103
|
+
})
|
|
31
104
|
})
|
|
32
105
|
})
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/* global describe, it */
|
|
4
|
+
|
|
5
|
+
// DID Core defines the verification relationships to be DISTINCT. `authentication`
|
|
6
|
+
// proves who you are right now; `assertionMethod` makes a claim that outlives the
|
|
7
|
+
// session. Keeping them apart is what stops a low-friction login signature from being
|
|
8
|
+
// redeemed as a signed statement.
|
|
9
|
+
//
|
|
10
|
+
// This module used to cross-list every key into both, and `rotateIssuerKey` rewrote both
|
|
11
|
+
// regardless of which one was being rotated — so replacing a compromised authentication
|
|
12
|
+
// key invalidated every statement the issuer had ever signed.
|
|
13
|
+
|
|
14
|
+
require('chai').should()
|
|
15
|
+
var didweb = require('../../lib/didweb')
|
|
16
|
+
|
|
17
|
+
var DOMAIN = 'example.com'
|
|
18
|
+
var DID = 'did:web:example.com'
|
|
19
|
+
var P256 = { kid: 'auth-1', jwk: { kty: 'EC', crv: 'P-256', x: 'a', y: 'b' } }
|
|
20
|
+
var K1 = { kid: 'sign-1', jwk: { kty: 'EC', crv: 'secp256k1', x: 'c', y: 'd' } }
|
|
21
|
+
|
|
22
|
+
function scoped (key, rels) {
|
|
23
|
+
return { kid: key.kid, jwk: key.jwk, relationships: rels }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('did:web verification relationships', function () {
|
|
27
|
+
describe('buildDidWebDocuments', function () {
|
|
28
|
+
// The compatibility guarantee: callers that never heard of `relationships` must get
|
|
29
|
+
// byte-identical documents, including key order.
|
|
30
|
+
it('keeps the previous output when no relationships are given', function () {
|
|
31
|
+
var doc = didweb.buildDidWebDocuments({ domain: DOMAIN, p256: P256, k1: K1 }).didDocument
|
|
32
|
+
doc.authentication.should.deep.equal([DID + '#auth-1', DID + '#sign-1'])
|
|
33
|
+
doc.assertionMethod.should.deep.equal([DID + '#auth-1', DID + '#sign-1'])
|
|
34
|
+
Object.keys(doc).should.deep.equal([
|
|
35
|
+
'@context', 'id', 'verificationMethod', 'authentication', 'assertionMethod'
|
|
36
|
+
])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('scopes each key to the relationships it was given', function () {
|
|
40
|
+
var doc = didweb.buildDidWebDocuments({
|
|
41
|
+
domain: DOMAIN,
|
|
42
|
+
p256: scoped(P256, ['authentication']),
|
|
43
|
+
k1: scoped(K1, ['assertionMethod'])
|
|
44
|
+
}).didDocument
|
|
45
|
+
doc.authentication.should.deep.equal([DID + '#auth-1'])
|
|
46
|
+
doc.assertionMethod.should.deep.equal([DID + '#sign-1'])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('supports the other three relationships and omits empty ones', function () {
|
|
50
|
+
var doc = didweb.buildDidWebDocuments({
|
|
51
|
+
domain: DOMAIN,
|
|
52
|
+
p256: scoped(P256, ['keyAgreement', 'capabilityInvocation', 'capabilityDelegation'])
|
|
53
|
+
}).didDocument
|
|
54
|
+
doc.keyAgreement.should.deep.equal([DID + '#auth-1'])
|
|
55
|
+
doc.capabilityInvocation.should.deep.equal([DID + '#auth-1'])
|
|
56
|
+
doc.capabilityDelegation.should.deep.equal([DID + '#auth-1'])
|
|
57
|
+
;(doc.authentication === undefined).should.equal(true)
|
|
58
|
+
;(doc.assertionMethod === undefined).should.equal(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('rejects an unknown relationship rather than silently dropping it', function () {
|
|
62
|
+
;(function () {
|
|
63
|
+
didweb.buildDidWebDocuments({ domain: DOMAIN, p256: scoped(P256, ['notARelationship']) })
|
|
64
|
+
}).should.throw(/unknown verification relationship/)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('rejects an empty relationship list', function () {
|
|
68
|
+
;(function () {
|
|
69
|
+
didweb.buildDidWebDocuments({ domain: DOMAIN, p256: scoped(P256, []) })
|
|
70
|
+
}).should.throw(/non-empty array/)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('rotateIssuerKey', function () {
|
|
75
|
+
function currentDoc () {
|
|
76
|
+
return didweb.buildDidWebDocuments({
|
|
77
|
+
domain: DOMAIN,
|
|
78
|
+
p256: scoped(P256, ['authentication']),
|
|
79
|
+
k1: scoped(K1, ['assertionMethod'])
|
|
80
|
+
}).didDocument
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The reported failure. Rotating a compromised auth key must not touch
|
|
84
|
+
// assertionMethod, or every seal the issuer ever made stops verifying.
|
|
85
|
+
it('leaves assertionMethod intact when only authentication is rotated', function () {
|
|
86
|
+
var before = currentDoc()
|
|
87
|
+
var after = didweb.rotateIssuerKey({
|
|
88
|
+
domain: DOMAIN,
|
|
89
|
+
newKey: { kid: 'auth-2', jwk: { kty: 'EC', crv: 'P-256', x: 'e', y: 'f' }, relationships: ['authentication'] },
|
|
90
|
+
currentDocument: before
|
|
91
|
+
}).didDocument
|
|
92
|
+
|
|
93
|
+
after.authentication.should.deep.equal([DID + '#auth-2'])
|
|
94
|
+
after.assertionMethod.should.deep.equal(before.assertionMethod)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('still defines the key that the carried-over relationship points at', function () {
|
|
98
|
+
var after = didweb.rotateIssuerKey({
|
|
99
|
+
domain: DOMAIN,
|
|
100
|
+
newKey: { kid: 'auth-2', jwk: { kty: 'EC' }, relationships: ['authentication'] },
|
|
101
|
+
currentDocument: currentDoc()
|
|
102
|
+
}).didDocument
|
|
103
|
+
|
|
104
|
+
var defined = after.verificationMethod.map(function (vm) { return vm.id })
|
|
105
|
+
// Every referenced id resolves — no dangling references.
|
|
106
|
+
Object.keys(after).forEach(function (k) {
|
|
107
|
+
if (Array.isArray(after[k]) && k !== 'verificationMethod' && k !== '@context') {
|
|
108
|
+
after[k].forEach(function (id) { defined.indexOf(id).should.be.above(-1, k + ' references undefined ' + id) })
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('drops the rotated-out key, which nothing references any more', function () {
|
|
114
|
+
var after = didweb.rotateIssuerKey({
|
|
115
|
+
domain: DOMAIN,
|
|
116
|
+
newKey: { kid: 'auth-2', jwk: { kty: 'EC' }, relationships: ['authentication'] },
|
|
117
|
+
currentDocument: currentDoc()
|
|
118
|
+
}).didDocument
|
|
119
|
+
after.verificationMethod.some(function (vm) {
|
|
120
|
+
return vm.id === DID + '#auth-1'
|
|
121
|
+
}).should.equal(false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('keeps the previous behaviour with no currentDocument', function () {
|
|
125
|
+
var doc = didweb.rotateIssuerKey({
|
|
126
|
+
domain: DOMAIN, newKey: { kid: 'k9', jwk: { kty: 'EC' } }
|
|
127
|
+
}).didDocument
|
|
128
|
+
doc.authentication.should.deep.equal([DID + '#k9'])
|
|
129
|
+
doc.assertionMethod.should.deep.equal([DID + '#k9'])
|
|
130
|
+
doc.verificationMethod.length.should.equal(1)
|
|
131
|
+
Object.keys(doc).should.deep.equal([
|
|
132
|
+
'@context', 'id', 'verificationMethod', 'authentication', 'assertionMethod', 'rotationInfo'
|
|
133
|
+
])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('still reports rotationInfo', function () {
|
|
137
|
+
var doc = didweb.rotateIssuerKey({
|
|
138
|
+
domain: DOMAIN, newKey: { kid: 'k9', jwk: { kty: 'EC' } }, keepOldForDays: 7
|
|
139
|
+
}).didDocument
|
|
140
|
+
doc.rotationInfo.gracePeriodDays.should.equal(7)
|
|
141
|
+
doc.rotationInfo.rotatedAt.should.be.a('string')
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
var chai = require('chai')
|
|
4
4
|
var expect = chai.expect
|
|
5
|
-
|
|
5
|
+
/* global structuredClone */
|
|
6
|
+
// structuredClone, not the former clone-deep dependency: the fixture below is plain
|
|
7
|
+
// JSON (strings and numbers), where the two are equivalent. The interpreter's stack
|
|
8
|
+
// clone is NOT — it holds Buffers, which structuredClone would return as Uint8Array.
|
|
9
|
+
// The global comment is for standard@12's eslint, which predates the API.
|
|
10
|
+
var cloneDeep = structuredClone
|
|
6
11
|
|
|
7
12
|
var bsv = require('../..')
|
|
8
13
|
var UnspentOutput = bsv.Transaction.UnspentOutput
|
package/version.js
CHANGED