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.
- package/README.md +20 -3
- package/dist/hd-wallet-wasi.wasm +0 -0
- package/dist/hd-wallet.js +1 -1
- package/dist/runtime/epm-attestation.d.ts +9 -0
- package/dist/runtime/epm-attestation.mjs +198 -24
- package/dist/runtime/index.d.ts +14 -2
- package/dist/runtime/index.mjs +55 -17
- package/dist/runtime/sdn-plugin.mjs +28 -7
- package/package.json +2 -2
|
@@ -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
|
|
223
|
-
*
|
|
224
|
-
*
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
345
|
+
if (!isStrictSecp256k1Der(sig)) return false;
|
|
346
|
+
return wallet.curves.secp256k1.verify(content, sig, publicKey);
|
|
269
347
|
}
|
|
270
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
394
|
-
|
|
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;
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Transaction building and signing
|
|
10
10
|
*
|
|
11
11
|
* @module hd-wallet-wasm
|
|
12
|
-
* @version 2.0
|
|
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
|
-
*
|
|
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}
|
|
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
|
|
1714
|
-
*
|
|
1715
|
-
*
|
|
1716
|
-
* @
|
|
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
|
-
*
|
|
1743
|
-
* @param {Uint8Array}
|
|
1744
|
-
* @param {Uint8Array}
|
|
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
|
-
*
|
|
1764
|
-
* @param {Uint8Array}
|
|
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(
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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(
|
|
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' ?
|
|
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
|
|
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": "
|
|
95
|
+
"gitHead": "62e752dfd9b7163619bfbe61a52b158532fc59c1"
|
|
96
96
|
}
|