@smartledger/bsv 3.4.5 → 4.0.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.
@@ -172,30 +172,27 @@ ECDSA.prototype.sigError = function () {
172
172
  return 'hashbuf must be a 32 byte buffer'
173
173
  }
174
174
 
175
- // === SmartLedger Security Patches ===
176
- // Apply security validation during cryptographic verification
177
175
  var r = this.sig.r
178
176
  var s = this.sig.s
179
177
  var n = Point.getN()
180
178
 
181
179
  try {
182
- // Fix 1 & 2: Enhanced range validation
183
- if (r.isZero() || s.isZero() || r.gte(n) || s.gte(n)) {
180
+ // Reject out-of-range r, s: both must lie in [1, n-1]. lte(0) covers
181
+ // negative and zero values; gte(n) covers anything at or above the order.
182
+ if (r.lte(BN.Zero) || s.lte(BN.Zero) || r.gte(n) || s.gte(n)) {
184
183
  return 'r and s not in range'
185
184
  }
186
185
 
187
- // Fix 3: Handle canonicalization properly
188
- // For verification, we need to try both the original s and canonical s
189
- // This handles both pre-canonicalized signatures and non-canonical ones
190
- var nh = n.shrn(1) // n/2
191
- var canonicalS = s.gt(nh) ? n.sub(s) : s
192
-
193
186
  var e = BN.fromBuffer(this.hashbuf, this.endian ? {
194
187
  endian: this.endian
195
188
  } : undefined)
196
189
 
197
- // Try verification with canonical s
198
- var sinv = canonicalS.invm(n)
190
+ // Standard ECDSA verification. This accepts both (r, s) and (r, n - s):
191
+ // ECDSA is inherently malleable in s, and verifying the canonical form
192
+ // yields the same accept/reject as verifying s directly, so there is no
193
+ // need to canonicalize or retry here. Low-S is enforced at *signing* time
194
+ // (see ECDSA.toLowS), which is where malleability protection belongs.
195
+ var sinv = s.invm(n)
199
196
  var u1 = sinv.mul(e).umod(n)
200
197
  var u2 = sinv.mul(r).umod(n)
201
198
 
@@ -205,23 +202,9 @@ ECDSA.prototype.sigError = function () {
205
202
  }
206
203
 
207
204
  if (p.getX().umod(n).cmp(r) !== 0) {
208
- // If canonical verification failed and s was already canonical,
209
- // try with the non-canonical version (for backwards compatibility)
210
- if (s.lte(nh)) {
211
- var nonCanonicalS = n.sub(s)
212
- sinv = nonCanonicalS.invm(n)
213
- u1 = sinv.mul(e).umod(n)
214
- u2 = sinv.mul(r).umod(n)
215
-
216
- p = Point.getG().mulAdd(u1, this.pubkey.point, u2)
217
- if (!p.isInfinity() && p.getX().umod(n).cmp(r) === 0) {
218
- return false // Verification succeeded with non-canonical s
219
- }
220
- }
221
205
  return 'Invalid signature'
222
- } else {
223
- return false // Verification succeeded with canonical s
224
206
  }
207
+ return false
225
208
  } catch (error) {
226
209
  return 'Signature security validation failed: ' + error.message
227
210
  }
@@ -163,7 +163,18 @@ ECIES.prototype.decrypt = function (encbuf) {
163
163
  var hmac2 = Hash.sha256hmac(encbuf.slice(0, encbuf.length - tagLength), this.kM)
164
164
  if (this.opts.shortTag) hmac2 = hmac2.slice(0, 4)
165
165
 
166
- if (!hmac.equals(hmac2)) {
166
+ // Constant-time tag comparison. Buffer.equals() short-circuits on the first
167
+ // differing byte, which leaks how many leading bytes matched and enables a
168
+ // forgery oracle on the MAC; compare every byte unconditionally instead
169
+ // (mirrors the loop in bitcore-ecies.js).
170
+ if (hmac.length !== hmac2.length) {
171
+ throw new errors.DecryptionError('Invalid checksum')
172
+ }
173
+ var equal = 0
174
+ for (var i = 0; i < hmac2.length; i++) {
175
+ equal |= (hmac[i] ^ hmac2[i])
176
+ }
177
+ if (equal !== 0) {
167
178
  throw new errors.DecryptionError('Invalid checksum')
168
179
  }
169
180
 
@@ -49,14 +49,49 @@ function AttestationSigner(privateKey, options) {
49
49
  return this
50
50
  }
51
51
 
52
+ /**
53
+ * Recursively produce a value with all object keys sorted, so that
54
+ * JSON.stringify yields a deterministic, fully-covering serialization.
55
+ *
56
+ * IMPORTANT: this replaces a previous implementation that called
57
+ * `JSON.stringify(obj, Object.keys(obj).sort())`. That used the JSON.stringify
58
+ * *replacer array* form, which whitelists keys at EVERY nesting level to the
59
+ * top-level key set. The result was that nested objects (notably
60
+ * `credentialSubject`) serialized to `{}` and were therefore NOT covered by the
61
+ * signature -- a forger could rewrite the subject's claims without invalidating
62
+ * the proof. This recursive sort includes every key at every depth.
63
+ *
64
+ * Note: keys are ordered with Array.prototype.sort (UTF-16 code-unit order) and
65
+ * inserted in that order. Credential keys are non-integer strings, so insertion
66
+ * order is preserved by JSON.stringify; do not feed integer-like keys here.
67
+ *
68
+ * @param {*} value - Value to canonicalize
69
+ * @returns {*} Value with all nested object keys sorted
70
+ * @private
71
+ */
72
+ AttestationSigner._sortValue = function(value) {
73
+ if (Array.isArray(value)) {
74
+ // Arrays are order-significant: preserve order, canonicalize each element.
75
+ return value.map(function(item) { return AttestationSigner._sortValue(item) })
76
+ }
77
+ if (value !== null && typeof value === 'object') {
78
+ return Object.keys(value).sort().reduce(function(sorted, key) {
79
+ sorted[key] = AttestationSigner._sortValue(value[key])
80
+ return sorted
81
+ }, {})
82
+ }
83
+ // Primitives (string, number, boolean, null) are returned unchanged.
84
+ return value
85
+ }
86
+
52
87
  /**
53
88
  * Create canonical JSON string for signing
54
89
  * @param {Object} obj - Object to canonicalize
55
90
  * @returns {String} Canonical JSON string
56
91
  */
57
92
  AttestationSigner._canonicalizeJSON = function(obj) {
58
- // Use deterministic JSON serialization
59
- return JSON.stringify(obj, Object.keys(obj).sort())
93
+ // Deterministic serialization that covers every nested key at every depth.
94
+ return JSON.stringify(AttestationSigner._sortValue(obj))
60
95
  }
61
96
 
62
97
  /**
@@ -251,6 +251,16 @@ AttestationVerifier._validateStructure = function(credential) {
251
251
  * Verify credential signature
252
252
  * @private
253
253
  */
254
+ AttestationVerifier._normalizeDID = function(issuer) {
255
+ // W3C VC allows `issuer` to be a DID string or an object with an `id`.
256
+ var did = (issuer && typeof issuer === 'object') ? issuer.id : issuer
257
+ if (typeof did !== 'string' || did.length === 0) {
258
+ return null
259
+ }
260
+ // Strip any fragment so `did:...#keys-1` compares equal to `did:...`.
261
+ return did.split('#')[0]
262
+ }
263
+
254
264
  AttestationVerifier._verifySignature = async function(credential) {
255
265
  try {
256
266
  var proof = credential.proof
@@ -273,7 +283,22 @@ AttestationVerifier._verifySignature = async function(credential) {
273
283
  // Extract DID from verification method
274
284
  var verificationMethod = proof.verificationMethod
275
285
  var did = verificationMethod.split('#')[0]
276
-
286
+
287
+ // Bind the signing key to the claimed issuer. Without this, a valid
288
+ // signature from ANY DID is accepted while `credential.issuer` names a
289
+ // different (e.g. trusted) authority -- an issuer-spoofing forgery. The
290
+ // DID that owns the verificationMethod MUST equal the credential issuer.
291
+ var issuerDID = AttestationVerifier._normalizeDID(credential.issuer)
292
+ if (!issuerDID) {
293
+ return { valid: false, errors: ['Missing or invalid credential issuer'] }
294
+ }
295
+ if (issuerDID !== did) {
296
+ return {
297
+ valid: false,
298
+ errors: ['Verification method DID (' + did + ') does not match credential issuer (' + issuerDID + ')']
299
+ }
300
+ }
301
+
277
302
  // Get public key from DID
278
303
  var publicKey = DIDResolver.getPublicKey(did)
279
304
 
@@ -292,9 +317,12 @@ AttestationVerifier._verifySignature = async function(credential) {
292
317
  ecdsa.hashbuf = credentialHash
293
318
  ecdsa.pubkey = publicKey
294
319
  ecdsa.sig = signature
295
-
296
- var valid = ecdsa.verify()
297
-
320
+
321
+ // NOTE: ECDSA.prototype.verify() returns the ECDSA *instance* (always
322
+ // truthy), not a boolean. Read the `.verified` flag, otherwise every
323
+ // structurally-valid proof passes regardless of the actual signature.
324
+ var valid = ecdsa.verify().verified
325
+
298
326
  if (valid) {
299
327
  return {
300
328
  valid: true,
@@ -355,9 +383,11 @@ AttestationVerifier._verifyPresentationSignature = async function(presentation)
355
383
  ecdsa.hashbuf = presentationHash
356
384
  ecdsa.pubkey = publicKey
357
385
  ecdsa.sig = signature
358
-
359
- var valid = ecdsa.verify()
360
-
386
+
387
+ // ECDSA.prototype.verify() returns the instance, not a boolean — read
388
+ // `.verified` (see _verifySignature).
389
+ var valid = ecdsa.verify().verified
390
+
361
391
  return {
362
392
  valid: valid,
363
393
  errors: valid ? [] : ['Presentation signature verification failed']
@@ -392,10 +422,18 @@ AttestationVerifier._validateIssuer = async function(credential, options) {
392
422
  return { valid: false, errors: errors }
393
423
  }
394
424
 
395
- // Check if issuer is in trusted list
425
+ // Enforce the trusted-issuer allow-list when one is supplied. Previously a
426
+ // non-member only produced a warning, so `trustedIssuers` provided no
427
+ // actual enforcement and any resolvable DID was accepted. Comparison is
428
+ // done on normalized DIDs so fragment/object issuer forms match.
396
429
  if (options.trustedIssuers && Array.isArray(options.trustedIssuers)) {
397
- if (!options.trustedIssuers.includes(issuer)) {
398
- warnings.push('Issuer not in trusted list')
430
+ var issuerDID = AttestationVerifier._normalizeDID(issuer)
431
+ var trusted = options.trustedIssuers
432
+ .map(function(t) { return AttestationVerifier._normalizeDID(t) })
433
+ .filter(Boolean)
434
+ if (!issuerDID || trusted.indexOf(issuerDID) === -1) {
435
+ errors.push('Issuer not in trusted list')
436
+ return { valid: false, errors: errors, warnings: warnings }
399
437
  }
400
438
  }
401
439
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartledger/bsv",
3
- "version": "3.4.5",
3
+ "version": "4.0.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",
@@ -74,7 +74,6 @@
74
74
  "lib/",
75
75
  "utilities/*.js",
76
76
  "utilities/README.md",
77
- "utilities/wallet.json",
78
77
  "ecies/",
79
78
  "message/",
80
79
  "mnemonic/",
@@ -132,7 +131,6 @@
132
131
  "debug-tools",
133
132
  "modular-loading",
134
133
  "standalone-modules",
135
- "security-hardened",
136
134
  "elliptic-curve-fix",
137
135
  "smartledger",
138
136
  "transaction",
@@ -184,7 +182,6 @@
184
182
  "webpack-ready",
185
183
  "cdn-ready",
186
184
  "typescript-definitions",
187
- "vulnerability-free",
188
185
  "drop-in-replacement",
189
186
  "performance-optimized",
190
187
  "bip21",
@@ -1,30 +0,0 @@
1
- {
2
- "wallet": {
3
- "privateKeyWIF": "KwbaQqFUpt9MYQRrkVWk7QC799nAfcqYRYKj4ZNeZBkut3YP45rJ",
4
- "privateKeyHex": "KwbaQqFUpt9MYQRrkVWk7QC799nAfcqYRYKj4ZNeZBkut3YP45rJ",
5
- "publicKey": "02330726b97010d7bb1f88b1213330e644916cedfd5fee9c57153592c6b5a44fe9",
6
- "address": "15XJXD7CSMqHL2ivFCu8PZTACQQ8MPbWY9",
7
- "pubkeyHash160": "319b9b8eef10d00cf84b5d4772dca69b5f5a7b5c"
8
- },
9
- "utxo": {
10
- "txid": "d6625d13f3130360dea7ebe8ccf8ccdb5e40b3cd5ea51b2094983a3789354c3e",
11
- "vout": 0,
12
- "outputIndex": 0,
13
- "script": "76a914319b9b8eef10d00cf84b5d4772dca69b5f5a7b5c88ac",
14
- "scriptPubKey": "76a914319b9b8eef10d00cf84b5d4772dca69b5f5a7b5c88ac",
15
- "satoshis": 50000,
16
- "address": "15XJXD7CSMqHL2ivFCu8PZTACQQ8MPbWY9"
17
- },
18
- "testParams": {
19
- "nLockTimeTarget": 1000,
20
- "sighashType": 65,
21
- "covenantAmount": 40000,
22
- "fee": 1000,
23
- "changeAmount": 9000
24
- },
25
- "metadata": {
26
- "created": "2025-10-19T14:33:10.987Z",
27
- "description": "Test wallet and UTXO for BSV preimage covenant testing",
28
- "version": "1.0"
29
- }
30
- }