@matter/protocol 0.16.0-alpha.0-20260104-558783063 → 0.16.0-alpha.0-20260105-ba8d57296

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 (31) hide show
  1. package/dist/cjs/certificate/CertificateAuthority.d.ts +30 -9
  2. package/dist/cjs/certificate/CertificateAuthority.d.ts.map +1 -1
  3. package/dist/cjs/certificate/CertificateAuthority.js +27 -14
  4. package/dist/cjs/certificate/CertificateAuthority.js.map +1 -1
  5. package/dist/cjs/fabric/Fabric.d.ts +6 -0
  6. package/dist/cjs/fabric/Fabric.d.ts.map +1 -1
  7. package/dist/cjs/fabric/Fabric.js +13 -0
  8. package/dist/cjs/fabric/Fabric.js.map +1 -1
  9. package/dist/cjs/peer/PeerSet.js +1 -1
  10. package/dist/cjs/session/SessionManager.d.ts +10 -3
  11. package/dist/cjs/session/SessionManager.d.ts.map +1 -1
  12. package/dist/cjs/session/SessionManager.js +47 -11
  13. package/dist/cjs/session/SessionManager.js.map +2 -2
  14. package/dist/esm/certificate/CertificateAuthority.d.ts +30 -9
  15. package/dist/esm/certificate/CertificateAuthority.d.ts.map +1 -1
  16. package/dist/esm/certificate/CertificateAuthority.js +27 -14
  17. package/dist/esm/certificate/CertificateAuthority.js.map +1 -1
  18. package/dist/esm/fabric/Fabric.d.ts +6 -0
  19. package/dist/esm/fabric/Fabric.d.ts.map +1 -1
  20. package/dist/esm/fabric/Fabric.js +13 -0
  21. package/dist/esm/fabric/Fabric.js.map +1 -1
  22. package/dist/esm/peer/PeerSet.js +1 -1
  23. package/dist/esm/session/SessionManager.d.ts +10 -3
  24. package/dist/esm/session/SessionManager.d.ts.map +1 -1
  25. package/dist/esm/session/SessionManager.js +48 -11
  26. package/dist/esm/session/SessionManager.js.map +2 -2
  27. package/package.json +6 -6
  28. package/src/certificate/CertificateAuthority.ts +63 -25
  29. package/src/fabric/Fabric.ts +14 -0
  30. package/src/peer/PeerSet.ts +1 -1
  31. package/src/session/SessionManager.ts +65 -17
@@ -35,9 +35,6 @@ const logger = Logger.get("CertificateAuthority");
35
35
  * Supports optional Intermediate Certificate Authority (ICAC) for 3-tier PKI hierarchy.
36
36
  * When ICAC is enabled, the certificate chain becomes: RCAC -> ICAC -> NOC instead of RCAC -> NOC.
37
37
  *
38
- * Configuration:
39
- * - intermediateCert: Enable/disable ICAC generation. Defaults to false (2-tier PKI).
40
- *
41
38
  * Behavior:
42
39
  * - When ICAC exists, it is always used to sign NOCs (operational certificates)
43
40
  * - When no ICAC exists, the root certificate signs NOCs directly
