hd-wallet-wasm 2.1.0 → 2.1.2

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.
@@ -51,6 +51,7 @@ import {
51
51
  Curve as AlignedCurve,
52
52
  Error as AlignedErrorCode,
53
53
  } from './generated/aligned/hd_wallet_aligned.mjs';
54
+ import { createInputValidation } from './input-validation.mjs';
54
55
 
55
56
  // Re-export for external use
56
57
  export { AlignedCurve, AlignedErrorCode };
@@ -90,12 +91,54 @@ const ERROR_MESSAGES = {
90
91
  */
91
92
  export class AlignedError extends Error {
92
93
  constructor(code, message) {
93
- super(message || ERROR_MESSAGES[code] || `Error code: ${code}`);
94
+ const lookup = code < 0 ? -code : code;
95
+ super(message || ERROR_MESSAGES[lookup] || `Error code: ${code}`);
94
96
  this.name = 'AlignedError';
95
- this.code = code;
97
+ this.code = lookup;
96
98
  }
97
99
  }
98
100
 
101
+ // Input validation shared with the main API (input-validation.mjs): every
102
+ // key, hash and signature is checked for type, length and (keys) validity
103
+ // before it is written into a request.
104
+ const V = createInputValidation((code, message) => new AlignedError(code, message));
105
+
106
+ // Largest batch: the request structs carry 32-bit counts, and the result
107
+ // buffers are allocated up front.
108
+ const MAX_BATCH = 0x100000;
109
+
110
+ // Allocate `size` bytes and zero them, so no field a request leaves unset is
111
+ // read from stale heap.
112
+ function allocZeroed(wasm, size) {
113
+ const pointer = wasm._hd_alloc(Math.max(1, size));
114
+ if (!pointer) {
115
+ throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
116
+ }
117
+ wasm.HEAPU8.fill(0, pointer, pointer + Math.max(1, size));
118
+ return pointer;
119
+ }
120
+
121
+ // Curves the batch APIs sign and verify with; P-384 signatures do not fit the
122
+ // 64-byte entries and X25519 does not sign, so entries for them report
123
+ // NOT_SUPPORTED.
124
+ const BATCH_CURVES = Object.freeze([
125
+ AlignedCurve.SECP256K1,
126
+ AlignedCurve.ED25519,
127
+ AlignedCurve.P256,
128
+ AlignedCurve.P384,
129
+ AlignedCurve.X25519,
130
+ ]);
131
+
132
+ // The batch exports return the number of entries written, or a negative
133
+ // -Error code. Only a count within [0, capacity] is success; anything else
134
+ // throws before a result entry is read.
135
+ function checkCount(result, capacity) {
136
+ if (!Number.isInteger(result) || result < 0 || result > capacity) {
137
+ throw new AlignedError(result < 0 ? result : AlignedErrorCode.INTERNAL);
138
+ }
139
+ return result;
140
+ }
141
+
99
142
  // =============================================================================
100
143
  // AlignedKeyDeriver - Batch Key Derivation
101
144
  // =============================================================================
@@ -141,6 +184,8 @@ export class AlignedKeyDeriver {
141
184
  * @returns {Array<DerivedKey>} Array of derived keys
142
185
  */
143
186
  deriveBatch(baseKey, startIndex, count, hardened = false) {
187
+ V.uint32(startIndex, 'startIndex');
188
+ V.integer(count, 'count', 0, MAX_BATCH);
144
189
  // Short-circuit: requesting zero keys returns empty array
145
190
  if (count === 0) {
146
191
  return [];
@@ -149,10 +194,7 @@ export class AlignedKeyDeriver {
149
194
  const wasm = this.#wasm;
150
195
 
151
196
  // Allocate request buffer
152
- const requestPtr = wasm._hd_alloc(BATCHDERIVEREQUEST_SIZE);
153
- if (!requestPtr) {
154
- throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
155
- }
197
+ const requestPtr = allocZeroed(wasm, BATCHDERIVEREQUEST_SIZE);
156
198
 
157
199
  try {
158
200
  // Fill request
@@ -171,15 +213,11 @@ export class AlignedKeyDeriver {
171
213
 
172
214
  try {
173
215
  // Call WASM function
174
- const derivedCount = wasm._hd_aligned_derive_batch(
216
+ const derivedCount = checkCount(wasm._hd_aligned_derive_batch(
175
217
  requestPtr,
176
218
  resultsPtr,
177
219
  count
178
- );
179
-
180
- if (derivedCount < 0) {
181
- throw new AlignedError(derivedCount);
182
- }
220
+ ), count);
183
221
 
184
222
  // Read results
185
223
  const results = [];
@@ -203,9 +241,13 @@ export class AlignedKeyDeriver {
203
241
 
204
242
  return results;
205
243
  } finally {
244
+ // Entries carry derived private keys.
245
+ wasm._hd_secure_wipe(resultsPtr, resultsSize);
206
246
  wasm._hd_dealloc(resultsPtr);
207
247
  }
208
248
  } finally {
249
+ // The request carries the base private key.
250
+ wasm._hd_secure_wipe(requestPtr, BATCHDERIVEREQUEST_SIZE);
209
251
  wasm._hd_dealloc(requestPtr);
210
252
  }
211
253
  }
@@ -221,23 +263,28 @@ export class AlignedKeyDeriver {
221
263
  */
222
264
  *streamKeys(baseKey, startIndex = 0, batchSize = 100, hardened = false) {
223
265
  const wasm = this.#wasm;
266
+ V.uint32(startIndex, 'startIndex');
267
+ // The results buffer holds exactly batchSize entries.
268
+ V.integer(batchSize, 'batchSize', 1, MAX_BATCH);
224
269
 
225
270
  // Allocate config buffer
226
- const configPtr = wasm._hd_alloc(STREAMDERIVECONFIG_SIZE);
227
- if (!configPtr) {
228
- throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
229
- }
271
+ const configPtr = allocZeroed(wasm, STREAMDERIVECONFIG_SIZE);
230
272
 
231
- // Fill config
232
- const configView = StreamDeriveConfigView.fromMemory(wasm.wasmMemory, configPtr);
233
- this.#fillBaseKey(configView, baseKey);
234
- configView.start_index = startIndex >>> 0;
235
- configView.batch_size = batchSize >>> 0;
236
- configView.hardened = hardened ? 1 : 0;
273
+ // Fill config, create the stream, then wipe the config (it carries the
274
+ // base private key) whatever happens.
275
+ let streamHandle;
276
+ try {
277
+ const configView = StreamDeriveConfigView.fromMemory(wasm.wasmMemory, configPtr);
278
+ this.#fillBaseKey(configView, baseKey);
279
+ configView.start_index = startIndex >>> 0;
280
+ configView.batch_size = batchSize >>> 0;
281
+ configView.hardened = hardened ? 1 : 0;
237
282
 
238
- // Create stream
239
- const streamHandle = wasm._hd_aligned_stream_create(configPtr);
240
- wasm._hd_dealloc(configPtr);
283
+ streamHandle = wasm._hd_aligned_stream_create(configPtr);
284
+ } finally {
285
+ wasm._hd_secure_wipe(configPtr, STREAMDERIVECONFIG_SIZE);
286
+ wasm._hd_dealloc(configPtr);
287
+ }
241
288
 
242
289
  if (!streamHandle) {
243
290
  throw new AlignedError(AlignedErrorCode.INTERNAL, 'Failed to create stream');
@@ -253,13 +300,15 @@ export class AlignedKeyDeriver {
253
300
 
254
301
  try {
255
302
  while (true) {
256
- const derivedCount = wasm._hd_aligned_stream_next(
303
+ // 0 ends the stream; a negative -Error throws instead of ending it
304
+ // silently.
305
+ const derivedCount = checkCount(wasm._hd_aligned_stream_next(
257
306
  streamHandle,
258
307
  resultsPtr,
259
308
  batchSize
260
- );
309
+ ), batchSize);
261
310
 
262
- if (derivedCount <= 0) {
311
+ if (derivedCount === 0) {
263
312
  break;
264
313
  }
265
314
 
@@ -286,6 +335,7 @@ export class AlignedKeyDeriver {
286
335
  yield batch;
287
336
  }
288
337
  } finally {
338
+ wasm._hd_secure_wipe(resultsPtr, resultsSize);
289
339
  wasm._hd_dealloc(resultsPtr);
290
340
  wasm._hd_aligned_stream_destroy(streamHandle);
291
341
  }
@@ -296,11 +346,18 @@ export class AlignedKeyDeriver {
296
346
  * @private
297
347
  */
298
348
  #fillBaseKey(view, baseKey) {
299
- if (baseKey._handle) {
349
+ if (baseKey && baseKey._handle) {
300
350
  // HDKey object - use conversion function
301
351
  const keyDataPtr = this.#wasm._hd_alloc(EXTENDEDKEYDATA_SIZE);
352
+ if (!keyDataPtr) {
353
+ throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
354
+ }
302
355
  try {
303
- this.#wasm._hd_aligned_from_extended_key(baseKey._handle, keyDataPtr);
356
+ // On failure the buffer holds no key: never copy it.
357
+ const result = this.#wasm._hd_aligned_from_extended_key(baseKey._handle, keyDataPtr);
358
+ if (result !== 0) {
359
+ throw new AlignedError(result);
360
+ }
304
361
  const keyDataView = ExtendedKeyDataView.fromMemory(
305
362
  this.#wasm.wasmMemory, keyDataPtr
306
363
  );
@@ -314,14 +371,27 @@ export class AlignedKeyDeriver {
314
371
  view.base_key_private_key_data.set(keyDataView.private_key_data);
315
372
  view.base_key_has_private_key = keyDataView.has_private_key;
316
373
  } finally {
374
+ this.#wasm._hd_secure_wipe(keyDataPtr, EXTENDEDKEYDATA_SIZE);
317
375
  this.#wasm._hd_dealloc(keyDataPtr);
318
376
  }
319
- } else if (baseKey.privateKey && baseKey.chainCode) {
320
- // Plain object with key data
321
- view.base_key_curve = baseKey.curve ?? AlignedCurve.SECP256K1;
322
- view.base_key_depth = baseKey.depth ?? 0;
323
- view.base_key_parent_fingerprint = baseKey.parentFingerprint ?? 0;
324
- view.base_key_child_index = baseKey.childIndex ?? 0;
377
+ } else if (baseKey && baseKey.privateKey && baseKey.chainCode) {
378
+ // Plain object with key data: every field at its exact size, the keys
379
+ // valid for the curve (32-byte scalars, 33-byte compressed keys).
380
+ const curve = baseKey.curve ?? AlignedCurve.SECP256K1;
381
+ V.curve(curve, [AlignedCurve.SECP256K1, AlignedCurve.P256]);
382
+ const depth = baseKey.depth ?? 0;
383
+ const parentFingerprint = baseKey.parentFingerprint ?? 0;
384
+ const childIndex = baseKey.childIndex ?? 0;
385
+ V.integer(depth, 'depth', 0, 255);
386
+ V.uint32(parentFingerprint, 'parentFingerprint');
387
+ V.uint32(childIndex, 'childIndex');
388
+ V.bytesOfLength(baseKey.chainCode, 32, 'chainCode');
389
+ V.privateKey(this.#wasm, baseKey.privateKey, curve);
390
+ V.publicKey(this.#wasm, baseKey.publicKey, curve, 'publicKey', [33]);
391
+ view.base_key_curve = curve;
392
+ view.base_key_depth = depth;
393
+ view.base_key_parent_fingerprint = parentFingerprint;
394
+ view.base_key_child_index = childIndex;
325
395
  view.base_key_chain_code_data.set(baseKey.chainCode);
326
396
  view.base_key_public_key_data.set(baseKey.publicKey);
327
397
  view.base_key_private_key_data.set(baseKey.privateKey);
@@ -371,13 +441,28 @@ export class AlignedSigner {
371
441
  */
372
442
  signBatch(privateKey, hashes, curve = AlignedCurve.SECP256K1) {
373
443
  const wasm = this.#wasm;
374
- const count = hashes.length;
444
+ V.curve(curve, BATCH_CURVES);
445
+ // The array and each entry are read once (a Proxy or a getter could answer
446
+ // a second read differently); only the copies are used below.
447
+ const hashCount = Array.isArray(hashes) ? hashes.length : -1;
448
+ if (!Number.isInteger(hashCount) || hashCount < 0 || hashCount > MAX_BATCH) {
449
+ throw new AlignedError(AlignedErrorCode.INVALID_ARGUMENT, `hashes must be an array of at most ${MAX_BATCH}`);
450
+ }
451
+ const hashCopies = [];
452
+ for (let i = 0; i < hashCount; i++) hashCopies.push(V.snapshot(hashes[i], 'hash'));
453
+ // The request holds a 32-byte key: secp256k1 and P-256 keys must be
454
+ // 0 < d < n; Ed25519 seeds are any 32 bytes. P-384 and X25519 entries
455
+ // report NOT_SUPPORTED.
456
+ if (curve === AlignedCurve.SECP256K1 || curve === AlignedCurve.P256 || curve === AlignedCurve.ED25519) {
457
+ V.privateKey(wasm, privateKey, curve);
458
+ } else {
459
+ V.bytesOfLength(privateKey, 32, 'privateKey', AlignedErrorCode.INVALID_PRIVATE_KEY);
460
+ }
461
+ for (const hash of hashCopies) V.bytesOfLength(hash, 32, 'hash');
462
+ const count = hashCopies.length;
375
463
 
376
464
  // Allocate request
377
- const requestPtr = wasm._hd_alloc(BATCHSIGNREQUEST_SIZE);
378
- if (!requestPtr) {
379
- throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
380
- }
465
+ const requestPtr = allocZeroed(wasm, BATCHSIGNREQUEST_SIZE);
381
466
 
382
467
  try {
383
468
  // Fill request
@@ -387,38 +472,28 @@ export class AlignedSigner {
387
472
  requestView.count = count;
388
473
 
389
474
  // Allocate and fill hashes
390
- const hashesPtr = wasm._hd_alloc(count * HASHENTRY_SIZE);
391
- if (!hashesPtr) {
392
- throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
393
- }
475
+ const hashesPtr = allocZeroed(wasm, count * HASHENTRY_SIZE);
394
476
 
395
477
  try {
396
478
  const hashesArray = HashEntryArrayView.fromMemory(wasm.wasmMemory, hashesPtr, count);
397
479
  for (let i = 0; i < count; i++) {
398
480
  const entry = hashesArray.at(i);
399
481
  entry.index = i;
400
- entry.hash_data.set(hashes[i]);
482
+ entry.hash_data.set(hashCopies[i]);
401
483
  }
402
484
 
403
485
  // Allocate results
404
- const resultsPtr = wasm._hd_alloc(count * SIGNATUREENTRY_SIZE);
405
- if (!resultsPtr) {
406
- throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
407
- }
486
+ const resultsPtr = allocZeroed(wasm, count * SIGNATUREENTRY_SIZE);
408
487
 
409
488
  try {
410
489
  // Call WASM function
411
- const signedCount = wasm._hd_aligned_sign_batch(
490
+ const signedCount = checkCount(wasm._hd_aligned_sign_batch(
412
491
  requestPtr,
413
492
  hashesPtr,
414
493
  count,
415
494
  resultsPtr,
416
495
  count
417
- );
418
-
419
- if (signedCount < 0) {
420
- throw new AlignedError(signedCount);
421
- }
496
+ ), count);
422
497
 
423
498
  // Read results
424
499
  const results = [];
@@ -446,6 +521,8 @@ export class AlignedSigner {
446
521
  wasm._hd_dealloc(hashesPtr);
447
522
  }
448
523
  } finally {
524
+ // The request carries the private key.
525
+ wasm._hd_secure_wipe(requestPtr, BATCHSIGNREQUEST_SIZE);
449
526
  wasm._hd_dealloc(requestPtr);
450
527
  }
451
528
  }
@@ -460,55 +537,83 @@ export class AlignedSigner {
460
537
  */
461
538
  verifyBatch(publicKey, entries, curve = AlignedCurve.SECP256K1) {
462
539
  const wasm = this.#wasm;
463
- const count = entries.length;
540
+ V.curve(curve, BATCH_CURVES);
541
+ // The array, each entry and each field are read once (a Proxy or a getter
542
+ // could answer a second read differently); only the copies are used below.
543
+ const entryCount = Array.isArray(entries) ? entries.length : -1;
544
+ if (!Number.isInteger(entryCount) || entryCount < 0 || entryCount > MAX_BATCH) {
545
+ throw new AlignedError(AlignedErrorCode.INVALID_ARGUMENT, `entries must be an array of at most ${MAX_BATCH}`);
546
+ }
547
+ // Types are programmer errors (throw); wrong lengths or an invalid key
548
+ // only make the affected entries invalid.
549
+ const publicKeyCopy = V.snapshot(publicKey, 'publicKey');
550
+ const entryCopies = [];
551
+ for (let i = 0; i < entryCount; i++) {
552
+ const entry = entries[i];
553
+ if (entry === null || typeof entry !== 'object') {
554
+ throw new AlignedError(AlignedErrorCode.INVALID_ARGUMENT, 'each entry must be { hash, signature }');
555
+ }
556
+ const { hash, signature } = entry;
557
+ entryCopies.push({ hash: V.snapshot(hash, 'hash'), signature: V.snapshot(signature, 'signature') });
558
+ }
559
+ const count = entryCopies.length;
560
+
561
+ // The request holds a 33-byte key: a compressed secp256k1 or P-256 key,
562
+ // or an Ed25519 key after one padding byte.
563
+ let keyBytes = null;
564
+ if (curve === AlignedCurve.ED25519) {
565
+ if (publicKeyCopy.length === 32) {
566
+ keyBytes = new Uint8Array(33);
567
+ keyBytes.set(publicKeyCopy, 1);
568
+ } else if (publicKeyCopy.length === 33) {
569
+ keyBytes = publicKeyCopy;
570
+ }
571
+ } else if (curve === AlignedCurve.SECP256K1 || curve === AlignedCurve.P256) {
572
+ if (V.isPublicKey(wasm, publicKeyCopy, curve, [33])) keyBytes = publicKeyCopy;
573
+ } else {
574
+ // P-384 / X25519: the export reports NOT_SUPPORTED for every entry.
575
+ keyBytes = new Uint8Array(33);
576
+ }
577
+ const entryValid = entryCopies.map((entry) => keyBytes !== null &&
578
+ entry.hash.length === 32 && entry.signature.length === 64);
464
579
 
465
580
  // Allocate request
466
- const requestPtr = wasm._hd_alloc(BATCHVERIFYREQUEST_SIZE);
467
- if (!requestPtr) {
468
- throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
469
- }
581
+ const requestPtr = allocZeroed(wasm, BATCHVERIFYREQUEST_SIZE);
470
582
 
471
583
  try {
472
584
  // Fill request
473
585
  const requestView = BatchVerifyRequestView.fromMemory(wasm.wasmMemory, requestPtr);
474
- requestView.public_key_data.set(publicKey);
586
+ if (keyBytes !== null) requestView.public_key_data.set(keyBytes);
475
587
  requestView.curve = curve;
476
588
  requestView.count = count;
477
589
 
478
- // Allocate and fill entries
479
- const entriesPtr = wasm._hd_alloc(count * VERIFYENTRY_SIZE);
480
- if (!entriesPtr) {
481
- throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
482
- }
590
+ // Allocate and fill entries (an invalid entry stays all zero and its
591
+ // result is reported invalid below whatever the export says)
592
+ const entriesPtr = allocZeroed(wasm, count * VERIFYENTRY_SIZE);
483
593
 
484
594
  try {
485
595
  const entriesArray = VerifyEntryArrayView.fromMemory(wasm.wasmMemory, entriesPtr, count);
486
596
  for (let i = 0; i < count; i++) {
487
597
  const view = entriesArray.at(i);
488
598
  view.index = i;
489
- view.hash_data.set(entries[i].hash);
490
- view.signature_data.set(entries[i].signature);
599
+ if (entryValid[i]) {
600
+ view.hash_data.set(entryCopies[i].hash);
601
+ view.signature_data.set(entryCopies[i].signature);
602
+ }
491
603
  }
492
604
 
493
605
  // Allocate results
494
- const resultsPtr = wasm._hd_alloc(count * VERIFYRESULTENTRY_SIZE);
495
- if (!resultsPtr) {
496
- throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
497
- }
606
+ const resultsPtr = allocZeroed(wasm, count * VERIFYRESULTENTRY_SIZE);
498
607
 
499
608
  try {
500
609
  // Call WASM function
501
- const verifiedCount = wasm._hd_aligned_verify_batch(
610
+ const verifiedCount = checkCount(wasm._hd_aligned_verify_batch(
502
611
  requestPtr,
503
612
  entriesPtr,
504
613
  count,
505
614
  resultsPtr,
506
615
  count
507
- );
508
-
509
- if (verifiedCount < 0) {
510
- throw new AlignedError(verifiedCount);
511
- }
616
+ ), count);
512
617
 
513
618
  // Read results
514
619
  const results = [];
@@ -518,6 +623,16 @@ export class AlignedSigner {
518
623
 
519
624
  for (let i = 0; i < verifiedCount; i++) {
520
625
  const entry = resultsArray.at(i);
626
+ if (!entryValid[i]) {
627
+ results.push({
628
+ index: i,
629
+ valid: false,
630
+ error: keyBytes === null
631
+ ? AlignedErrorCode.INVALID_PUBLIC_KEY
632
+ : AlignedErrorCode.INVALID_SIGNATURE,
633
+ });
634
+ continue;
635
+ }
521
636
  results.push({
522
637
  index: entry.index,
523
638
  valid: entry.error === AlignedErrorCode.OK,
@@ -588,6 +703,9 @@ export class AlignedAPI {
588
703
  */
589
704
  keyToBytes(hdKey) {
590
705
  const wasm = this.#wasm;
706
+ if (!hdKey || !hdKey._handle) {
707
+ throw new AlignedError(AlignedErrorCode.INVALID_ARGUMENT, 'hdKey must be a live HDKey');
708
+ }
591
709
  const ptr = wasm._hd_alloc(EXTENDEDKEYDATA_SIZE);
592
710
  if (!ptr) {
593
711
  throw new AlignedError(AlignedErrorCode.OUT_OF_MEMORY);
@@ -600,6 +718,7 @@ export class AlignedAPI {
600
718
  }
601
719
  return new Uint8Array(wasm.HEAPU8.buffer, ptr, EXTENDEDKEYDATA_SIZE).slice();
602
720
  } finally {
721
+ wasm._hd_secure_wipe(ptr, EXTENDEDKEYDATA_SIZE);
603
722
  wasm._hd_dealloc(ptr);
604
723
  }
605
724
  }
@@ -224,6 +224,15 @@ export function buildEPMSigningContent(epm) {
224
224
  const SECP256K1_ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
225
225
  const SECP256K1_HALF_ORDER = SECP256K1_ORDER >> 1n;
226
226
 
227
+ // Hex from an attestation or proof is untrusted data: anything that is not an
228
+ // even-length hex string decodes to null, and the verification fails.
229
+ function hexOrNull(wallet, value) {
230
+ if (typeof value !== 'string' || value.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(value)) {
231
+ return null;
232
+ }
233
+ return wallet.utils.decodeHex(value);
234
+ }
235
+
227
236
  function bytesToBigInt(bytes) {
228
237
  let value = 0n;
229
238
  for (const b of bytes) value = (value << 8n) | BigInt(b);
@@ -337,7 +346,8 @@ export function verifyEPMSignature(wallet, epm, publicKey, options = {}) {
337
346
  if (!sigHex) return false;
338
347
 
339
348
  const content = buildEPMSigningContent(epm);
340
- const sig = wallet.utils.decodeHex(sigHex);
349
+ const sig = hexOrNull(wallet, sigHex);
350
+ if (sig === null) return false;
341
351
  const curve = String(
342
352
  options.curve || (publicKey && publicKey.length === 32 ? 'ed25519' : 'secp256k1'),
343
353
  ).toLowerCase();
@@ -547,9 +557,14 @@ export function buildSolanaChainProof(wallet, { address, publicKeyHex, privateKe
547
557
  * @returns {boolean} True if the signature is valid for the given public key and payload
548
558
  */
549
559
  export function verifyChainProof(wallet, proof) {
550
- const pubKey = wallet.utils.decodeHex(proof.PUBLIC_KEY);
551
- const sig = wallet.utils.decodeHex(proof.SIGNATURE);
552
- const payload = wallet.utils.decodeHex(proof.SIGNED_PAYLOAD);
560
+ if (proof === null || typeof proof !== 'object') {
561
+ throw new TypeError('proof must be a ChainProof object');
562
+ }
563
+ // A proof whose fields are not hex does not verify.
564
+ const pubKey = hexOrNull(wallet, proof.PUBLIC_KEY);
565
+ const sig = hexOrNull(wallet, proof.SIGNATURE);
566
+ const payload = hexOrNull(wallet, proof.SIGNED_PAYLOAD);
567
+ if (pubKey === null || sig === null || payload === null) return false;
553
568
 
554
569
  const algorithm = proof.ALGORITHM;
555
570
 
@@ -12,8 +12,16 @@
12
12
  // Import aligned API types
13
13
  import { AlignedAPI } from './aligned.js';
14
14
 
15
- export type BinaryLike = Uint8Array | ArrayBuffer | ArrayBufferView;
16
- export type X509CertificateInput = string | BinaryLike;
15
+ /**
16
+ * Binary input to every wallet API: a Uint8Array (Node Buffers included) of
17
+ * this realm. A DataView, an ArrayBuffer, another typed array, a detached or
18
+ * cross-realm array is refused.
19
+ */
20
+ export type BinaryLike = Uint8Array;
21
+ /** A PEM string or its bytes, or DER bytes. */
22
+ export type X509CertificateInput = string | Uint8Array;
23
+ /** What an SDN plugin capability callback may return (it is copied to a Uint8Array). */
24
+ export type PluginBinaryLike = Uint8Array | ArrayBuffer | ArrayBufferView;
17
25
 
18
26
  export interface SdnPluginTypeRef {
19
27
  schemaName: string | null;
@@ -113,7 +121,7 @@ export interface SdnPluginInvocationResult {
113
121
  }
114
122
 
115
123
  export interface SdnPluginCapabilities {
116
- randomBytes?: (length: number, context?: Record<string, unknown>) => BinaryLike;
124
+ randomBytes?: (length: number, context?: Record<string, unknown>) => PluginBinaryLike;
117
125
  /**
118
126
  * Wallet-held signer for sign_detached. For secp256k1 it receives the message
119
127
  * (digest is null) and must return ECDSA over SHA-256(messageBytes), hashed once.
@@ -124,8 +132,8 @@ export interface SdnPluginCapabilities {
124
132
  messageBytes: Uint8Array;
125
133
  digest: Uint8Array | null;
126
134
  }) => {
127
- signature: BinaryLike;
128
- publicKey: BinaryLike;
135
+ signature: PluginBinaryLike;
136
+ publicKey: PluginBinaryLike;
129
137
  algorithm?: string;
130
138
  };
131
139
  }
@@ -818,7 +826,12 @@ export interface BitcoinTxBuilder {
818
826
 
819
827
  export interface BitcoinTx {
820
828
  addInput(txid: string, vout: number, sequence?: number): this;
821
- addOutput(address: string, amount: bigint): this;
829
+ /**
830
+ * Pay `address`: P2PKH, P2SH, P2WPKH (v0, 20 bytes), P2WSH (v0, 32 bytes)
831
+ * or P2TR (v1, 32 bytes) of `network` (mainnet by default). Any other
832
+ * address, or one of the other network, throws (BIP-350).
833
+ */
834
+ addOutput(address: string, amount: bigint, network?: Network): this;
822
835
  sign(inputIndex: number, privateKey: Uint8Array, redeemScript?: Uint8Array): this;
823
836
  serialize(): Uint8Array;
824
837
  getTxid(): string;
@@ -867,6 +880,11 @@ export interface EthereumAPI {
867
880
  tx: EthereumTxBuilder;
868
881
  }
869
882
 
883
+ /**
884
+ * `nonce` and `chainId` are required: a missing nonce or chainId, or a chainId
885
+ * outside 1 to 2^63 - 37 (EIP-2294), throws (an explicit nonce 0 is valid).
886
+ * Each field is read once.
887
+ */
870
888
  export interface EthereumTxBuilder {
871
889
  create(params: {
872
890
  nonce: number;
@@ -1044,7 +1062,7 @@ export interface UtilsAPI {
1044
1062
 
1045
1063
  export interface X509WalletAttestationOptions {
1046
1064
  curve: Curve;
1047
- privateKey: BinaryLike;
1065
+ privateKey: Uint8Array;
1048
1066
  keyLabel?: string;
1049
1067
  commentPrefix?: string;
1050
1068
  }
@@ -1090,22 +1108,22 @@ export interface X509Pkcs12Bundle {
1090
1108
  export interface X509API {
1091
1109
  isAvailable(): boolean;
1092
1110
  generatePrivateKey(curve: Curve): Uint8Array;
1093
- exportPrivateKeyPem(curve: Curve, privateKey: BinaryLike): string;
1111
+ exportPrivateKeyPem(curve: Curve, privateKey: Uint8Array): string;
1094
1112
  importPrivateKeyPem(curve: Curve, pem: string): Uint8Array;
1095
1113
  createSelfSignedCertificate(
1096
1114
  options: X509CertificateOptions,
1097
1115
  certificateCurve: Curve,
1098
- certificatePrivateKey: BinaryLike,
1116
+ certificatePrivateKey: Uint8Array,
1099
1117
  outputEncoding?: X509Encoding
1100
1118
  ): string | Uint8Array;
1101
1119
  issueCertificate(
1102
1120
  options: X509CertificateOptions,
1103
1121
  issuerCurve: Curve,
1104
- issuerPrivateKey: BinaryLike,
1122
+ issuerPrivateKey: Uint8Array,
1105
1123
  issuerCertificate: X509CertificateInput,
1106
1124
  issuerCertificateEncoding: X509Encoding,
1107
1125
  subjectCurve: Curve,
1108
- subjectPrivateKey: BinaryLike,
1126
+ subjectPrivateKey: Uint8Array,
1109
1127
  outputEncoding?: X509Encoding
1110
1128
  ): string | Uint8Array;
1111
1129
  convertCertificate(
@@ -1119,12 +1137,12 @@ export interface X509API {
1119
1137
  certificate: X509CertificateInput,
1120
1138
  certificateEncoding: X509Encoding,
1121
1139
  privateKeyCurve: Curve,
1122
- privateKey: BinaryLike,
1140
+ privateKey: Uint8Array,
1123
1141
  password: string,
1124
1142
  friendlyName?: string,
1125
1143
  chainPem?: X509CertificateInput
1126
1144
  ): Uint8Array;
1127
- importPkcs12(pkcs12Bundle: BinaryLike, password: string): X509Pkcs12Bundle;
1145
+ importPkcs12(pkcs12Bundle: Uint8Array, password: string): X509Pkcs12Bundle;
1128
1146
  }
1129
1147
 
1130
1148
  // =============================================================================