@smartledger/bsv 8.1.0 → 8.3.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +38 -38
  3. package/bsv-ecies.min.js +1 -1
  4. package/bsv-gdaf.min.js +62 -62
  5. package/bsv-ltp.min.js +48 -48
  6. package/bsv-smartcontract.min.js +1 -1
  7. package/bsv.bundle.js +62 -62
  8. package/bsv.min.js +62 -62
  9. package/docs/AUDIT_SCOPE.md +8 -8
  10. package/docs/BRC220_ENCODING_AMENDMENT.md +100 -0
  11. package/docs/BRC220_PLAN.md +224 -0
  12. package/docs/MODULE_REFERENCE_COMPLETE.md +27 -27
  13. package/docs/advanced/UTXO_MANAGER_GUIDE.md +1 -1
  14. package/docs/getting-started/INSTALLATION.md +23 -23
  15. package/docs/getting-started/QUICK_START.md +7 -7
  16. package/docs/migration/FROM_BSV_1_5_6.md +5 -5
  17. package/index.js +5 -0
  18. package/index.mjs +1 -0
  19. package/lib/gdaf/attestation-signer.js +54 -2
  20. package/lib/gdaf/attestation-verifier.js +41 -25
  21. package/lib/gdaf/zk-prover.js +143 -46
  22. package/lib/notaryhash/certificate.js +282 -0
  23. package/lib/notaryhash/encoding.js +150 -0
  24. package/lib/notaryhash/index.js +334 -0
  25. package/lib/notaryhash/merkle.js +209 -0
  26. package/lib/notaryhash/script.js +261 -0
  27. package/lib/notaryhash/suites.js +156 -0
  28. package/lib/util/jcs.js +75 -0
  29. package/package.json +9 -6
  30. package/test/gdaf/canonicalization.js +106 -0
  31. package/test/gdaf/zk_prover.js +204 -0
  32. package/test/notaryhash/certificate.js +249 -0
  33. package/test/notaryhash/encoding.js +186 -0
  34. package/test/notaryhash/merkle.js +181 -0
  35. package/test/notaryhash/script.js +270 -0
  36. package/test/notaryhash/verify.js +339 -0
  37. package/tools/minimal_reproduction.js +119 -0
  38. package/tools/opcode_map.js +342 -0
  39. package/tools/server.js +97 -0
  40. package/tools/simple_real_tx.js +136 -0
  41. package/tools/sv-sighash-harness.js +123 -0
  42. package/tools/sv-sighash-report.js +71 -0
  43. package/tools/sv-tx-harness.js +172 -0
  44. package/tools/sv-tx-report.js +43 -0
  45. package/tools/sv-vector-harness.js +304 -0
  46. package/tools/sv-vector-report.js +88 -0
  47. package/version.js +1 -1
