hd-wallet-wasm 2.0.32 → 2.1.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.
@@ -103,3 +103,12 @@ export function verifyAllChainProofs(
103
103
  wallet: HDWalletModule,
104
104
  chainProofs: ChainProofData[],
105
105
  ): ChainProofVerifyResult;
106
+
107
+ /** DER-encode a 64-byte compact r||s ECDSA signature (the SDS EPM annex encoding). */
108
+ export function compactSignatureToDer(compact: Uint8Array): Uint8Array;
109
+
110
+ /** SHA-256(SHA-256("\x18Bitcoin Signed Message:\n" || CompactSize(len) || message)). */
111
+ export function bitcoinSignedMessageHash(wallet: HDWalletModule, message: Uint8Array): Uint8Array;
112
+
113
+ /** EIP-191 personal_sign: Keccak-256("\x19Ethereum Signed Message:\n" || decimal(len) || message). */
114
+ export function ethereumPersonalSignHash(wallet: HDWalletModule, message: Uint8Array): Uint8Array;
@@ -217,25 +217,101 @@ export function buildEPMSigningContent(epm) {
217
217
  return new TextEncoder().encode(epmJcsCanonicalize(content));
218
218
  }
219
219
 
220
+ // =============================================================================
221
+ // ECDSA encodings (formatting only; hashing and signing stay in native code)
222
+ // =============================================================================
223
+
224
+ const SECP256K1_ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
225
+ const SECP256K1_HALF_ORDER = SECP256K1_ORDER >> 1n;
226
+
227
+ function bytesToBigInt(bytes) {
228
+ let value = 0n;
229
+ for (const b of bytes) value = (value << 8n) | BigInt(b);
230
+ return value;
231
+ }
232
+
233
+ // One DER INTEGER for an unsigned big-endian scalar: minimal length, a 0x00
234
+ // pad when the high bit is set.
235
+ function derInteger(scalar) {
236
+ let start = 0;
237
+ while (start < scalar.length - 1 && scalar[start] === 0) start++;
238
+ const body = scalar.subarray(start);
239
+ const pad = body[0] & 0x80 ? 1 : 0;
240
+ const out = new Uint8Array(2 + pad + body.length);
241
+ out[0] = 0x02;
242
+ out[1] = pad + body.length;
243
+ out.set(body, 2 + pad);
244
+ return out;
245
+ }
246
+
247
+ /**
248
+ * Encode a 64-byte compact r||s ECDSA signature as DER
249
+ * (SEQUENCE { INTEGER r, INTEGER s }), the SDS EPM annex encoding.
250
+ *
251
+ * @param {Uint8Array} compact - 64-byte r||s
252
+ * @returns {Uint8Array} DER signature
253
+ */
254
+ export function compactSignatureToDer(compact) {
255
+ if (!(compact instanceof Uint8Array) || compact.length !== 64) {
256
+ throw new TypeError('compact signature must be 64 bytes (r||s)');
257
+ }
258
+ const r = derInteger(compact.subarray(0, 32));
259
+ const s = derInteger(compact.subarray(32, 64));
260
+ const der = new Uint8Array(2 + r.length + s.length);
261
+ der[0] = 0x30;
262
+ der[1] = r.length + s.length;
263
+ der.set(r, 2);
264
+ der.set(s, 2 + r.length);
265
+ return der;
266
+ }
267
+
268
+ // Strict DER: exactly SEQUENCE { INTEGER r, INTEGER s } with minimal,
269
+ // positive, nonzero integers of at most 32 bytes and no trailing bytes.
270
+ function isStrictSecp256k1Der(der) {
271
+ if (!(der instanceof Uint8Array) || der.length < 8 || der.length > 72) return false;
272
+ if (der[0] !== 0x30 || der[1] !== der.length - 2) return false;
273
+ let pos = 2;
274
+ for (let i = 0; i < 2; i++) {
275
+ if (der[pos] !== 0x02) return false;
276
+ const len = der[pos + 1];
277
+ pos += 2;
278
+ if (len < 1 || len > 33 || pos + len > der.length) return false;
279
+ const body = der.subarray(pos, pos + len);
280
+ if (body[0] & 0x80) return false; // negative
281
+ if (len > 1 && body[0] === 0 && !(body[1] & 0x80)) return false; // not minimal
282
+ if (len === 33 && body[0] !== 0) return false;
283
+ const value = bytesToBigInt(body);
284
+ if (value === 0n || value >= SECP256K1_ORDER) return false;
285
+ pos += len;
286
+ }
287
+ return pos === der.length;
288
+ }
289
+
220
290
  /**
221
291
  * Sign EPM content. Default curve is ed25519 (fast, the network default); pass
222
- * `{ curve: 'secp256k1' }` to sign with secp256k1 (ECDSA-DER over sha256(content),
223
- * byte-compatible with the Go/C++ EPM verifiers). The content canonicalization is
224
- * identical for both curves; only the signature differs.
292
+ * `{ curve: 'secp256k1' }` to sign with secp256k1: DER-encoded ECDSA over
293
+ * SHA-256 of the canonical content, hashed once (SDS EPM annex; the Go and C++
294
+ * EPM verifiers check exactly this). The content canonicalization is identical
295
+ * for both curves; only the signature differs.
225
296
  *
226
297
  * @param {Object} wallet - Initialized HDWalletModule
227
298
  * @param {Object} epm - EPM fields as a plain object (without SIGNATURE/SIGNATURE_TIMESTAMP)
228
299
  * @param {Uint8Array} privateKey - 32-byte private key (ed25519 seed or secp256k1 key)
229
300
  * @param {{ curve?: 'ed25519'|'secp256k1' }} [options]
230
- * @returns {{ signature: string, timestamp: number }} Hex signature and Unix timestamp
301
+ * @returns {{ signature: string, timestamp: number }} Hex signature (ed25519: 64 raw
302
+ * bytes; secp256k1: DER) and Unix timestamp
231
303
  */