@@ -113,7 +110,7 @@ export class CertificateAuthority {
113
110
  const certValues = options instanceof StorageContext ? await options.values() : (options ?? {});
114
111
 
115
112
  // When generateIntermediateCert is set, we ensure it, or if a valid ICAC is stored then we require it
116
- // else we check whats in the storage and default to false
113
+ // else we check what's in the storage and default to false
117
114
  const requireIcac = generateIntermediateCert ?? this.#isValidStoredIcacCertificate(certValues);
118
115
 
119
116
  if (this.#isValidStoredRootCertificate(certValues)) {
@@ -166,18 +163,20 @@ export class CertificateAuthority {
166
163
  get config(): CertificateAuthority.Configuration {
167
164
  return {
168
165
  rootCertId: this.#rootCertId,
169
- rootKeyPair: this.construction.assert("root key pair", this.#rootKeyPair).keyPair,
170
166
  rootKeyIdentifier: this.construction.assert("root key identifier", this.#rootKeyIdentifier),
171
167
  rootCertBytes: this.construction.assert("root cert bytes", this.#rootCertBytes),
172
168
  nextCertificateId: this.#nextCertificateId,
173
169
  ...(this.#icacProps !== undefined
174
170
  ? {
171
+ rootKeyPair: this.#rootKeyPair?.keyPair, // rootKeyPair is optional when using ICAC
175
172
  icacCertId: this.#icacProps.certId,
176
173
  icacKeyPair: this.construction.assert("icac key pair", this.#icacProps.keyPair).keyPair,
177
174
  icacKeyIdentifier: this.construction.assert("icac key identifier", this.#icacProps.keyIdentifier),
178
175
  icacCertBytes: this.construction.assert("icac cert bytes", this.#icacProps.certBytes),
179
176
  }
180
- : {}),
177
+ : {
178
+ rootKeyPair: this.construction.assert("root key pair", this.#rootKeyPair).keyPair,
179
+ }),
181
180
  };
182
181
  }
183
182
 
@@ -295,7 +294,9 @@ export class CertificateAuthority {
295
294
  #isValidStoredRootCertificate(certValues: Record<string, unknown>): boolean {
296
295
  return (
297
296
  (typeof certValues.rootCertId === "number" || typeof certValues.rootCertId === "bigint") &&
298
- (Bytes.isBytes(certValues.rootKeyPair) || typeof certValues.rootKeyPair === "object") &&
297
+ (certValues.rootKeyPair === undefined ||
298
+ Bytes.isBytes(certValues.rootKeyPair) ||
299
+ typeof certValues.rootKeyPair === "object") &&
299
300
  Bytes.isBytes(certValues.rootKeyIdentifier) &&
300
301
  Bytes.isBytes(certValues.rootCertBytes) &&
301
302
  (typeof certValues.nextCertificateId === "number" || typeof certValues.nextCertificateId === "bigint")
@@ -313,7 +314,10 @@ export class CertificateAuthority {
313
314
 
314
315
  #loadFromStorage(certValues: Record<string, unknown>, requireIcac?: boolean): void {
315
316
  this.#rootCertId = BigInt(certValues.rootCertId as bigint | number);
316
- this.#rootKeyPair = PrivateKey(certValues.rootKeyPair as BinaryKeyPair);
317
+ if (certValues.rootKeyPair !== undefined) {
318
+ // rootKeyPair is optional when using ICAC (3-tier PKI without RCAC private key)
319
+ this.#rootKeyPair = PrivateKey(certValues.rootKeyPair as BinaryKeyPair);
320
+ }
317
321
  this.#rootKeyIdentifier = certValues.rootKeyIdentifier as Bytes;
318
322
  this.#rootCertBytes = certValues.rootCertBytes as Bytes;
319
323
  this.#nextCertificateId = BigInt(certValues.nextCertificateId as bigint | number);
@@ -332,26 +336,34 @@ export class CertificateAuthority {
332
336
  keyIdentifier: certValues.icacKeyIdentifier as Bytes,
333
337
  certBytes: certValues.icacCertBytes as Bytes,
334
338
  };
339
+ } else {
340
+ // Validate: when no ICAC, rootKeyPair is required for signing NOCs
341
+ if (this.#rootKeyPair === undefined) {
342
+ throw new ImplementationError(
343
+ "rootKeyPair is required when not using ICAC (2-tier PKI requires RCAC private key to sign NOCs)",
344
+ );
345
+ }
335
346
  }
336
347
  }
337
348
 
338
349
  #buildStorageData(): CertificateAuthority.Configuration {
339
- const data: CertificateAuthority.Configuration = {
350
+ return {
340
351
  rootCertId: this.#rootCertId,
341
- rootKeyPair: this.#initializedRootKeyPair.keyPair,
342
352
  rootKeyIdentifier: this.#initializedRootKeyIdentifier,
343
353
  rootCertBytes: this.#initializedRootCertBytes,
344
354
  nextCertificateId: this.#nextCertificateId,
355
+ ...(this.#icacProps
356
+ ? {
357
+ rootKeyPair: this.#rootKeyPair?.keyPair, // rootKeyPair is optional when using ICAC
358
+ icacCertId: this.#icacProps.certId,
359
+ icacKeyPair: this.#icacProps.keyPair.keyPair,
360
+ icacKeyIdentifier: this.#icacProps.keyIdentifier,
361
+ icacCertBytes: this.#icacProps.certBytes,
362
+ }
363
+ : {
364
+ rootKeyPair: this.#initializedRootKeyPair.keyPair,
365
+ }),
345
366
  };
346
-
347
- if (this.#icacProps) {
348
- data.icacCertId = this.#icacProps.certId;
349
- data.icacKeyPair = this.#icacProps.keyPair.keyPair;
350
- data.icacKeyIdentifier = this.#icacProps.keyIdentifier;
351
- data.icacCertBytes = this.#icacProps.certBytes;
352
- }
353
-
354
- return data;
355
367
  }
356
368
 
357
369
  #getSigningParameters(): {
@@ -390,15 +402,41 @@ interface IcacProps {
390
402
  }
391
403
 
392
404
  export namespace CertificateAuthority {
393
- export type Configuration = {
405
+ /** Base configuration fields shared by both 2-tier and 3-tier PKI */
406
+ type ConfigurationBase = {
394
407
  rootCertId: bigint;
395
- rootKeyPair: BinaryKeyPair;
396
408
  rootKeyIdentifier: Bytes;
397
409
  rootCertBytes: Bytes;
398
410
  nextCertificateId: bigint;
399
- icacCertId?: bigint;
400
- icacKeyPair?: BinaryKeyPair;
401
- icacKeyIdentifier?: Bytes;
402
- icacCertBytes?: Bytes;
403
411
  };
412
+
413
+ /**
414
+ * Configuration for 2-tier PKI (RCAC -> NOC).
415
+ * rootKeyPair is REQUIRED since RCAC signs NOCs directly.
416
+ */
417
+ export type ConfigurationWithoutIcac = ConfigurationBase & {
418
+ rootKeyPair: BinaryKeyPair;
419
+ };
420
+
421
+ /**
422
+ * Configuration for 3-tier PKI (RCAC -> ICAC -> NOC).
423
+ * rootKeyPair is OPTIONAL since ICAC signs NOCs, not RCAC.
424
+ * This allows controllers to operate without access to the RCAC private key.
425
+ */
426
+ export type ConfigurationWithIcac = ConfigurationBase & {
427
+ rootKeyPair?: BinaryKeyPair;
428
+ icacCertId: bigint;
429
+ icacKeyPair: BinaryKeyPair;
430
+ icacKeyIdentifier: Bytes;
431
+ icacCertBytes: Bytes;
432
+ };
433
+
434
+ /**
435
+ * Configuration for CertificateAuthority with external certificates.
436
+ *
437
+ * When using ICAC (3-tier PKI), the rootKeyPair can be omitted since NOCs are signed
438
+ * by the ICAC, not the RCAC. This allows controllers to operate without access to
439
+ * the RCAC private key.
440
+ */
441
+ export type Configuration = ConfigurationWithoutIcac | ConfigurationWithIcac;
404
442
  }
@@ -377,6 +377,20 @@ export class Fabric {
377
377
  this.#persistCallback = callback;
378
378
  }
379
379
 
380
+ /**
381
+ * Handles actions when a fabric got replaced.
382
+ *
383
+ * It flushes subscriptions to ensure fabric updates are reported and closes sessions.
384
+ */
385
+ async replaced(currentExchange?: MessageExchange) {
386
+ for (const session of [...this.#sessions]) {
387
+ await session.initiateClose(async () => {
388
+ await session.closeSubscriptions(true);
389
+ });
390
+ await session.initiateForceClose(currentExchange);
391
+ }
392
+ }
393
+
380
394
  /**
381
395
  * Gracefully exit the fabric.
382
396
  *
@@ -704,7 +704,7 @@ export class PeerSet implements ImmutableSet<Peer>, ObservableSet<Peer> {
704
704
 
705
705
  const unsecuredSession = this.#sessions.createUnsecuredSession({
706
706
  channel: operationalChannel,
707
- // Use the session parameters from MDNS announcements when available and rest is assumed to be fall back
707
+ // Use the session parameters from MDNS announcements when available and rest is assumed to be fall-backs
708
708
  sessionParameters: mergedSessionParameters,
709
709
  isInitiator: true,
710
710
  });
@@ -17,6 +17,7 @@ import {
17
17
  Duration,
18
18
  Environment,
19
19
  Environmental,
20
+ InternalError,
20
21
  Lifecycle,
21
22
  Logger,
22
23
  MatterAggregateError,
@@ -46,15 +47,22 @@ import { UnsecuredSession } from "./UnsecuredSession.js";
46
47
 
47
48
  const logger = Logger.get("SessionManager");
48
49
 
49
- export interface ResumptionRecord {
50
+ /** Resumption record without a fabric reference but relevant lookup data used internally in SessionManager */
51
+ interface InternalResumptionRecord {
50
52
  sharedSecret: Bytes;
51
53
  resumptionId: Bytes;
52
- fabric: Fabric;
54
+ fabricId: FabricId;
55
+ fabricIndex: FabricIndex;
53
56
  peerNodeId: NodeId;
54
57
  sessionParameters: SessionParameters;
55
58
  caseAuthenticatedTags?: CaseAuthenticatedTag[];
56
59
  }
57
60
 
61
+ /** Resumption record with Fabric reference. */
62
+ export interface ResumptionRecord extends Omit<InternalResumptionRecord, "fabricId" | "fabricIndex"> {
63
+ fabric: Fabric;
64
+ }
65
+
58
66
  type ResumptionStorageRecord = {
59
67
  nodeId: NodeId;
60
68
  sharedSecret: Bytes;
@@ -119,7 +127,7 @@ export class SessionManager {
119
127
  readonly #sessions = new BasicSet<NodeSession>();
120
128
  readonly #groupSessions = new Map<NodeId, BasicSet<GroupSession>>();
121
129
  #nextSessionId: number;
122
- #resumptionRecords = new PeerAddressMap<ResumptionRecord>();
130
+ #resumptionRecords = new PeerAddressMap<InternalResumptionRecord>();
123
131
  readonly #globalUnencryptedMessageCounter;
124
132
  #sessionParameters: SessionParameters;
125
133
  readonly #construction: Construction<SessionManager>;
@@ -553,19 +561,38 @@ export class SessionManager {
553
561
  }
554
562
  }
555
563
 
564
+ #asExposedResumptionRecord(record: InternalResumptionRecord): ResumptionRecord {
565
+ return { ...record, fabric: this.#fabricForId(record.fabricId, record.fabricIndex) };
566
+ }
567
+
556
568
  findResumptionRecordById(resumptionId: Bytes) {
557
569
  this.#construction.assert();
558
- return [...this.#resumptionRecords.values()].find(record => Bytes.areEqual(record.resumptionId, resumptionId));
570
+ const record = [...this.#resumptionRecords.values()].find(record =>
571
+ Bytes.areEqual(record.resumptionId, resumptionId),
572
+ );
573
+ if (record !== undefined) {
574
+ return this.#asExposedResumptionRecord(record);
575
+ }
559
576
  }
560
577
 
561
578
  findResumptionRecordByAddress(address: PeerAddress) {
562
579
  this.#construction.assert();
563
- return this.#resumptionRecords.get(address);
580
+ const record = this.#resumptionRecords.get(address);
581
+ if (record !== undefined) {
582
+ return this.#asExposedResumptionRecord(record);
583
+ }
564
584
  }
565
585
 
566
586
  async saveResumptionRecord(resumptionRecord: ResumptionRecord) {
567
587
  await this.#construction;
568
- this.#resumptionRecords.set(resumptionRecord.fabric.addressOf(resumptionRecord.peerNodeId), resumptionRecord);
588
+ const { fabric, ...rest } = resumptionRecord;
589
+
590
+ const record = {
591
+ ...rest,
592
+ fabricId: fabric.fabricId,
593
+ fabricIndex: fabric.fabricIndex,
594
+ };
595
+ this.#resumptionRecords.set(fabric.addressOf(resumptionRecord.peerNodeId), record);
569
596
  await this.#storeResumptionRecords();
570
597
  }
571
598
 
@@ -576,14 +603,22 @@ export class SessionManager {
576
603
  [...this.#resumptionRecords].map(
577
604
  ([
578
605
  address,
579
- { sharedSecret, resumptionId, peerNodeId, fabric, sessionParameters, caseAuthenticatedTags },
606
+ {
607
+ sharedSecret,
608
+ resumptionId,
609
+ peerNodeId,
610
+ fabricId,
611
+ fabricIndex,
612
+ sessionParameters,
613
+ caseAuthenticatedTags,
614
+ },
580
615
  ]): ResumptionStorageRecord => ({
581
616
  nodeId: address.nodeId,
582
617
  sharedSecret,
583
618
  resumptionId,
584
- fabricId: fabric.fabricId,
585
- fabricIndex: fabric.fabricIndex,
586
- peerNodeId: peerNodeId,
619
+ fabricId,
620
+ fabricIndex,
621
+ peerNodeId,
587
622
  sessionParameters: {
588
623
  ...sessionParameters,
589
624
  supportedTransports: sessionParameters.supportedTransports
@@ -596,6 +631,23 @@ export class SessionManager {
596
631
  );
597
632
  }
598
633
 
634
+ #maybeFabricForId(fabricId: FabricId, fabricIndex?: FabricIndex) {
635
+ return this.#context.fabrics.find(
636
+ fabric =>
637
+ fabric.fabricId === fabricId &&
638
+ // Backward compatibility logic: fabricIndex was added later (0.15.5), so it might be undefined in older records
639
+ (fabricIndex === undefined || fabric.fabricIndex === fabricIndex),
640
+ );
641
+ }
642
+
643
+ #fabricForId(fabricId: FabricId, fabricIndex?: FabricIndex) {
644
+ const fabric = this.#maybeFabricForId(fabricId, fabricIndex);
645
+ if (fabric === undefined) {
646
+ throw new InternalError(`Fabric not found for ID=${fabricId}, index=${fabricIndex}`);
647
+ }
648
+ return fabric;
649
+ }
650
+
599
651
  async #initialize() {
600
652
  await this.#context.fabrics.construction;
601
653
 
@@ -615,12 +667,7 @@ export class SessionManager {
615
667
  sessionParameters,
616
668
  caseAuthenticatedTags,
617
669
  }) => {
618
- const fabric = this.#context.fabrics.find(
619
- fabric =>
620
- fabric.fabricId === fabricId &&
621
- // Backward compatibility logic: fabricIndex was added later (0.15.5), so it might be undefined in older records
622
- (fabricIndex === undefined || fabric.fabricIndex === fabricIndex),
623
- );
670
+ const fabric = this.#maybeFabricForId(fabricId, fabricIndex);
624
671
  if (!fabric) {
625
672
  logger.warn(
626
673
  `Ignoring resumption record for fabric 0x${toHex(fabricId)} and index ${fabricIndex} because we cannot find a matching fabric`,
@@ -639,7 +686,8 @@ export class SessionManager {
639
686
  this.#resumptionRecords.set(fabric.addressOf(nodeId), {
640
687
  sharedSecret,
641
688
  resumptionId,
642
- fabric,
689
+ fabricId,
690
+ fabricIndex: fabric.fabricIndex,
643
691
  peerNodeId,
644
692
  // Make sure to initialize default values when restoring an older resumption record
645
693
  sessionParameters: SessionParameters(sessionParameters),