@@ -0,0 +1,261 @@
1
+ 'use strict'
2
+
3
+ var Script = require('../script/script')
4
+ var Opcode = require('../opcode')
5
+ var Hash = require('../crypto/hash')
6
+ var $ = require('../util/preconditions')
7
+
8
+ /**
9
+ * BRC-220 on-chain record: an `OP_FALSE OP_RETURN` safe data output.
10
+ *
11
+ * full (mode 0): "NOTARYHASH" | u8(1) | u8(0) | algorithm | hashAlgorithm |
12
+ * payloadHash | proofHash | publicKey | signature
13
+ * hybrid (mode 1): as full, but the last two pushes are SHA-256 of each
14
+ * batch (kind 2): "NOTARYHASH" | u8(1) | u8(2) | merkleRoot(32) | u32be(leafCount)
15
+ *
16
+ * NOTE the framing differs from the canonical proof bytes on purpose. Those use
17
+ * `lp(x) = u32be(len(x)) || x` because they are one flat byte string; here each field is
18
+ * its own SCRIPT PUSH, and the push opcode already carries the length. Applying `lp()`
19
+ * again would double-encode every field. Both achieve unambiguous boundaries; only one is
20
+ * correct in each place.
21
+ */
22
+
23
+ var NotaryScript = {}
24
+
25
+ /** Push index 0. Ten ASCII bytes; the spec states no length, the literal is the spec. */
26
+ NotaryScript.PREFIX = 'NOTARYHASH'
27
+
28
+ /** Push index 1. */
29
+ NotaryScript.VERSION = 1
30
+
31
+ /** Push index 2 — the byte that discriminates the layout. */
32
+ NotaryScript.MODE = {
33
+ FULL: 0,
34
+ HYBRID: 1,
35
+ BATCH: 2
36
+ }
37
+
38
+ function u8 (n) { return Buffer.from([n]) }
39
+
40
+ function u32be (n) {
41
+ var b = Buffer.alloc(4)
42
+ b.writeUInt32BE(n, 0)
43
+ return b
44
+ }
45
+
46
+ function requireBytes (value, name, length) {
47
+ if (!Buffer.isBuffer(value)) {
48
+ throw new Error(name + ' must be a Buffer of raw bytes, not ' + (typeof value) +
49
+ '. Decode hex with Buffer.from(hex, \'hex\') first.')
50
+ }
51
+ if (length != null && value.length !== length) {
52
+ throw new Error(name + ' must be exactly ' + length + ' bytes, got ' + value.length)
53
+ }
54
+ return value
55
+ }
56
+
57
+ /**
58
+ * Read a push that carries a single unsigned byte.
59
+ *
60
+ * A 1-byte data push is what this library emits and what the spec's `u8(...)` describes.
61
+ * `OP_0` and `OP_1`–`OP_16` are ALSO accepted, because a builder that minimally encodes
62
+ * its pushes — which is the default in several Bitcoin libraries — represents the same
63
+ * values that way, and rejecting those would reject records that are otherwise perfectly
64
+ * conformant. Being strict here would buy nothing: the mode byte is a routing hint, and
65
+ * every field that matters is checked on its own terms further down.
66
+ *
67
+ * @private
68
+ */
69
+ function readU8 (chunk, name) {
70
+ if (chunk.buf && chunk.buf.length === 1) return chunk.buf[0]
71
+ if (chunk.opcodenum === Opcode.OP_0) return 0
72
+ if (chunk.opcodenum >= Opcode.OP_1 && chunk.opcodenum <= Opcode.OP_16) {
73
+ return chunk.opcodenum - Opcode.OP_1 + 1
74
+ }
75
+ throw new Error(name + ' must be a single byte at this push')
76
+ }
77
+
78
+ function readBuf (chunk, name) {
79
+ if (!chunk || !chunk.buf) throw new Error(name + ' push is missing or carries no data')
80
+ return chunk.buf
81
+ }
82
+
83
+ /**
84
+ * Build the on-chain record.
85
+ *
86
+ * `publicKey` and `signature` are always supplied in FULL form, whatever the mode. In
87
+ * hybrid mode this function hashes them itself rather than accepting pre-hashed values —
88
+ * a caller who passed an already-hashed blob would produce a record that looks right and
89
+ * cannot be reconciled with the certificate, and nothing downstream would notice.
90
+ *
91
+ * @param {Object} record
92
+ * @param {Number} record.mode - NotaryScript.MODE.*
93
+ * @param {String} record.algorithm - full/hybrid
94
+ * @param {String} record.hashAlgorithm - full/hybrid
95
+ * @param {Buffer} record.payloadHash - full/hybrid, 32 bytes
96
+ * @param {Buffer} record.proofHash - full/hybrid, 32 bytes
97
+ * @param {Buffer} record.publicKey - full/hybrid, raw bytes
98
+ * @param {Buffer} record.signature - full/hybrid, raw bytes
99
+ * @param {Buffer} record.merkleRoot - batch, 32 bytes
100
+ * @param {Number} record.leafCount - batch
101
+ * @returns {Script}
102
+ */
103
+ NotaryScript.build = function (record) {
104
+ $.checkArgument(record && typeof record === 'object', 'record is required')
105
+
106
+ var mode = record.mode
107
+ $.checkArgument(mode === NotaryScript.MODE.FULL ||
108
+ mode === NotaryScript.MODE.HYBRID ||
109
+ mode === NotaryScript.MODE.BATCH,
110
+ 'mode must be 0 (full), 1 (hybrid) or 2 (batch)')
111
+
112
+ var script = new Script()
113
+ .add(Opcode.OP_FALSE)
114
+ .add(Opcode.OP_RETURN)
115
+ .add(Buffer.from(NotaryScript.PREFIX, 'ascii'))
116
+ .add(u8(NotaryScript.VERSION))
117
+ .add(u8(mode))
118
+
119
+ if (mode === NotaryScript.MODE.BATCH) {
120
+ $.checkArgument(typeof record.leafCount === 'number' && Number.isInteger(record.leafCount) &&
121
+ record.leafCount >= 0, 'leafCount must be a non-negative integer')
122
+ script.add(requireBytes(record.merkleRoot, 'merkleRoot', 32))
123
+ script.add(u32be(record.leafCount))
124
+ return script
125
+ }
126
+
127
+ $.checkArgument(typeof record.algorithm === 'string' && record.algorithm.length > 0,
128
+ 'algorithm must be a non-empty string')
129
+ $.checkArgument(typeof record.hashAlgorithm === 'string' && record.hashAlgorithm.length > 0,
130
+ 'hashAlgorithm must be a non-empty string')
131
+
132
+ script.add(Buffer.from(record.algorithm, 'utf8'))
133
+ script.add(Buffer.from(record.hashAlgorithm, 'utf8'))
134
+ script.add(requireBytes(record.payloadHash, 'payloadHash', 32))
135
+ script.add(requireBytes(record.proofHash, 'proofHash', 32))
136
+
137
+ var publicKey = requireBytes(record.publicKey, 'publicKey')
138
+ var signature = requireBytes(record.signature, 'signature')
139
+
140
+ if (mode === NotaryScript.MODE.HYBRID) {
141
+ script.add(Hash.sha256(publicKey))
142
+ script.add(Hash.sha256(signature))
143
+ } else {
144
+ script.add(publicKey)
145
+ script.add(signature)
146
+ }
147
+
148
+ return script
149
+ }
150
+
151
+ /**
152
+ * Parse an on-chain record.
153
+ *
154
+ * Returns a plain object, or throws with a message naming what was wrong. It never
155
+ * returns a partially-populated result: a caller that cannot distinguish "parsed" from
156
+ * "parsed some of it" is the shape that produces confident wrong answers.
157
+ *
158
+ * In hybrid mode the returned `publicKeyHash` / `signatureHash` are the on-chain digests;
159
+ * the full blobs live only in the certificate, and reconciling the two is the caller's
160
+ * job.
161
+ *
162
+ * @param {Script|String} script
163
+ * @returns {Object}
164
+ */
165
+ NotaryScript.parse = function (script) {
166
+ if (typeof script === 'string') script = Script.fromHex(script)
167
+ $.checkArgument(script instanceof Script, 'script must be a Script or hex string')
168
+
169
+ var chunks = script.chunks
170
+
171
+ if (chunks.length < 5) {
172
+ throw new Error('not a NotaryHash record: too few pushes')
173
+ }
174
+ if (chunks[0].opcodenum !== Opcode.OP_FALSE && chunks[0].opcodenum !== Opcode.OP_0) {
175
+ throw new Error('not a NotaryHash record: does not start with OP_FALSE')
176
+ }
177
+ if (chunks[1].opcodenum !== Opcode.OP_RETURN) {
178
+ throw new Error('not a NotaryHash record: no OP_RETURN')
179
+ }
180
+
181
+ var prefix = readBuf(chunks[2], 'prefix')
182
+ if (prefix.toString('ascii') !== NotaryScript.PREFIX) {
183
+ throw new Error('not a NotaryHash record: prefix is ' + JSON.stringify(prefix.toString('ascii')))
184
+ }
185
+
186
+ var version = readU8(chunks[3], 'version')
187
+ if (version !== NotaryScript.VERSION) {
188
+ throw new Error('unsupported NotaryHash version: ' + version)
189
+ }
190
+
191
+ var mode = readU8(chunks[4], 'mode')
192
+
193
+ if (mode === NotaryScript.MODE.BATCH) {
194
+ if (chunks.length !== 7) {
195
+ throw new Error('batch record must have exactly 7 pushes, got ' + chunks.length)
196
+ }
197
+ var leafCountBuf = readBuf(chunks[6], 'leafCount')
198
+ if (leafCountBuf.length !== 4) {
199
+ throw new Error('leafCount must be 4 bytes (u32be), got ' + leafCountBuf.length)
200
+ }
201
+ return {
202
+ mode: mode,
203
+ version: version,
204
+ merkleRoot: requireBytes(readBuf(chunks[5], 'merkleRoot'), 'merkleRoot', 32),
205
+ leafCount: leafCountBuf.readUInt32BE(0)
206
+ }
207
+ }
208
+
209
+ if (mode !== NotaryScript.MODE.FULL && mode !== NotaryScript.MODE.HYBRID) {
210
+ throw new Error('unknown NotaryHash mode: ' + mode)
211
+ }
212
+
213
+ if (chunks.length !== 11) {
214
+ throw new Error('full/hybrid record must have exactly 11 pushes, got ' + chunks.length)
215
+ }
216
+
217
+ var parsed = {
218
+ mode: mode,
219
+ version: version,
220
+ algorithm: readBuf(chunks[5], 'algorithm').toString('utf8'),
221
+ hashAlgorithm: readBuf(chunks[6], 'hashAlgorithm').toString('utf8'),
222
+ payloadHash: requireBytes(readBuf(chunks[7], 'payloadHash'), 'payloadHash', 32),
223
+ proofHash: requireBytes(readBuf(chunks[8], 'proofHash'), 'proofHash', 32)
224
+ }
225
+
226
+ if (mode === NotaryScript.MODE.HYBRID) {
227
+ parsed.publicKeyHash = requireBytes(readBuf(chunks[9], 'publicKeyHash'), 'publicKeyHash', 32)
228
+ parsed.signatureHash = requireBytes(readBuf(chunks[10], 'signatureHash'), 'signatureHash', 32)
229
+ } else {
230
+ parsed.publicKey = readBuf(chunks[9], 'publicKey')
231
+ parsed.signature = readBuf(chunks[10], 'signature')
232
+ }
233
+
234
+ return parsed
235
+ }
236
+
237
+ /**
238
+ * Does this script look like a NotaryHash record?
239
+ *
240
+ * Strictly a cheap filter for scanning outputs — a true result means `parse()` is worth
241
+ * attempting, NOT that the record is well-formed. Anything that acts on the contents must
242
+ * call `parse()` and handle its errors.
243
+ *
244
+ * @param {Script|String} script
245
+ * @returns {Boolean}
246
+ */
247
+ NotaryScript.isNotaryHash = function (script) {
248
+ try {
249
+ if (typeof script === 'string') script = Script.fromHex(script)
250
+ var c = script.chunks
251
+ return c.length >= 5 &&
252
+ (c[0].opcodenum === Opcode.OP_FALSE || c[0].opcodenum === Opcode.OP_0) &&
253
+ c[1].opcodenum === Opcode.OP_RETURN &&
254
+ !!c[2].buf &&
255
+ c[2].buf.toString('ascii') === NotaryScript.PREFIX
256
+ } catch (e) {
257
+ return false
258
+ }
259
+ }
260
+
261
+ module.exports = NotaryScript
@@ -0,0 +1,156 @@
1
+ 'use strict'
2
+
3
+ var BN = require('../crypto/bn')
4
+ var ECDSA = require('../crypto/ecdsa')
5
+ var Signature = require('../crypto/signature')
6
+ var PublicKey = require('../publickey')
7
+ var Point = require('../crypto/point')
8
+ var $ = require('../util/preconditions')
9
+
10
+ /**
11
+ * Signature suites, keyed on the `algorithm` string BRC-220 already carries.
12
+ *
13
+ * `ECDSA-secp256k1` is registered here and needs nothing new. ML-DSA and SLH-DSA are NOT
14
+ * built in, and that is deliberate rather than unfinished:
15
+ *
16
+ * @noble/post-quantum is the one Noble package with no independent audit -- its README
17
+ * says so -- and it is 0.x, 669 KB, and does not claim constant-time execution. This
18
+ * library's other primitives carry a published Cure53 audit, which is what lets
19
+ * docs/AUDIT_SCOPE.md tell a vendor not to price the primitive layer. Making an
20
+ * unaudited implementation a hard dependency of a transaction-signing library would
21
+ * forfeit that for a capability the spec does not require: ECDSA-secp256k1 is a
22
+ * first-class algorithm alongside the post-quantum ones, so an ECDSA-only
23
+ * implementation is conformant.
24
+ *
25
+ * So post-quantum suites are supplied by the caller -- most obviously from
26
+ * @smartledger/keys, which already wraps @noble/post-quantum -- and callers who do not
27
+ * need them pay nothing in bundle size or audit surface. See docs/BRC220_PLAN.md §2.
28
+ */
29
+
30
+ var Suites = {}
31
+
32
+ var registry = {}
33
+
34
+ /**
35
+ * Register a signature suite.
36
+ *
37
+ * `verify` MUST return a strict boolean. A suite returning a truthy object would make
38
+ * every signature check pass, which is the single defect class this codebase has had to
39
+ * fix most often -- so the registry enforces it at call time rather than trusting the
40
+ * suite.
41
+ *
42
+ * @param {String} algorithm - e.g. 'ML-DSA-65'
43
+ * @param {Object} suite
44
+ * @param {Function} suite.verify - (payloadHash: Buffer, signature: Buffer, publicKey: Buffer) => boolean
45
+ */
46
+ Suites.register = function (algorithm, suite) {
47
+ $.checkArgument(typeof algorithm === 'string' && algorithm.length > 0,
48
+ 'algorithm must be a non-empty string')
49
+ $.checkArgument(suite && typeof suite.verify === 'function',
50
+ 'suite must provide a verify(payloadHash, signature, publicKey) function')
51
+ registry[algorithm] = suite
52
+ return Suites
53
+ }
54
+
55
+ /** @returns {Object|undefined} */
56
+ Suites.get = function (algorithm) {
57
+ return registry[algorithm]
58
+ }
59
+
60
+ /** @returns {Array<String>} registered algorithm identifiers */
61
+ Suites.list = function () {
62
+ return Object.keys(registry).sort()
63
+ }
64
+
65
+ /** Test seam. */
66
+ Suites.unregister = function (algorithm) {
67
+ delete registry[algorithm]
68
+ return Suites
69
+ }
70
+
71
+ /**
72
+ * Verify a signature under the named suite.
73
+ *
74
+ * An UNREGISTERED algorithm returns false. It does not fall through to a default, and it
75
+ * does not throw: a certificate naming an algorithm this process cannot check has not
76
+ * been verified, and `false` is the honest answer. Falling through to ECDSA for an
77
+ * `ML-DSA-65` certificate would be catastrophic and is exactly what a default invites.
78
+ *
79
+ * The suite's own return value is coerced with `=== true`, so a suite that returns a
80
+ * truthy object cannot smuggle a pass through.
81
+ *
82
+ * @param {String} algorithm
83
+ * @param {Buffer} payloadHash - the 32 bytes that were signed
84
+ * @param {Buffer} signature
85
+ * @param {Buffer} publicKey
86
+ * @returns {Boolean}
87
+ */
88
+ Suites.verify = function (algorithm, payloadHash, signature, publicKey) {
89
+ var suite = registry[algorithm]
90
+ if (!suite) return false
91
+ if (!Buffer.isBuffer(payloadHash) || !Buffer.isBuffer(signature) || !Buffer.isBuffer(publicKey)) {
92
+ return false
93
+ }
94
+ try {
95
+ return suite.verify(payloadHash, signature, publicKey) === true
96
+ } catch (e) {
97
+ return false
98
+ }
99
+ }
100
+
101
+ /**
102
+ * ECDSA over secp256k1.
103
+ *
104
+ * The signer signs the 32-byte `payloadHash` DIRECTLY (spec §Algorithms: "post-quantum
105
+ * schemes apply their own internal hashing"). There is no second hash, and no Bitcoin
106
+ * sighash — this is a detached signature over a digest.
107
+ *
108
+ * Signatures are 64 raw bytes, `r || s`, each a 32-byte big-endian integer. DER is
109
+ * accepted as a length-discriminated fallback for Bitcoin-native signers, because the
110
+ * certificate's `encoding` field may say so; see docs/BRC220_ENCODING_AMENDMENT.md for
111
+ * why raw is what new implementations should emit.
112
+ *
113
+ * Low-S is REQUIRED. A high-S signature is rejected rather than normalised: normalising
114
+ * changes the signature bytes, and those bytes are inside `proofHash`, so accepting both
115
+ * forms would mean two valid certificates exist for one signing act.
116
+ */
117
+ Suites.register('ECDSA-secp256k1', {
118
+ verify: function (payloadHash, signature, publicKey) {
119
+ if (payloadHash.length !== 32) return false
120
+
121
+ var sig
122
+ if (signature.length === 64) {
123
+ sig = new Signature(
124
+ BN.fromBuffer(signature.slice(0, 32)),
125
+ BN.fromBuffer(signature.slice(32, 64))
126
+ )
127
+ } else {
128
+ // DER, for the legacy `encoding: "der"` case.
129
+ try {
130
+ sig = Signature.fromDER(signature)
131
+ } catch (e) {
132
+ return false
133
+ }
134
+ }
135
+
136
+ // Low-S, enforced rather than normalised.
137
+ var halfOrder = Point.getN().div(new BN(2))
138
+ if (sig.s.gt(halfOrder)) return false
139
+
140
+ var pubkey
141
+ try {
142
+ pubkey = PublicKey.fromBuffer(publicKey)
143
+ } catch (e) {
144
+ return false
145
+ }
146
+
147
+ var ecdsa = new ECDSA()
148
+ ecdsa.hashbuf = payloadHash
149
+ ecdsa.endian = 'little'
150
+ ecdsa.pubkey = pubkey
151
+ ecdsa.sig = sig
152
+ return ecdsa.verify() === true
153
+ }
154
+ })
155
+
156
+ module.exports = Suites
@@ -0,0 +1,75 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * RFC 8785 — JSON Canonicalization Scheme.
5
+ *
6
+ * Extracted from lib/gdaf/attestation-signer.js, where it was added in 8.2.0. Two
7
+ * unrelated things now need it — GDAF credential signatures and BRC-220 certificates —
8
+ * and a notarization module reaching into the credentials module for it would be the
9
+ * wrong direction. JCS is a general serialization concern, not a GDAF concept.
10
+ *
11
+ * The behaviour is unchanged; `AttestationSigner._canonicalizeJCS` delegates here.
12
+ *
13
+ * The point of JCS is that two independent implementations produce identical bytes, so
14
+ * everything below is fixed by the RFC rather than by local preference:
15
+ *
16
+ * - Object keys sort by UTF-16 code unit, which is what Array.prototype.sort already
17
+ * does for strings. The sort is applied DURING serialization rather than by
18
+ * rebuilding an object, because V8 orders integer-like own properties numerically
19
+ * ahead of string keys and would silently undo it — `{"2":…,"10":…}` where the RFC
20
+ * requires `{"10":…,"2":…}`. That was a real defect in this codebase before 8.2.0.
21
+ * - JSON.stringify supplies the leaf types deliberately: for finite numbers it produces
22
+ * ECMAScript Number::toString, which the RFC mandates, and since ES2019 it emits
23
+ * well-formed output for lone surrogates. Both are easy to get subtly wrong by hand.
24
+ * - Non-finite numbers throw rather than serializing as `null`, which is what
25
+ * JSON.stringify does and which would silently canonicalize a different document than
26
+ * the one supplied.
27
+ */
28
+
29
+ var JCS = {}
30
+
31
+ /**
32
+ * Serialize a value as RFC 8785 canonical JSON.
33
+ *
34
+ * @param {*} value
35
+ * @returns {String}
36
+ */
37
+ JCS.stringify = function (value) {
38
+ if (value === null) return 'null'
39
+
40
+ var type = typeof value
41
+
42
+ if (type === 'boolean') return value ? 'true' : 'false'
43
+
44
+ if (type === 'number') {
45
+ if (!isFinite(value)) {
46
+ throw new Error('Cannot canonicalize non-finite number: ' + value)
47
+ }
48
+ return JSON.stringify(value)
49
+ }
50
+
51
+ if (type === 'string') return JSON.stringify(value)
52
+
53
+ if (Array.isArray(value)) {
54
+ // Arrays are order-significant. `undefined` has no JSON form and becomes null in an
55
+ // array, matching JSON.stringify.
56
+ return '[' + value.map(function (item) {
57
+ return item === undefined ? 'null' : JCS.stringify(item)
58
+ }).join(',') + ']'
59
+ }
60
+
61
+ if (type === 'object') {
62
+ // Absent and explicitly-undefined are indistinguishable in JSON, so both are dropped.
63
+ var keys = Object.keys(value).filter(function (key) {
64
+ return value[key] !== undefined && typeof value[key] !== 'function'
65
+ }).sort()
66
+
67
+ return '{' + keys.map(function (key) {
68
+ return JSON.stringify(key) + ':' + JCS.stringify(value[key])
69
+ }).join(',') + '}'
70
+ }
71
+
72
+ throw new Error('Cannot canonicalize value of type: ' + type)
73
+ }
74
+
75
+ module.exports = JCS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartledger/bsv",
3
- "version": "8.1.0",
3
+ "version": "8.3.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",
@@ -39,6 +39,7 @@
39
39
  "./lib/ltp": "./lib/ltp/index.js",