232
304
  export function signEPMContent(wallet, epm, privateKey, options = {}) {
233
305
  const curve = String(options.curve || 'ed25519').toLowerCase();
306
+ if (curve !== 'ed25519' && curve !== 'secp256k1') {
307
+ throw new Error(`signEPMContent: unsupported curve "${curve}"`);
308
+ }
234
309
  const timestamp = Math.floor(Date.now() / 1000);
235
310
  const content = buildEPMSigningContent({ ...epm, SIGNATURE_TIMESTAMP: timestamp });
311
+ // curves.secp256k1.sign hashes its input with SHA-256: pass the content itself.
236
312
  const sig =
237
313
  curve === 'secp256k1'
238
- ? wallet.curves.secp256k1.sign(wallet.utils.sha256(content), privateKey)
314
+ ? compactSignatureToDer(wallet.curves.secp256k1.sign(content, privateKey))
239
315
  : wallet.curves.ed25519.sign(content, privateKey);
240
316
  return {
241
317
  signature: wallet.utils.encodeHex(sig),
@@ -246,8 +322,9 @@ export function signEPMContent(wallet, epm, privateKey, options = {}) {
246
322
  /**
247
323
  * Verify an EPM content signature. Dispatches on the explicit `options.curve`,
248
324
  * else infers from the public key length (32 = ed25519; 33/65 = secp256k1).
249
- * secp256k1 is verified as ECDSA-DER over sha256(content), matching signEPMContent
250
- * and the Go/C++ verifiers.
325
+ * secp256k1 must be a strict DER ECDSA signature over SHA-256 of the canonical
326
+ * content (hashed once), matching signEPMContent and the Go/C++ verifiers; a
327
+ * signature over SHA-256(SHA-256(content)) does not verify.
251
328
  *
252
329
  * @param {Object} wallet - Initialized HDWalletModule
253
330
  * @param {Object} epm - Full EPM object including SIGNATURE and SIGNATURE_TIMESTAMP
@@ -265,9 +342,101 @@ export function verifyEPMSignature(wallet, epm, publicKey, options = {}) {
265
342
  options.curve || (publicKey && publicKey.length === 32 ? 'ed25519' : 'secp256k1'),
266
343
  ).toLowerCase();
267
344
  if (curve === 'secp256k1') {
268
- return wallet.curves.secp256k1.verify(wallet.utils.sha256(content), sig, publicKey);
345
+ if (!isStrictSecp256k1Der(sig)) return false;
346
+ return wallet.curves.secp256k1.verify(content, sig, publicKey);
269
347
  }
270
- return wallet.curves.ed25519.verify(content, sig, publicKey);
348
+ if (curve === 'ed25519') {
349
+ return wallet.curves.ed25519.verify(content, sig, publicKey);
350
+ }
351
+ return false;
352
+ }
353
+
354
+ // =============================================================================
355
+ // Chain Proof Building
356
+ // =============================================================================
357
+
358
+ // =============================================================================
359
+ // Chain message hashes (each chain's own signed-message protocol)
360
+ // =============================================================================
361
+
362
+ function concatBytes(...parts) {
363
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
364
+ let offset = 0;
365
+ for (const p of parts) {
366
+ out.set(p, offset);
367
+ offset += p.length;
368
+ }
369
+ return out;
370
+ }
371
+
372
+ // Bitcoin CompactSize (little-endian varint).
373
+ function compactSize(length) {
374
+ if (length < 253) return Uint8Array.of(length);
375
+ if (length <= 0xffff) return Uint8Array.of(253, length & 0xff, length >>> 8);
376
+ if (length <= 0xffffffff) {
377
+ return Uint8Array.of(254, length & 0xff, (length >>> 8) & 0xff, (length >>> 16) & 0xff, length >>> 24);
378
+ }
379
+ throw new RangeError('message too long for a Bitcoin signed message');
380
+ }
381
+
382
+ const BITCOIN_MESSAGE_MAGIC = new TextEncoder().encode('\x18Bitcoin Signed Message:\n');
383
+
384
+ /**
385
+ * Bitcoin signed-message hash: SHA-256(SHA-256("\x18Bitcoin Signed Message:\n"
386
+ * || CompactSize(len) || message)), the digest Bitcoin Core's signmessage signs
387
+ * and SDN's Go verifier recovers from. The double SHA-256 is the protocol's.
388
+ *
389
+ * @param {Object} wallet - Initialized HDWalletModule
390
+ * @param {Uint8Array} message - Message bytes
391
+ * @returns {Uint8Array} 32-byte digest
392
+ */
393
+ export function bitcoinSignedMessageHash(wallet, message) {
394
+ const preimage = concatBytes(BITCOIN_MESSAGE_MAGIC, compactSize(message.length), message);
395
+ return wallet.utils.sha256(wallet.utils.sha256(preimage));
396
+ }
397
+
398
+ /**
399
+ * Ethereum EIP-191 personal_sign hash: Keccak-256("\x19Ethereum Signed Message:\n"
400
+ * || decimal(len) || message).
401
+ *
402
+ * @param {Object} wallet - Initialized HDWalletModule
403
+ * @param {Uint8Array} message - Message bytes
404
+ * @returns {Uint8Array} 32-byte digest
405
+ */
406
+ export function ethereumPersonalSignHash(wallet, message) {
407
+ const prefix = new TextEncoder().encode(`\x19Ethereum Signed Message:\n${message.length}`);
408
+ return wallet.utils.keccak256(concatBytes(prefix, message));
409
+ }
410
+
411
+ // Recoverable signature over a protocol digest -> 65-byte compact header||r||s
412
+ // (header 31 + recovery id: a compressed key, as Bitcoin Core and dcrd emit).
413
+ function signCompactRecoverable(wallet, digest, privateKey) {
414
+ const { signature, recoveryId } = wallet.curves.secp256k1.signRecoverable(digest, privateKey);
415
+ return concatBytes(Uint8Array.of(31 + recoveryId), signature);
416
+ }
417
+
418
+ // Recover the compressed key from a 65-byte header||r||s signature over a
419
+ // protocol digest; null when malformed. Headers 27..34 as dcrd accepts.
420
+ function recoverCompact(wallet, digest, signature, { requireLowS }) {
421
+ if (signature.length !== 65) return null;
422
+ const header = signature[0];
423
+ if (header < 27 || header > 34) return null;
424
+ const r = bytesToBigInt(signature.subarray(1, 33));
425
+ const s = bytesToBigInt(signature.subarray(33, 65));
426
+ if (r === 0n || r >= SECP256K1_ORDER || s === 0n || s >= SECP256K1_ORDER) return null;
427
+ if (requireLowS && s > SECP256K1_HALF_ORDER) return null;
428
+ try {
429
+ return wallet.curves.secp256k1.recover(digest, signature.subarray(1, 65), (header - 27) & 3);
430
+ } catch {
431
+ return null;
432
+ }
433
+ }
434
+
435
+ function bytesEqual(a, b) {
436
+ if (a.length !== b.length) return false;
437
+ let diff = 0;
438
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
439
+ return diff === 0;
271
440
  }
272
441
 
273
442
  // =============================================================================
@@ -275,8 +444,9 @@ export function verifyEPMSignature(wallet, epm, publicKey, options = {}) {
275
444
  // =============================================================================
276
445
 
277
446
  /**
278
- * Build a Bitcoin chain proof.
279
- * Signs the canonical payload with secp256k1 using Bitcoin message signing format.
447
+ * Build a Bitcoin chain proof: a Bitcoin signed message (signmessage) over the
448
+ * canonical payload, as a 65-byte compact recoverable signature
449
+ * (header 31+recid || r || s).
280
450
  *
281
451
  * @param {Object} wallet - Initialized HDWalletModule
282
452
  * @param {Object} params
@@ -289,10 +459,7 @@ export function verifyEPMSignature(wallet, epm, publicKey, options = {}) {
289
459
  */
290
460
  export function buildBitcoinChainProof(wallet, { address, publicKeyHex, privateKey, keyPath, canonicalPayload }) {
291
461
  const payloadBytes = new TextEncoder().encode(canonicalPayload);
292
- const payloadHash = wallet.utils.sha256(payloadBytes);
293
- const sig = wallet.curves.secp256k1.signRecoverable
294
- ? wallet.curves.secp256k1.signRecoverable(payloadHash, privateKey)
295
- : wallet.curves.secp256k1.sign(payloadHash, privateKey);
462
+ const sig = signCompactRecoverable(wallet, bitcoinSignedMessageHash(wallet, payloadBytes), privateKey);
296
463
 
297
464
  return {
298
465
  CHAIN: 'bitcoin',
@@ -307,8 +474,9 @@ export function buildBitcoinChainProof(wallet, { address, publicKeyHex, privateK
307
474
  }
308
475
 
309
476
  /**
310
- * Build an Ethereum chain proof.
311
- * Signs the canonical payload with secp256k1 using Ethereum personal_sign prefix.
477
+ * Build an Ethereum chain proof: an EIP-191 personal_sign signature over the
478
+ * canonical payload, as a 65-byte compact recoverable signature
479
+ * (header 31+recid || r || s, low-S).
312
480
  *
313
481
  * @param {Object} wallet - Initialized HDWalletModule
314
482
  * @param {Object} params
@@ -321,10 +489,7 @@ export function buildBitcoinChainProof(wallet, { address, publicKeyHex, privateK
321
489
  */
322
490
  export function buildEthereumChainProof(wallet, { address, publicKeyHex, privateKey, keyPath, canonicalPayload }) {
323
491
  const payloadBytes = new TextEncoder().encode(canonicalPayload);
324
- const payloadHash = wallet.utils.sha256(payloadBytes);
325
- const sig = wallet.curves.secp256k1.signRecoverable
326
- ? wallet.curves.secp256k1.signRecoverable(payloadHash, privateKey)
327
- : wallet.curves.secp256k1.sign(payloadHash, privateKey);
492
+ const sig = signCompactRecoverable(wallet, ethereumPersonalSignHash(wallet, payloadBytes), privateKey);
328
493
 
329
494
  return {
330
495
  CHAIN: 'ethereum',
@@ -372,7 +537,10 @@ export function buildSolanaChainProof(wallet, { address, publicKeyHex, privateKe
372
537
  // =============================================================================
373
538
 
374
539
  /**
375
- * Verify a single chain proof.
540
+ * Verify a single chain proof. Bitcoin and Ethereum proofs are checked as their
541
+ * chains do (and as SDN's Go verifier does): hash the payload with the chain's
542
+ * signed-message protocol, recover the key from the 65-byte compact signature,
543
+ * and compare it with PUBLIC_KEY. Ethereum proofs must be low-S.
376
544
  *
377
545
  * @param {Object} wallet - Initialized HDWalletModule
378
546
  * @param {Object} proof - ChainProof object with CHAIN, PUBLIC_KEY, SIGNATURE, SIGNED_PAYLOAD, ALGORITHM
@@ -390,8 +558,14 @@ export function verifyChainProof(wallet, proof) {
390
558
  }
391
559
 
392
560
  if (algorithm === 'secp256k1-compact-bitcoin' || algorithm === 'secp256k1-compact-ethereum') {
393
- const payloadHash = wallet.utils.sha256(payload);
394
- return wallet.curves.secp256k1.verify(payloadHash, sig, pubKey);
561
+ if (proof.ENCODING !== undefined && proof.ENCODING !== 'compact') return false;
562
+ if (pubKey.length !== 33) return false;
563
+ const ethereum = algorithm === 'secp256k1-compact-ethereum';
564
+ const digest = ethereum
565
+ ? ethereumPersonalSignHash(wallet, payload)
566
+ : bitcoinSignedMessageHash(wallet, payload);
567
+ const recovered = recoverCompact(wallet, digest, sig, { requireLowS: ethereum });
568
+ return recovered !== null && bytesEqual(recovered, pubKey);
395
569
  }
396
570
 
397
571
  return false;
@@ -114,6 +114,10 @@ export interface SdnPluginInvocationResult {
114
114
 
115
115
  export interface SdnPluginCapabilities {
116
116
  randomBytes?: (length: number, context?: Record<string, unknown>) => BinaryLike;
117
+ /**
118
+ * Wallet-held signer for sign_detached. For secp256k1 it receives the message
119
+ * (digest is null) and must return ECDSA over SHA-256(messageBytes), hashed once.
120
+ */
117
121
  walletSign?: (request: {
118
122
  curve: string;
119
123
  payload: any;
@@ -731,10 +735,14 @@ export interface CurvesAPI {
731
735
 
732
736
  // ECDSA (secp256k1)
733
737
  secp256k1: {
738
+ /** ECDSA over SHA-256(message), hashed here: pass the message, never a hash of it. Returns 64-byte r||s. */
734
739
  sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array;
735
- signRecoverable(message: Uint8Array, privateKey: Uint8Array): { signature: Uint8Array; recoveryId: number };
740
+ /** Signs the 32-byte hash as given (no hashing): pass your protocol digest (e.g. Keccak-256). */
741
+ signRecoverable(hash: Uint8Array, privateKey: Uint8Array): { signature: Uint8Array; recoveryId: number };
742
+ /** Verifies ECDSA over SHA-256(message), hashed here; signature is 64-byte r||s or DER. */
736
743
  verify(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
737
- recover(message: Uint8Array, signature: Uint8Array, recoveryId: number): Uint8Array;
744
+ /** Recovers the compressed key from a signature over the 32-byte hash, used as given. */
745
+ recover(hash: Uint8Array, signature: Uint8Array, recoveryId: number): Uint8Array;
738
746
  ecdh(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array;
739
747
  };
740
748
 
@@ -746,14 +754,18 @@ export interface CurvesAPI {
746
754
 
747
755
  // ECDSA (P-256)
748
756
  p256: {
757
+ /** ECDSA over SHA-256(message), hashed here: pass the message, never a hash of it. Returns 64-byte r||s. */
749
758
  sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array;
759
+ /** Verifies ECDSA over SHA-256(message), hashed here. */
750
760
  verify(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
751
761
  ecdh(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array;
752
762
  };
753
763
 
754
764
  // ECDSA (P-384)
755
765
  p384: {
766
+ /** ECDSA over SHA-384(message), hashed here: pass the message, never a hash of it. Returns 96-byte r||s. */
756
767
  sign(message: Uint8Array, privateKey: Uint8Array): Uint8Array;
768
+ /** Verifies ECDSA over SHA-384(message), hashed here. */
757
769
  verify(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean;
758
770
  ecdh(privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array;
759
771
  };
@@ -9,7 +9,7 @@
9
9
  * - Transaction building and signing
10
10
  *
11
11
  * @module hd-wallet-wasm
12
- * @version 2.0.32
12
+ * @version 2.1.0
13
13
  */
14
14
 
15
15
  // Import aligned API for batch operations
@@ -1688,10 +1688,13 @@ function createModule(wasm) {
1688
1688
  */
1689
1689
  secp256k1: {
1690
1690
  /**
1691
- * Sign message with secp256k1
1692
- * @param {Uint8Array} message - Message to sign (typically 32-byte hash)
1691
+ * Sign a message with secp256k1 ECDSA. Hashes the message with SHA-256
1692
+ * internally and signs that hash (RFC 6979, low-S): pass the message, never
1693
+ * a hash of it, or the signature covers SHA-256(SHA-256(message)) and no
1694
+ * standard verifier accepts it.
1695
+ * @param {Uint8Array} message - Message bytes (hashed once with SHA-256 here)
1693
1696
  * @param {Uint8Array} privateKey - 32-byte private key
1694
- * @returns {Uint8Array} Signature
1697
+ * @returns {Uint8Array} 64-byte compact signature r||s
1695
1698
  */
1696
1699
  sign(message, privateKey) {
1697
1700
  const msgPtr = allocAndCopy(wasm, message);
@@ -1710,10 +1713,12 @@ function createModule(wasm) {
1710
1713
  },
1711
1714
 
1712
1715
  /**
1713
- * Sign message with recovery ID
1714
- * @param {Uint8Array} message - Message to sign
1715
- * @param {Uint8Array} privateKey - Private key
1716
- * @returns {Object} { signature: Uint8Array, recoveryId: number }
1716
+ * Sign a 32-byte hash with recovery ID. Unlike sign(), the input is NOT
1717
+ * hashed: pass the protocol digest you computed (e.g. Keccak-256 for
1718
+ * Ethereum, the Bitcoin signed-message hash).
1719
+ * @param {Uint8Array} message - 32-byte hash, signed as given
1720
+ * @param {Uint8Array} privateKey - 32-byte private key
1721
+ * @returns {Object} { signature: Uint8Array (64-byte r||s), recoveryId: number }
1717
1722
  */
1718
1723
  signRecoverable(message, privateKey) {
1719
1724
  const signRecoverableFn = requireWasmFunction(wasm, '_hd_secp256k1_sign_recoverable');
@@ -1738,10 +1743,11 @@ function createModule(wasm) {
1738
1743
  },
1739
1744
 
1740
1745
  /**
1741
- * Verify secp256k1 signature
1742
- * @param {Uint8Array} message - Original message
1743
- * @param {Uint8Array} signature - Signature to verify
1744
- * @param {Uint8Array} publicKey - Public key
1746
+ * Verify a secp256k1 ECDSA signature over SHA-256(message). Hashes the
1747
+ * message with SHA-256 internally: pass the message, not a hash of it.
1748
+ * @param {Uint8Array} message - Original message bytes (hashed once with SHA-256 here)
1749
+ * @param {Uint8Array} signature - 64-byte compact r||s or DER signature
1750
+ * @param {Uint8Array} publicKey - 33- or 65-byte public key
1745
1751
  * @returns {boolean} True if valid
1746
1752
  */
1747
1753
  verify(message, signature, publicKey) {
@@ -1759,11 +1765,12 @@ function createModule(wasm) {
1759
1765
  },
1760
1766
 
1761
1767
  /**
1762
- * Recover public key from signature
1763
- * @param {Uint8Array} message - Original message
1764
- * @param {Uint8Array} signature - Signature
1768
+ * Recover the public key from a recoverable signature. Like
1769
+ * signRecoverable(), the input is the 32-byte hash, used as given.
1770
+ * @param {Uint8Array} message - 32-byte hash that was signed
1771
+ * @param {Uint8Array} signature - 64-byte r||s
1765
1772
  * @param {number} recoveryId - Recovery ID (0-3)
1766
- * @returns {Uint8Array} Recovered public key
1773
+ * @returns {Uint8Array} Recovered 33-byte compressed public key
1767
1774
  */
1768
1775
  recover(message, signature, recoveryId) {
1769
1776
  const recoverFn = requireWasmFunction(wasm, '_hd_secp256k1_recover');
@@ -1881,6 +1888,13 @@ function createModule(wasm) {
1881
1888
  * P-256 (secp256r1) ECDSA operations
1882
1889
  */
1883
1890
  p256: {
1891
+ /**
1892
+ * Sign a message with P-256 ECDSA. Hashes the message with SHA-256
1893
+ * internally: pass the message, never a hash of it.
1894
+ * @param {Uint8Array} message - Message bytes (hashed once with SHA-256 here)
1895
+ * @param {Uint8Array} privateKey - 32-byte private key
1896
+ * @returns {Uint8Array} 64-byte compact signature r||s
1897
+ */
1884
1898
  sign(message, privateKey) {
1885
1899
  const msgPtr = allocAndCopy(wasm, message);
1886
1900
  const keyPtr = allocAndCopy(wasm, privateKey);
@@ -1897,6 +1911,13 @@ function createModule(wasm) {
1897
1911
  }
1898
1912
  },
1899
1913
 
1914
+ /**
1915
+ * Verify a P-256 ECDSA signature over SHA-256(message).
1916
+ * @param {Uint8Array} message - Original message bytes (hashed once with SHA-256 here)
1917
+ * @param {Uint8Array} signature - 64-byte compact signature r||s
1918
+ * @param {Uint8Array} publicKey - 33- or 65-byte public key
1919
+ * @returns {boolean} True if valid
1920
+ */
1900
1921
  verify(message, signature, publicKey) {
1901
1922
  const verifyFn = requireWasmFunction(wasm, '_hd_p256_verify');
1902
1923
  const msgPtr = allocAndCopy(wasm, message);
@@ -1934,6 +1955,13 @@ function createModule(wasm) {
1934
1955
  * P-384 (secp384r1) ECDSA operations
1935
1956
  */
1936
1957
  p384: {
1958
+ /**
1959
+ * Sign a message with P-384 ECDSA. Hashes the message with SHA-384
1960
+ * internally: pass the message, never a hash of it.
1961
+ * @param {Uint8Array} message - Message bytes (hashed once with SHA-384 here)
1962
+ * @param {Uint8Array} privateKey - 48-byte private key
1963
+ * @returns {Uint8Array} 96-byte compact signature r||s
1964
+ */
1937
1965
  sign(message, privateKey) {
1938
1966
  const msgPtr = allocAndCopy(wasm, message);
1939
1967
  const keyPtr = allocAndCopy(wasm, privateKey);
@@ -1950,6 +1978,13 @@ function createModule(wasm) {
1950
1978
  }
1951
1979
  },
1952
1980
 
1981
+ /**
1982
+ * Verify a P-384 ECDSA signature over SHA-384(message).
1983
+ * @param {Uint8Array} message - Original message bytes (hashed once with SHA-384 here)
1984
+ * @param {Uint8Array} signature - 96-byte compact signature r||s
1985
+ * @param {Uint8Array} publicKey - 49- or 97-byte public key
1986
+ * @returns {boolean} True if valid
1987
+ */
1953
1988
  verify(message, signature, publicKey) {
1954
1989
  const verifyFn = requireWasmFunction(wasm, '_hd_p384_verify');
1955
1990
  const msgPtr = allocAndCopy(wasm, message);
@@ -4707,7 +4742,7 @@ export function buildEncryptionPath(coinType, account = '0', index = '0') {
4707
4742
  * const seed = wallet.mnemonic.toSeed(mnemonic);
4708
4743
  * const root = wallet.hdkey.fromSeed(seed);
4709
4744
  * const { privateKey, publicKey, path } = getSigningKey(root, 60);
4710
- * const sig = wallet.curves.secp256k1.sign(messageHash, privateKey);
4745
+ * const sig = wallet.curves.secp256k1.sign(message, privateKey); // hashes with SHA-256
4711
4746
  */
4712
4747
  export function getSigningKey(hdRoot, coinType, account = '0', index = '0') {
4713
4748
  const path = buildSigningPath(coinType, account, index);
@@ -4816,4 +4851,7 @@ export {
4816
4851
  buildAllChainProofs,
4817
4852
  verifyChainProof,
4818
4853
  verifyAllChainProofs,
4854
+ compactSignatureToDer,
4855
+ bitcoinSignedMessageHash,
4856
+ ethereumPersonalSignHash,
4819
4857
  } from './epm-attestation.mjs';
@@ -105,16 +105,35 @@ function detectSigningCurve(payload) {
105
105
  return 'secp256k1';
106
106
  }
107
107
 
108
+ function bytesEqual(a, b) {
109
+ if (a.length !== b.length) {
110
+ return false;
111
+ }
112
+ let diff = 0;
113
+ for (let i = 0; i < a.length; i += 1) {
114
+ diff |= a[i] ^ b[i];
115
+ }
116
+ return diff === 0;
117
+ }
118
+
119
+ // secp256k1-sha256 is ECDSA over SHA-256(message), hashed once by
120
+ // curves.secp256k1.sign/verify. The digest is reported, never signed: a
121
+ // caller digest that is not SHA-256(message) is refused rather than signed
122
+ // (which would be ECDSA over SHA-256(digest)).
108
123
  function resolveSigningInput(wallet, payload, curve) {
109
124
  const messageBytes = toUint8Array(
110
125
  payload.messageBytes ?? payload.message ?? payload.protectedRecordBytes,
111
126
  'message'
112
127
  );
113
128
  if (curve === 'secp256k1') {
114
- return {
115
- messageBytes,
116
- digest: optionalUint8Array(payload.digest, 'digest') ?? wallet.utils.sha256(messageBytes),
117
- };
129
+ const digest = wallet.utils.sha256(messageBytes);
130
+ const callerDigest = optionalUint8Array(payload.digest, 'digest');
131
+ if (callerDigest && !bytesEqual(callerDigest, digest)) {
132
+ throw new Error(
133
+ 'secp256k1-sha256 signs the message bytes; digest must be omitted or equal SHA-256(message).'
134
+ );
135
+ }
136
+ return { messageBytes, digest };
118
137
  }
119
138
  return {
120
139
  messageBytes,
@@ -133,11 +152,13 @@ function resolveSignatureEnvelope(wallet, payload, walletSign) {
133
152
  }
134
153
 
135
154
  if (typeof walletSign === 'function' && !payload.signerPrivateKey) {
155
+ // secp256k1: the wallet gets the message and hashes it once itself; it is
156
+ // never handed a digest to sign. ed25519 prehash keeps its digest input.
136
157
  const response = walletSign({
137
158
  curve,
138
159
  payload,
139
160
  messageBytes,
140
- digest,
161
+ digest: curve === 'secp256k1' ? null : digest,
141
162
  });
142
163
  if (!response || typeof response !== 'object') {
143
164
  throw new Error('walletSign capability must return a signature envelope.');
@@ -181,7 +202,7 @@ function resolveSignatureEnvelope(wallet, payload, walletSign) {
181
202
  algorithm: payload.algorithm ?? 'secp256k1-sha256',
182
203
  messageBytes,
183
204
  digest,
184
- signature: wallet.curves.secp256k1.sign(digest, privateKey),
205
+ signature: wallet.curves.secp256k1.sign(messageBytes, privateKey),
185
206
  publicKey:
186
207
  optionalUint8Array(payload.publicKey, 'publicKey') ??
187
208
  wallet.curves.publicKeyFromPrivate(privateKey, 0),
@@ -193,7 +214,7 @@ function resolveVerificationEnvelope(wallet, payload) {
193
214
  const signature = toUint8Array(payload.signature, 'signature');
194
215
  const publicKey = toUint8Array(payload.publicKey, 'publicKey');
195
216
  const { messageBytes, digest } = resolveSigningInput(wallet, payload, curve);
196
- const verificationInput = curve === 'secp256k1' ? digest : digest ?? messageBytes;
217
+ const verificationInput = curve === 'secp256k1' ? messageBytes : digest ?? messageBytes;
197
218
  const valid =
198
219
  curve === 'ed25519'
199
220
  ? wallet.curves.ed25519.verify(verificationInput, signature, publicKey)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-wallet-wasm",
3
- "version": "2.0.32",
3
+ "version": "2.1.0",
4
4
  "description": "Comprehensive HD Wallet implementation in WebAssembly - BIP-32/39/44, multi-curve, multi-chain support",
5
5
  "type": "module",
6
6
  "main": "./dist/runtime/index.mjs",
@@ -92,5 +92,5 @@
92
92
  "devDependencies": {
93
93
  "@types/node": "^20.0.0"
94
94
  },
95
- "gitHead": "040f9ccc2f9f49069a4d1657c09b743beff3aec6"
95
+ "gitHead": "62e752dfd9b7163619bfbe61a52b158532fc59c1"
96
96
  }