@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.
- package/dist/cjs/certificate/CertificateAuthority.d.ts +30 -9
- package/dist/cjs/certificate/CertificateAuthority.d.ts.map +1 -1
- package/dist/cjs/certificate/CertificateAuthority.js +27 -14
- package/dist/cjs/certificate/CertificateAuthority.js.map +1 -1
- package/dist/cjs/fabric/Fabric.d.ts +6 -0
- package/dist/cjs/fabric/Fabric.d.ts.map +1 -1
- package/dist/cjs/fabric/Fabric.js +13 -0
- package/dist/cjs/fabric/Fabric.js.map +1 -1
- package/dist/cjs/peer/PeerSet.js +1 -1
- package/dist/cjs/session/SessionManager.d.ts +10 -3
- package/dist/cjs/session/SessionManager.d.ts.map +1 -1
- package/dist/cjs/session/SessionManager.js +47 -11
- package/dist/cjs/session/SessionManager.js.map +2 -2
- package/dist/esm/certificate/CertificateAuthority.d.ts +30 -9
- package/dist/esm/certificate/CertificateAuthority.d.ts.map +1 -1
- package/dist/esm/certificate/CertificateAuthority.js +27 -14
- package/dist/esm/certificate/CertificateAuthority.js.map +1 -1
- package/dist/esm/fabric/Fabric.d.ts +6 -0
- package/dist/esm/fabric/Fabric.d.ts.map +1 -1
- package/dist/esm/fabric/Fabric.js +13 -0
- package/dist/esm/fabric/Fabric.js.map +1 -1
- package/dist/esm/peer/PeerSet.js +1 -1
- package/dist/esm/session/SessionManager.d.ts +10 -3
- package/dist/esm/session/SessionManager.d.ts.map +1 -1
- package/dist/esm/session/SessionManager.js +48 -11
- package/dist/esm/session/SessionManager.js.map +2 -2
- package/package.json +6 -6
- package/src/certificate/CertificateAuthority.ts +63 -25
- package/src/fabric/Fabric.ts +14 -0
- package/src/peer/PeerSet.ts +1 -1
- 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
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/fabric/Fabric.ts
CHANGED
|
@@ -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
|
*
|
package/src/peer/PeerSet.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
|
585
|
-
fabricIndex
|
|
586
|
-
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.#
|
|
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
|
-
|
|
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),
|