40
40
  "./lib/message": "./lib/message/index.js",
41
41
  "./lib/mnemonic": "./lib/mnemonic/index.js",
42
+ "./lib/notaryhash": "./lib/notaryhash/index.js",
42
43
  "./lib/ordinals": "./lib/ordinals/index.js",
43
44
  "./lib/script": "./lib/script/index.js",
44
45
  "./lib/smart_contract": "./lib/smart_contract/index.js",
@@ -117,6 +118,7 @@
117
118
  "message/",
118
119
  "mnemonic/",
119
120
  "build/",
121
+ "tools/",
120
122
  "*-entry.js",
121
123
  "bsv.min.js",
122
124
  "bsv.bundle.js",
@@ -187,7 +189,7 @@
187
189
  "preimage",
188
190
  "secret-splitting",
189
191
  "did-resolution",
190
- "zero-knowledge-proofs",
192
+ "selective-disclosure",
191
193
  "blockchain-anchoring",
192
194
  "attestation-framework",
193
195
  "nchain",
@@ -230,9 +232,9 @@
230
232
  "request": "browser-request"
231
233
  },
232
234
  "dependencies": {
233
- "@noble/ciphers": "2.2.0",
234
- "@noble/curves": "2.2.0",
235
- "@noble/hashes": "2.2.0",
235
+ "@noble/ciphers": "^2.3.0",
236
+ "@noble/curves": "^2.3.0",
237
+ "@noble/hashes": "^2.3.0",
236
238
  "bn.js": "=4.12.5",
237
239
  "secrets.js-grempe": "^2.0.0"
238
240
  },
