@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.
- package/CHANGELOG.md +166 -0
- package/README.md +38 -38
- package/bsv-ecies.min.js +1 -1
- package/bsv-gdaf.min.js +62 -62
- package/bsv-ltp.min.js +48 -48
- package/bsv-smartcontract.min.js +1 -1
- package/bsv.bundle.js +62 -62
- package/bsv.min.js +62 -62
- package/docs/AUDIT_SCOPE.md +8 -8
- package/docs/BRC220_ENCODING_AMENDMENT.md +100 -0
- package/docs/BRC220_PLAN.md +224 -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 +5 -0
- package/index.mjs +1 -0
- package/lib/gdaf/attestation-signer.js +54 -2
- package/lib/gdaf/attestation-verifier.js +41 -25
- package/lib/gdaf/zk-prover.js +143 -46
- package/lib/notaryhash/certificate.js +282 -0
- package/lib/notaryhash/encoding.js +150 -0
- package/lib/notaryhash/index.js +334 -0
- package/lib/notaryhash/merkle.js +209 -0
- package/lib/notaryhash/script.js +261 -0
- package/lib/notaryhash/suites.js +156 -0
- package/lib/util/jcs.js +75 -0
- package/package.json +9 -6
- package/test/gdaf/canonicalization.js +106 -0
- package/test/gdaf/zk_prover.js +204 -0
- package/test/notaryhash/certificate.js +249 -0
- package/test/notaryhash/encoding.js +186 -0
- package/test/notaryhash/merkle.js +181 -0
- package/test/notaryhash/script.js +270 -0
- package/test/notaryhash/verify.js +339 -0
- package/tools/minimal_reproduction.js +119 -0
- package/tools/opcode_map.js +342 -0
- package/tools/server.js +97 -0
- package/tools/simple_real_tx.js +136 -0
- package/tools/sv-sighash-harness.js +123 -0
- package/tools/sv-sighash-report.js +71 -0
- package/tools/sv-tx-harness.js +172 -0
- package/tools/sv-tx-report.js +43 -0
- package/tools/sv-vector-harness.js +304 -0
- package/tools/sv-vector-report.js +88 -0
- 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
|
package/lib/util/jcs.js
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
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.
|
|
234
|
-
"@noble/curves": "2.
|
|
235
|
-
"@noble/hashes": "2.
|
|
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
|
+
})
|