@bsv/sdk 1.9.31 → 1.10.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.
Files changed (51) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/Peer.js +68 -48
  3. package/dist/cjs/src/auth/Peer.js.map +1 -1
  4. package/dist/cjs/src/identity/IdentityClient.js +124 -20
  5. package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
  6. package/dist/cjs/src/primitives/BigNumber.js +28 -54
  7. package/dist/cjs/src/primitives/BigNumber.js.map +1 -1
  8. package/dist/cjs/src/primitives/ECDSA.js +36 -1
  9. package/dist/cjs/src/primitives/ECDSA.js.map +1 -1
  10. package/dist/cjs/src/primitives/ReductionContext.js +35 -46
  11. package/dist/cjs/src/primitives/ReductionContext.js.map +1 -1
  12. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  13. package/dist/esm/src/auth/Peer.js +68 -48
  14. package/dist/esm/src/auth/Peer.js.map +1 -1
  15. package/dist/esm/src/identity/IdentityClient.js +124 -20
  16. package/dist/esm/src/identity/IdentityClient.js.map +1 -1
  17. package/dist/esm/src/primitives/BigNumber.js +28 -54
  18. package/dist/esm/src/primitives/BigNumber.js.map +1 -1
  19. package/dist/esm/src/primitives/ECDSA.js +36 -1
  20. package/dist/esm/src/primitives/ECDSA.js.map +1 -1
  21. package/dist/esm/src/primitives/ReductionContext.js +35 -46
  22. package/dist/esm/src/primitives/ReductionContext.js.map +1 -1
  23. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  24. package/dist/types/src/auth/Peer.d.ts.map +1 -1
  25. package/dist/types/src/auth/types.d.ts +2 -0
  26. package/dist/types/src/auth/types.d.ts.map +1 -1
  27. package/dist/types/src/identity/IdentityClient.d.ts +8 -0
  28. package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
  29. package/dist/types/src/primitives/BigNumber.d.ts +8 -0
  30. package/dist/types/src/primitives/BigNumber.d.ts.map +1 -1
  31. package/dist/types/src/primitives/ECDSA.d.ts +24 -0
  32. package/dist/types/src/primitives/ECDSA.d.ts.map +1 -1
  33. package/dist/types/src/primitives/ReductionContext.d.ts +9 -0
  34. package/dist/types/src/primitives/ReductionContext.d.ts.map +1 -1
  35. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  36. package/dist/umd/bundle.js +3 -3
  37. package/dist/umd/bundle.js.map +1 -1
  38. package/docs/index.md +15 -1
  39. package/docs/reference/auth.md +2 -0
  40. package/docs/reference/messages.md +0 -24
  41. package/docs/reference/primitives.md +91 -31
  42. package/package.json +1 -1
  43. package/src/auth/Peer.ts +122 -57
  44. package/src/auth/__tests/Peer.test.ts +166 -257
  45. package/src/auth/types.ts +2 -0
  46. package/src/identity/IdentityClient.ts +153 -29
  47. package/src/identity/__tests/IdentityClient.test.ts +289 -1
  48. package/src/primitives/BigNumber.ts +27 -31
  49. package/src/primitives/ECDSA.ts +41 -2
  50. package/src/primitives/ReductionContext.ts +44 -48
  51. package/src/primitives/__tests/ECDSA.test.ts +16 -0
package/docs/index.md CHANGED
@@ -59,6 +59,7 @@ Finally, you can deep dive into the details of the interface and types in the re
59
59
  - [Storage](./reference/storage.md)
60
60
  - [KV Store](./reference/kvstore.md)
61
61
  - [Messages](./reference/messages.md)