@@ -276,7 +278,8 @@
276
278
  "beforeEach",
277
279
  "describe",
278
280
  "it",
279
- "globalThis"
281
+ "globalThis",
282
+ "BigInt"
280
283
  ],
281
284
  "ignore": [
282
285
  "dist/**",
@@ -0,0 +1,106 @@
1
+ 'use strict'
2
+
3
+ /* global describe, it */
4
+
5
+ // GDAF signs a hash of canonical JSON, so the canonicalization IS part of the signature
6
+ // scheme. It was sorted-key JSON that rebuilt the object, which loses the sort: V8 orders
7
+ // integer-like own properties numerically, ahead of string keys, whatever order they were
8
+ // inserted in.
9
+ //
10
+ // Within this library that was deterministic — signing and verification agreed, and no
11
+ // forgery followed. Across implementations it was a verification failure, which for
12
+ // credentials that exist to be checked by other parties is the thing that matters.
13
+
14
+ require('chai').should()
15
+ var Signer = require('../../lib/gdaf/attestation-signer')
16
+
17
+ describe('GDAF canonicalization', function () {
18
+ describe('RFC 8785 conformance', function () {
19
+ // The concrete divergence. JCS sorts by UTF-16 code unit, where '10' < '2'.
20
+ it('sorts integer-like keys lexicographically, not numerically', function () {
21
+ var o = {}
22
+ o['10'] = 'ten'
23
+ o['2'] = 'two'
24
+ Signer._canonicalizeJCS(o).should.equal('{"10":"ten","2":"two"}')
25
+ })
26
+
27
+ it('is what the legacy form got wrong, which is why both exist', function () {
28
+ var o = {}
29
+ o['10'] = 'ten'
30
+ o['2'] = 'two'
31
+ Signer._canonicalizeJSON(o).should.equal('{"2":"two","10":"ten"}')
32
+ Signer._canonicalizeJCS(o).should.not.equal(Signer._canonicalizeJSON(o))
33
+ })
34
+
35
+ it('sorts at every depth and leaves arrays in order', function () {
36
+ Signer._canonicalizeJCS({ z: { b: 1, a: 2 }, a: [3, { d: 1, c: 2 }] })
37
+ .should.equal('{"a":[3,{"c":2,"d":1}],"z":{"a":2,"b":1}}')
38
+ })
39
+
40
+ it('is insensitive to key insertion order', function () {
41
+ var x = {}; x.b = 1; x.a = 2
42
+ var y = {}; y.a = 2; y.b = 1
43
+ Signer._canonicalizeJCS(x).should.equal(Signer._canonicalizeJCS(y))
44
+ })
45
+
46
+ // These are the same IEEE-754 double, so collapsing them is correct rather than a
47
+ // weakness — JavaScript has one number type. Pinned so nobody "fixes" it.
48
+ it('treats 1847, 1847.0 and 1.847e3 as the one number they are', function () {
49
+ Signer._canonicalizeJCS({ a: 1847 }).should.equal('{"a":1847}')
50
+ Signer._canonicalizeJCS({ a: 1847.0 }).should.equal('{"a":1847}')
51
+ Signer._canonicalizeJCS({ a: 1.847e3 }).should.equal('{"a":1847}')
52
+ })
53
+
54
+ it('keeps a number distinct from its string form', function () {
55
+ Signer._canonicalizeJCS({ a: 1847 }).should.not.equal(Signer._canonicalizeJCS({ a: '1847' }))
56
+ })
57
+
58
+ // JSON.stringify would emit `null` here, silently signing a different document.
59
+ it('refuses non-finite numbers rather than emitting null', function () {
60
+ ;(function () { Signer._canonicalizeJCS({ a: NaN }) }).should.throw(/non-finite/)
61
+ ;(function () { Signer._canonicalizeJCS({ a: Infinity }) }).should.throw(/non-finite/)
62
+ })
63
+
64
+ it('emits unicode and non-BMP characters directly', function () {
65
+ Signer._canonicalizeJCS({ a: 'é' }).should.equal('{"a":"é"}')
66
+ Signer._canonicalizeJCS({ a: '😀' }).should.equal('{"a":"😀"}')
67
+ })
68
+
69
+ it('drops undefined members, which JSON cannot represent', function () {
70
+ Signer._canonicalizeJCS({ a: 1, b: undefined }).should.equal('{"a":1}')
71
+ })
72
+
73
+ it('nulls undefined array elements, as JSON.stringify does', function () {
74
+ Signer._canonicalizeJCS([1, undefined, 2]).should.equal('[1,null,2]')
75
+ })
76
+ })
77
+
78
+ describe('hashing and migration', function () {
79
+ var CRED = { id: 'urn:x', credentialSubject: { name: 'Alice', age: 41 } }
80
+
81
+ it('hashes with JCS by default', function () {
82
+ Signer._hashCredential(CRED).toString('hex')
83
+ .should.equal(Signer._hashCredential(CRED, Signer.CANONICALIZATION.JCS).toString('hex'))
84
+ })
85
+
86
+ // The migration path. A credential whose keys make the two forms differ must hash
87
+ // differently, or the legacy fallback in the verifier would be pointless.
88
+ it('gives a different hash under the legacy form when the forms diverge', function () {
89
+ var withIntKeys = { '10': 'ten', '2': 'two' }
90
+ Signer._hashCredential(withIntKeys, Signer.CANONICALIZATION.JCS).toString('hex')
91
+ .should.not.equal(
92
+ Signer._hashCredential(withIntKeys, Signer.CANONICALIZATION.LEGACY).toString('hex')
93
+ )
94
+ })
95
+
96
+ it('agrees between the forms when no integer-like keys are present', function () {
97
+ Signer._hashCredential(CRED, Signer.CANONICALIZATION.JCS).toString('hex')
98
+ .should.equal(Signer._hashCredential(CRED, Signer.CANONICALIZATION.LEGACY).toString('hex'))
99
+ })
100
+
101
+ it('exposes both forms by name', function () {
102
+ Signer.CANONICALIZATION.JCS.should.equal('jcs')
103
+ Signer.CANONICALIZATION.LEGACY.should.equal('legacy')
104
+ })
105
+ })
106
+ })