62
+ - Please note [*Security Considerations*](#security-considerations-for-encrypted-messages).
62
63
  - [TOTP](./reference/totp.md)
63
64
  - [Compatibility](./reference/compat.md)
64
65
 
@@ -74,4 +75,17 @@ Finally, you can deep dive into the details of the interface and types in the re
74
75
 
75
76
  ## Performance Reports
76
77
 
77
- - [Benchmarks](./performance.md)
78
+ - [Benchmarks](./performance.md)
79
+
80
+ ## Security Considerations for Encrypted Messages
81
+
82
+ The encrypted message protocol implemented in this SDK derives per-message encryption keys deterministically from the parties’ long-term keys and a caller-supplied invoice number (BRC-42 style derivation).
83
+
84
+ This construction does not provide the guarantees of a standard authenticated key exchange (AKE). In particular:
85
+
86
+ No forward secrecy: Compromise of a long-term private key compromises all past and future messages derived from it.
87
+ No replay protection: Messages encrypted under the same invoice number and key pair can be replayed.
88
+ Potential identity misbinding: Public keys alone do not guarantee peer identity without additional authentication or identity verification.
89
+ This protocol is intended for lightweight, deterministic messaging between parties that already trust each other’s long-term public keys. It SHOULD NOT be used for high-security or high-value communications without additional protocol-layer protections.
90
+
91
+ Applications requiring strong authentication, replay protection, or forward secrecy should use a formally analyzed protocol such as X3DH, Noise, or SIGMA.
@@ -47,6 +47,8 @@ export interface PeerSession {
47
47
  peerNonce?: string;
48
48
  peerIdentityKey?: string;
49
49
  lastUpdate: number;
50
+ certificatesRequired?: boolean;
51
+ certificatesValidated?: boolean;
50
52
  }
51
53
  ```
52
54
 
@@ -2,30 +2,6 @@
2
2
 
3
3
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Variables](#variables)
4
4
 
5
- ## Security Considerations for Encrypted Messages
6
-
7
- The encrypted message protocol implemented in this SDK derives per-message
8
- encryption keys deterministically from the parties’ long-term keys and a
9
- caller-supplied invoice number (BRC-42 style derivation).
10
-
11
- This construction does **not** provide the guarantees of a standard
12
- authenticated key exchange (AKE). In particular:
13
-
14
- - **No forward secrecy**: Compromise of a long-term private key compromises
15
- all past and future messages derived from it.
16
- - **No replay protection**: Messages encrypted under the same invoice number
17
- and key pair can be replayed.
18
- - **Potential identity misbinding**: Public keys alone do not guarantee peer
19
- identity without additional authentication or identity verification.
20
-
21
- This protocol is intended for lightweight, deterministic messaging between
22
- parties that already trust each other’s long-term public keys. It SHOULD NOT
23
- be used for high-security or high-value communications without additional
24
- protocol-layer protections.
25
-
26
- Applications requiring strong authentication, replay protection, or forward
27
- secrecy should use a formally analyzed protocol such as X3DH, Noise, or SIGMA.
28
-
29
5
  ## Interfaces
30
6
 
31
7
  ## Classes
@@ -299,6 +299,13 @@ console.log(BigNumber.wordSize); // output: 26
299
299
  Compute the multiplicative inverse of the current BigNumber in the modulus field specified by `p`.
300
300
  The multiplicative inverse is a number which when multiplied with the current BigNumber gives '1' in the modulus field.
301
301
 
302
+ SECURITY NOTE:
303
+ This implementation avoids variable-time extended Euclidean algorithms
304
+ to reduce timing side-channel leakage. However, JavaScript BigInt arithmetic
305
+ does not provide constant-time guarantees. This implementation is suitable
306
+ for browser and single-tenant environments but is not hardened against
307
+ high-resolution timing attacks in shared CPU contexts.
308
+
302
309
  ```ts
303
310
  _invmp(p: BigNumber): BigNumber
304
311
  ```
@@ -1786,6 +1793,7 @@ export default class Point extends BasePoint {
1786
1793
  getX(): BigNumber
1787
1794
  getY(): BigNumber
1788
1795
  mul(k: BigNumber | number | number[] | string): Point
1796
+ mulCT(k: BigNumber | number | number[] | string): Point
1789
1797
  mulAdd(k1: BigNumber, p2: Point, k2: BigNumber): Point
1790
1798
  jmulAdd(k1: BigNumber, p2: Point, k2: BigNumber): JPoint
1791
1799
  eq(p: Point): boolean
@@ -2508,6 +2516,32 @@ Returns
2508
2516
 
2509
2517
  #### Method deriveChild
2510
2518
 
2519
+ SECURITY NOTE – DETERMINISTIC CHILD KEY DERIVATION
2520
+
2521
+ This method derives child private keys deterministically from the caller’s
2522
+ long-term private key, the counterparty’s public key, and a caller-supplied
2523
+ invoice number using HMAC over an ECDH shared secret (BRC-42 style derivation).
2524
+
2525
+ This construction does NOT implement a formally authenticated key exchange
2526
+ (AKE) and does NOT provide the following security properties:
2527
+
2528
+ - Forward secrecy: Compromise of a long-term private key compromises all
2529
+ past and future child keys derived from it.
2530
+ - Replay protection: Child keys are deterministic for a given invoice
2531
+ number and key pair; previously observed messages can be replayed.
2532
+ - Explicit authentication / identity binding: Possession of a public key
2533
+ alone does not guarantee the intended peer identity, enabling potential
2534
+ identity misbinding attacks if higher-level identity verification is absent.
2535
+
2536
+ This derivation is intended for lightweight, deterministic key hierarchies
2537
+ where both parties already possess and trust each other’s long-term public
2538
+ keys. It SHOULD NOT be used as a drop-in replacement for a standard
2539
+ authenticated key exchange (e.g. X3DH, Noise, or SIGMA) in high-security or
2540
+ high-value contexts.
2541
+
2542
+ Any future protocol providing forward secrecy, replay protection, or strong
2543
+ peer authentication will require a versioned, breaking change.
2544
+
2511
2545
  Derives a child key with BRC-42.
2512
2546
 
2513
2547
  ```ts
@@ -3309,6 +3343,14 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
3309
3343
  ---
3310
3344
  ### Class: ReductionContext
3311
3345
 
3346
+ SECURITY NOTE:
3347
+ This reduction context avoids obvious variable-time constructs (such as
3348
+ sliding-window exponentiation and conditional modular reduction) to reduce
3349
+ timing side-channel leakage. However, JavaScript BigInt arithmetic does not
3350
+ provide constant-time guarantees. These mitigations improve resistance to
3351
+ coarse timing attacks but do not make the implementation suitable for
3352
+ hostile multi-tenant or shared-CPU environments.
3353
+
3312
3354
  A base reduction engine that provides several arithmetic operations over
3313
3355
  big numbers under a modulus context. It's particularly suitable for
3314
3356
  calculations required in cryptography algorithms and encoding schemas.
@@ -4961,14 +5003,14 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
4961
5003
 
4962
5004
  | | |
4963
5005
  | --- | --- |
4964
- | [AES](#function-aes) | [pbkdf2](#function-pbkdf2) |
4965
- | [AESGCM](#function-aesgcm) | [realHtonl](#function-realhtonl) |
4966
- | [AESGCMDecrypt](#function-aesgcmdecrypt) | [red](#function-red) |
4967
- | [assertValidHex](#function-assertvalidhex) | [swapBytes32](#function-swapbytes32) |
4968
- | [base64ToArray](#function-base64toarray) | [toArray](#function-toarray) |
5006
+ | [AES](#function-aes) | [normalizeHex](#function-normalizehex) |
5007
+ | [AESGCM](#function-aesgcm) | [pbkdf2](#function-pbkdf2) |
5008
+ | [AESGCMDecrypt](#function-aesgcmdecrypt) | [realHtonl](#function-realhtonl) |
5009
+ | [assertValidHex](#function-assertvalidhex) | [red](#function-red) |
5010
+ | [base64ToArray](#function-base64toarray) | [swapBytes32](#function-swapbytes32) |
5011
+ | [constantTimeEquals](#function-constanttimeequals) | [toArray](#function-toarray) |
4969
5012
  | [ghash](#function-ghash) | [toBase64](#function-tobase64) |
4970
5013
  | [htonl](#function-htonl) | [verifyNotNull](#function-verifynotnull) |
4971
- | [normalizeHex](#function-normalizehex) | |
4972
5014
 
4973
5015
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
4974
5016
 
@@ -5067,6 +5109,15 @@ export function base64ToArray(msg: string): number[]
5067
5109
 
5068
5110
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5069
5111
 
5112
+ ---
5113
+ ### Function: constantTimeEquals
5114
+
5115
+ ```ts
5116
+ export function constantTimeEquals(a: Uint8Array | number[], b: Uint8Array | number[]): boolean
5117
+ ```
5118
+
5119
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5120
+
5070
5121
  ---
5071
5122
  ### Function: ghash
5072
5123
 
@@ -5736,7 +5787,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
5736
5787
  ```ts
5737
5788
  incrementLeastSignificantThirtyTwoBits = function (block: Bytes): Bytes {
5738
5789
  const result = block.slice();
5739
- for (let i = 15; i !== 11; i--) {
5790
+ for (let i = 15; i > 11; i--) {
5740
5791
  result[i] = (result[i] + 1) & 255;
5741
5792
  if (result[i] !== 0) {
5742
5793
  break;
@@ -5914,16 +5965,18 @@ multiply = function (block0: Bytes, block1: Bytes): Bytes {
5914
5965
  const v = block1.slice();
5915
5966
  const z = createZeroBlock(16);
5916
5967
  for (let i = 0; i < 16; i++) {
5968
+ const b = block0[i];
5917
5969
  for (let j = 7; j >= 0; j--) {
5918
- if ((block0[i] & (1 << j)) !== 0) {
5919
- xorInto(z, v);
5970
+ const bit = (b >> j) & 1;
5971
+ const mask = -bit & 255;
5972
+ for (let k = 0; k < 16; k++) {
5973
+ z[k] ^= v[k] & mask;
5920
5974
  }
5921
- if ((v[15] & 1) !== 0) {
5922
- rightShift(v);
5923
- xorInto(v, R);
5924
- }
5925
- else {
5926
- rightShift(v);
5975
+ const lsb = v[15] & 1;
5976
+ const rmask = -lsb & 255;
5977
+ rightShift(v);
5978
+ for (let k = 0; k < 16; k++) {
5979
+ v[k] ^= R[k] & rmask;
5927
5980
  }
5928
5981
  }
5929
5982
  }
@@ -6100,9 +6153,13 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
6100
6153
 
6101
6154
  ```ts
6102
6155
  sign = (msg: BigNumber, key: BigNumber, forceLowS: boolean = false, customK?: BigNumber | ((iter: number) => BigNumber)): Signature => {
6156
+ const nBitLength = curve.n.bitLength();
6157
+ if (msg.bitLength() > nBitLength) {
6158
+ throw new Error(`ECDSA message is too large: expected <= ${nBitLength} bits. Callers must hash messages before signing.`);
6159
+ }
6103
6160
  msg = truncateToN(msg);
6104
- const msgBig = BigInt("0x" + msg.toString(16));
6105
- const keyBig = BigInt("0x" + key.toString(16));
6161
+ const msgBig = bnToBigInt(msg);
6162
+ const keyBig = bnToBigInt(key);
6106
6163
  const bkey = key.toArray("be", bytes);
6107
6164
  const nonce = msg.toArray("be", bytes);
6108
6165
  const drbg = new DRBG(bkey, nonce);
@@ -6112,26 +6169,24 @@ sign = (msg: BigNumber, key: BigNumber, forceLowS: boolean = false, customK?: Bi
6112
6169
  : BigNumber.isBN(customK)
6113
6170
  ? customK
6114
6171
  : new BigNumber(drbg.generate(bytes), 16);
6115
- if (kBN == null)
6172
+ if (kBN == null) {
6116
6173
  throw new Error("k is undefined");
6174
+ }
6117
6175
  kBN = truncateToN(kBN, true);
6118
6176
  if (kBN.cmpn(1) < 0 || kBN.cmp(ns1) > 0) {
6119
6177
  if (BigNumber.isBN(customK)) {
6120
- throw new Error("Invalid fixed custom K value (must be >1 and <N\u20111)");
6178
+ throw new Error("Invalid fixed custom K value (must be >1 and <N-1)");
6121
6179
  }
6122
6180
  continue;
6123
6181
  }
6124
- const kBig = BigInt("0x" + kBN.toString(16));
6125
- const R = scalarMultiplyWNAF(kBig, { x: GX_BIGINT, y: GY_BIGINT });
6126
- if (R.Z === 0n) {
6182
+ const R = curve.g.mulCT(kBN);
6183
+ if (R.isInfinity()) {
6127
6184
  if (BigNumber.isBN(customK)) {
6128
6185
  throw new Error("Invalid fixed custom K value (k\u00B7G at infinity)");
6129
6186
  }
6130
6187
  continue;
6131
6188
  }
6132
- const zInv = biModInv(R.Z);
6133
- const zInv2 = biModMul(zInv, zInv);
6134
- const xAff = biModMul(R.X, zInv2);
6189
+ const xAff = BigInt("0x" + R.getX().toString(16));
6135
6190
  const rBig = modN(xAff);
6136
6191
  if (rBig === 0n) {
6137
6192
  if (BigNumber.isBN(customK)) {
@@ -6139,6 +6194,7 @@ sign = (msg: BigNumber, key: BigNumber, forceLowS: boolean = false, customK?: Bi
6139
6194
  }
6140
6195
  continue;
6141
6196
  }
6197
+ const kBig = BigInt("0x" + kBN.toString(16));
6142
6198
  const kInv = modInvN(kBig);
6143
6199
  const rTimesKey = modMulN(rBig, keyBig);
6144
6200
  const sum = modN(msgBig + rTimesKey);
@@ -6159,7 +6215,7 @@ sign = (msg: BigNumber, key: BigNumber, forceLowS: boolean = false, customK?: Bi
6159
6215
  }
6160
6216
  ```
6161
6217
 
6162
- See also: [BigNumber](./primitives.md#class-bignumber), [DRBG](./primitives.md#class-drbg), [GX_BIGINT](./primitives.md#variable-gx_bigint), [GY_BIGINT](./primitives.md#variable-gy_bigint), [N_BIGINT](./primitives.md#variable-n_bigint), [Signature](./primitives.md#class-signature), [biModInv](./primitives.md#variable-bimodinv), [biModMul](./primitives.md#variable-bimodmul), [modInvN](./primitives.md#variable-modinvn), [modMulN](./primitives.md#variable-modmuln), [modN](./primitives.md#variable-modn), [scalarMultiplyWNAF](./primitives.md#variable-scalarmultiplywnaf), [toArray](./primitives.md#variable-toarray)
6218
+ See also: [BigNumber](./primitives.md#class-bignumber), [DRBG](./primitives.md#class-drbg), [N_BIGINT](./primitives.md#variable-n_bigint), [Signature](./primitives.md#class-signature), [modInvN](./primitives.md#variable-modinvn), [modMulN](./primitives.md#variable-modmuln), [modN](./primitives.md#variable-modn), [toArray](./primitives.md#variable-toarray)
6163
6219
 
6164
6220
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
6165
6221
 
@@ -6350,17 +6406,21 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
6350
6406
 
6351
6407
  ```ts
6352
6408
  verify = (msg: BigNumber, sig: Signature, key: Point): boolean => {
6353
- const hash = BigInt("0x" + msg.toString(16));
6409
+ const nBitLength = curve.n.bitLength();
6410
+ if (msg.bitLength() > nBitLength) {
6411
+ return false;
6412
+ }
6413
+ const hash = bnToBigInt(msg);
6354
6414
  if ((key.x == null) || (key.y == null)) {
6355
6415
  throw new Error("Invalid public key: missing coordinates.");
6356
6416
  }
6357
6417
  const publicKey = {
6358
- x: BigInt("0x" + key.x.toString(16)),
6359
- y: BigInt("0x" + key.y.toString(16))
6418
+ x: bnToBigInt(key.x),
6419
+ y: bnToBigInt(key.y)
6360
6420
  };
6361
6421
  const signature = {
6362
- r: BigInt("0x" + sig.r.toString(16)),
6363
- s: BigInt("0x" + sig.s.toString(16))
6422
+ r: bnToBigInt(sig.r),
6423
+ s: bnToBigInt(sig.s)
6364
6424
  };
6365
6425
  const { r, s } = signature;
6366
6426
  const z = hash;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.9.31",
3
+ "version": "1.10.2",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
package/src/auth/Peer.ts CHANGED
@@ -127,7 +127,17 @@ export class Peer {
127
127
 
128
128
  const peerSession = await this.getAuthenticatedSession(identityKey, maxWaitTime)
129
129
 
130
- // Prepare the general message
130
+ if (peerSession.peerIdentityKey == null) {
131
+ throw new Error('Peer identity is not established')
132
+ }
133
+
134
+ if (peerSession.certificatesRequired === true &&
135
+ peerSession.certificatesValidated !== true) {
136
+ throw new Error(
137
+ 'Cannot send general message before certificate validation is complete'
138
+ )
139
+ }
140
+
131
141
  const requestNonce = Utils.toBase64(Random(32))
132
142
  const { signature } = await this.wallet.createSignature({
133
143
  data: message,
@@ -339,15 +349,19 @@ export class Peer {
339
349
  identityKey?: string,
340
350
  maxWaitTime = 10000
341
351
  ): Promise<string> {
342
- const sessionNonce = await createNonce(this.wallet, undefined, this.originator) // Initial request nonce
352
+ const sessionNonce = await createNonce(this.wallet, undefined, this.originator)
343
353
 
344
- // Create the preliminary session (not yet authenticated)
345
354
  const now = Date.now()
355
+ const certificatesRequired =
356
+ this.certificatesToRequest.certifiers.length > 0
357
+
346
358
  this.sessionManager.addSession({
347
359
  isAuthenticated: false,
348
360
  sessionNonce,
349
361
  peerIdentityKey: identityKey,
350
- lastUpdate: now
362
+ lastUpdate: now,
363
+ certificatesRequired,
364
+ certificatesValidated: !certificatesRequired
351
365
  })
352
366
 
353
367
  const initialRequest: AuthMessage = {
@@ -448,28 +462,33 @@ export class Peer {
448
462
  )
449
463
  }
450
464
 
451
- switch (message.messageType) {
452
- case 'initialRequest':
453
- await this.processInitialRequest(message)
454
- break
455
- case 'initialResponse':
456
- await this.processInitialResponse(message)
457
- break
458
- case 'certificateRequest':
459
- await this.processCertificateRequest(message)
460
- break
461
- case 'certificateResponse':
462
- await this.processCertificateResponse(message)
463
- break
464
- case 'general':
465
- await this.processGeneralMessage(message)
466
- break
467
- default:
468
- throw new Error(
469
- `Unknown message type of ${String(message.messageType)} from ${String(
470
- message.identityKey
471
- )}`
472
- )
465
+ try {
466
+ switch (message.messageType) {
467
+ case 'initialRequest':
468
+ await this.processInitialRequest(message)
469
+ break
470
+ case 'initialResponse':
471
+ await this.processInitialResponse(message)
472
+ break
473
+ case 'certificateRequest':
474
+ await this.processCertificateRequest(message)
475
+ break
476
+ case 'certificateResponse':
477
+ await this.processCertificateResponse(message)
478
+ break
479
+ case 'general':
480
+ await this.processGeneralMessage(message)
481
+ break
482
+ default:
483
+ throw new Error(
484
+ `Unknown message type of ${String(message.messageType)} from ${String(
485
+ message.identityKey
486
+ )}`
487
+ )
488
+ }
489
+ } catch (err) {
490
+ // Swallow protocol violations so transport does not crash the process
491
+ // (Message is intentionally rejected)
473
492
  }
474
493
  }
475
494
 
@@ -487,33 +506,35 @@ export class Peer {
487
506
  throw new Error('Missing required fields in initialRequest message.')
488
507
  }
489
508
 
490
- // Create a new sessionNonce for our side
491
509
  const sessionNonce = await createNonce(this.wallet, undefined, this.originator)
492
510
  const now = Date.now()
493
511
 
494
- // We'll treat this as fully authenticated from *our* perspective (the responding side).
512
+ const certificatesRequired =
513
+ Array.isArray(this.certificatesToRequest?.certifiers) &&
514
+ this.certificatesToRequest.certifiers.length > 0
515
+
495
516
  this.sessionManager.addSession({
496
517
  isAuthenticated: true,
497
518
  sessionNonce,
498
519
  peerNonce: message.initialNonce,
499
520
  peerIdentityKey: message.identityKey,
500
- lastUpdate: now
521
+ lastUpdate: now,
522
+ certificatesRequired,
523
+ certificatesValidated: !certificatesRequired
501
524
  })
502
525
 
503
- // Possibly handle the peer's requested certs
504
526
  let certificatesToInclude: VerifiableCertificate[] | undefined
527
+
528
+ // Handle THEIR certificate request (if any)
505
529
  if (
506
- (message.requestedCertificates != null) &&
507
- Array.isArray(message.requestedCertificates.certifiers) &&
530
+ Array.isArray(message.requestedCertificates?.certifiers) &&
508
531
  message.requestedCertificates.certifiers.length > 0
509
532
  ) {
510
533
  if (this.onCertificateRequestReceivedCallbacks.size > 0) {
511
- // Let the application handle it
512
534
  this.onCertificateRequestReceivedCallbacks.forEach(cb => {
513
535
  cb(message.identityKey, message.requestedCertificates as RequestedCertificateSet)
514
536
  })
515
537
  } else {
516
- // Attempt to find automatically
517
538
  certificatesToInclude = await getVerifiableCertificates(
518
539
  this.wallet,
519
540
  message.requestedCertificates,
@@ -523,7 +544,6 @@ export class Peer {
523
544
  }
524
545
  }
525
546
 
526
- // Create signature
527
547
  const { signature } = await this.wallet.createSignature({
528
548
  data: Peer.base64ToBytes(message.initialNonce + sessionNonce),
529
549
  protocolID: [2, 'auth message signature'],
@@ -542,12 +562,10 @@ export class Peer {
542
562
  signature
543
563
  }
544
564
 
545
- // If we haven't interacted with a peer yet, store this identity as "lastInteracted"
546
565
  if (this.lastInteractedWithPeer === undefined) {
547
566
  this.lastInteractedWithPeer = message.identityKey
548
567
  }
549
568
 
550
- // Send the response
551
569
  await this.transport.send(initialResponseMessage)
552
570
  }
553
571
 
@@ -559,23 +577,27 @@ export class Peer {
559
577
  * @throws Will throw an error if nonce or signature verification fails.
560
578
  */
561
579
  private async processInitialResponse (message: AuthMessage): Promise<void> {
562
- const validNonce = await verifyNonce(message.yourNonce as string, this.wallet, undefined, this.originator)
580
+ const validNonce = await verifyNonce(
581
+ message.yourNonce as string,
582
+ this.wallet,
583
+ undefined,
584
+ this.originator
585
+ )
563
586
  if (!validNonce) {
564
587
  throw new Error(
565
588
  `Initial response nonce verification failed from peer: ${message.identityKey}`
566
589
  )
567
590
  }
568
591
 
569
- // This is the session we previously created by calling initiateHandshake
570
592
  const peerSession = this.sessionManager.getSession(message.yourNonce as string)
571
593
  if (peerSession == null) {
572
594
  throw new Error(`Peer session not found for peer: ${message.identityKey}`)
573
595
  }
574
596
 
575
- // Validate message signature
576
597
  const dataToVerify = Peer.base64ToBytes(
577
598
  (peerSession.sessionNonce ?? '') + (message.initialNonce ?? '')
578
599
  )
600
+
579
601
  const { valid } = await this.wallet.verifySignature({
580
602
  data: dataToVerify,
581
603
  signature: message.signature as number[],
@@ -583,55 +605,74 @@ export class Peer {
583
605
  keyID: `${peerSession.sessionNonce ?? ''} ${message.initialNonce ?? ''}`,
584
606
  counterparty: message.identityKey
585
607
  }, this.originator)
608
+
586
609
  if (!valid) {
587
610
  throw new Error(
588
611
  `Unable to verify initial response signature for peer: ${message.identityKey}`
589
612
  )
590
613
  }
591
614
 
592
- // Now mark the session as authenticated
615
+ // --- Transport authentication complete ---
593
616
  peerSession.peerNonce = message.initialNonce
594
617
  peerSession.peerIdentityKey = message.identityKey
595
618
  peerSession.isAuthenticated = true
619
+
620
+ peerSession.certificatesRequired =
621
+ Array.isArray(this.certificatesToRequest?.certifiers) &&
622
+ this.certificatesToRequest.certifiers.length > 0
623
+
624
+ // IMPORTANT: validation defaults to false if certs are required
625
+ peerSession.certificatesValidated = !peerSession.certificatesRequired
626
+
596
627
  peerSession.lastUpdate = Date.now()
597
628
  this.sessionManager.updateSession(peerSession)
598
629
 
599
- // If the handshake had requested certificates, validate them
630
+ // --- Validate certificates if provided ---
600
631
  if (
601
- this.certificatesToRequest?.certifiers?.length > 0 &&
602
- message.certificates?.length as number > 0
632
+ peerSession.certificatesRequired &&
633
+ Array.isArray(message.certificates) &&
634
+ message.certificates.length > 0
603
635
  ) {
604
- await validateCertificates(this.wallet, message, this.certificatesToRequest, this.originator)
636
+ await validateCertificates(
637
+ this.wallet,
638
+ message,
639
+ this.certificatesToRequest,
640
+ this.originator
641
+ )
642
+
643
+ peerSession.certificatesValidated = true
644
+ peerSession.lastUpdate = Date.now()
645
+ this.sessionManager.updateSession(peerSession)
605
646
 
606
- // Notify listeners
607
647
  this.onCertificatesReceivedCallbacks.forEach(cb =>
608
648
  cb(message.identityKey, message.certificates as VerifiableCertificate[])
609
649
  )
610
650
  }
611
651
 
612
- // Update lastInteractedWithPeer
652
+ // Update last-interacted peer
613
653
  this.lastInteractedWithPeer = message.identityKey
614
654
 
615
- // Let the handshake wait-latch know we got our response
655
+ // Release handshake waiters (even if certs still pending)
616
656
  this.onInitialResponseReceivedCallbacks.forEach(entry => {
617
657
  if (entry.sessionNonce === peerSession.sessionNonce) {
618
658
  entry.callback(peerSession.sessionNonce)
619
659
  }
620
660
  })
621
661
 
622
- // The peer might also request certificates from us
662
+ // --- Peer may request certificates from us ---
623
663
  if (
624
- (message.requestedCertificates != null) &&
664
+ message.requestedCertificates != null &&
625
665
  Array.isArray(message.requestedCertificates.certifiers) &&
626
666
  message.requestedCertificates.certifiers.length > 0
627
667
  ) {
628
668
  if (this.onCertificateRequestReceivedCallbacks.size > 0) {
629
- // Let the application handle it
630
669
  this.onCertificateRequestReceivedCallbacks.forEach(cb => {
631
- cb(message.identityKey, message.requestedCertificates as RequestedCertificateSet)
670
+ cb(
671
+ message.identityKey,
672
+ message.requestedCertificates as RequestedCertificateSet
673
+ )
632
674
  })
633
675
  } else {
634
- // Attempt auto
635
676
  const verifiableCertificates = await getVerifiableCertificates(
636
677
  this.wallet,
637
678
  message.requestedCertificates,
@@ -789,13 +830,14 @@ export class Peer {
789
830
  this.originator
790
831
  )
791
832
 
833
+ peerSession.certificatesValidated = true
834
+ peerSession.lastUpdate = Date.now()
835
+ this.sessionManager.updateSession(peerSession)
836
+
792
837
  // Notify any listeners
793
838
  this.onCertificatesReceivedCallbacks.forEach(cb => {
794
839
  cb(message.identityKey, message.certificates ?? [])
795
840
  })
796
-
797
- peerSession.lastUpdate = Date.now()
798
- this.sessionManager.updateSession(peerSession)
799
841
  }
800
842
 
801
843
  /**
@@ -806,7 +848,13 @@ export class Peer {
806
848
  * @throws Will throw an error if nonce or signature verification fails.
807
849
  */
808
850
  private async processGeneralMessage (message: AuthMessage): Promise<void> {
809
- const validNonce = await verifyNonce(message.yourNonce as string, this.wallet, undefined, this.originator)
851
+ const validNonce = await verifyNonce(
852
+ message.yourNonce as string,
853
+ this.wallet,
854
+ undefined,
855
+ this.originator
856
+ )
857
+
810
858
  if (!validNonce) {
811
859
  throw new Error(
812
860
  `Unable to verify nonce for general message from: ${message.identityKey}`
@@ -818,6 +866,17 @@ export class Peer {
818
866
  throw new Error(`Session not found for nonce: ${message.yourNonce as string}`)
819
867
  }
820
868
 
869
+ const certificatesRequired = peerSession.certificatesRequired === true
870
+ const certificatesValidated = peerSession.certificatesValidated === true
871
+
872
+ if (certificatesRequired && !certificatesValidated) {
873
+ throw new Error(
874
+ `Received general message before certificate validation from peer ${
875
+ peerSession.peerIdentityKey ?? 'unknown'
876
+ }`
877
+ )
878
+ }
879
+
821
880
  const { valid } = await this.wallet.verifySignature({
822
881
  data: message.payload,
823
882
  signature: message.signature as number[],
@@ -825,6 +884,7 @@ export class Peer {
825
884
  keyID: `${message.nonce ?? ''} ${peerSession.sessionNonce ?? ''}`,
826
885
  counterparty: peerSession.peerIdentityKey
827
886
  }, this.originator)
887
+
828
888
  if (!valid) {
829
889
  throw new Error(
830
890
  `Invalid signature in generalMessage from ${peerSession.peerIdentityKey as string}`
@@ -848,10 +908,12 @@ export class Peer {
848
908
  if (this.identityPublicKey != null) {
849
909
  return this.identityPublicKey
850
910
  }
911
+
851
912
  const { publicKey } = await this.wallet.getPublicKey(
852
913
  { identityKey: true },
853
914
  this.originator
854
915
  )
916
+
855
917
  this.identityPublicKey = publicKey
856
918
  return publicKey
857
919
  }
@@ -860,9 +922,11 @@ export class Peer {
860
922
  if (BufferCtor != null) {
861
923
  return Array.from(BufferCtor.from(data, 'utf8'))
862
924
  }
925
+
863
926
  if (typeof TextEncoder !== 'undefined') {
864
927
  return Array.from(new TextEncoder().encode(data))
865
928
  }
929
+
866
930
  return Utils.toArray(data, 'utf8')
867
931
  }
868
932
 
@@ -870,6 +934,7 @@ export class Peer {
870
934
  if (BufferCtor != null) {
871
935
  return Array.from(BufferCtor.from(data, 'base64'))
872
936
  }
937
+
873
938
  return Utils.toArray(data, 'base64')
874
939
  }
875
940
  }