@matter/protocol 0.17.2 → 0.17.3-alpha.0-20260617-ea3690abc

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 (159) hide show
  1. package/dist/cjs/action/client/ClientInteraction.d.ts.map +1 -1
  2. package/dist/cjs/action/client/ClientInteraction.js +16 -2
  3. package/dist/cjs/action/client/ClientInteraction.js.map +1 -1
  4. package/dist/cjs/action/client/ClientRequest.d.ts +8 -1
  5. package/dist/cjs/action/client/ClientRequest.d.ts.map +1 -1
  6. package/dist/cjs/action/server/AttributeReadResponse.d.ts.map +1 -1
  7. package/dist/cjs/action/server/AttributeReadResponse.js +26 -12
  8. package/dist/cjs/action/server/AttributeReadResponse.js.map +1 -1
  9. package/dist/cjs/action/server/AttributeWriteResponse.d.ts.map +1 -1
  10. package/dist/cjs/action/server/AttributeWriteResponse.js +27 -10
  11. package/dist/cjs/action/server/AttributeWriteResponse.js.map +1 -1
  12. package/dist/cjs/action/server/CommandInvokeResponse.d.ts.map +1 -1
  13. package/dist/cjs/action/server/CommandInvokeResponse.js +27 -10
  14. package/dist/cjs/action/server/CommandInvokeResponse.js.map +1 -1
  15. package/dist/cjs/action/server/EventReadResponse.d.ts.map +1 -1
  16. package/dist/cjs/action/server/EventReadResponse.js +27 -10
  17. package/dist/cjs/action/server/EventReadResponse.js.map +1 -1
  18. package/dist/cjs/certificate/DeviceAttestationValidator.d.ts.map +1 -1
  19. package/dist/cjs/certificate/DeviceAttestationValidator.js +15 -12
  20. package/dist/cjs/certificate/DeviceAttestationValidator.js.map +1 -1
  21. package/dist/cjs/certificate/kinds/Certificate.d.ts +7 -0
  22. package/dist/cjs/certificate/kinds/Certificate.d.ts.map +1 -1
  23. package/dist/cjs/certificate/kinds/Certificate.js +30 -1
  24. package/dist/cjs/certificate/kinds/Certificate.js.map +1 -1
  25. package/dist/cjs/codec/MessagePrivacy.d.ts +1 -1
  26. package/dist/cjs/codec/MessagePrivacy.d.ts.map +1 -1
  27. package/dist/cjs/codec/MessagePrivacy.js +3 -2
  28. package/dist/cjs/codec/MessagePrivacy.js.map +1 -1
  29. package/dist/cjs/events/OccurrenceManager.d.ts +2 -2
  30. package/dist/cjs/events/OccurrenceManager.d.ts.map +1 -1
  31. package/dist/cjs/interaction/AttributeDataEncoder.d.ts.map +1 -1
  32. package/dist/cjs/interaction/AttributeDataEncoder.js.map +1 -1
  33. package/dist/cjs/peer/Peer.d.ts +5 -0
  34. package/dist/cjs/peer/Peer.d.ts.map +1 -1
  35. package/dist/cjs/peer/Peer.js +1 -0
  36. package/dist/cjs/peer/Peer.js.map +1 -1
  37. package/dist/cjs/peer/PeerConnection.d.ts +6 -1
  38. package/dist/cjs/peer/PeerConnection.d.ts.map +1 -1
  39. package/dist/cjs/peer/PeerConnection.js +23 -5
  40. package/dist/cjs/peer/PeerConnection.js.map +1 -1
  41. package/dist/cjs/peer/PeerExchangeProvider.d.ts.map +1 -1
  42. package/dist/cjs/peer/PeerExchangeProvider.js +3 -0
  43. package/dist/cjs/peer/PeerExchangeProvider.js.map +1 -1
  44. package/dist/cjs/peer/PeerTimingParameters.d.ts +9 -0
  45. package/dist/cjs/peer/PeerTimingParameters.d.ts.map +1 -1
  46. package/dist/cjs/peer/PeerTimingParameters.js +3 -1
  47. package/dist/cjs/peer/PeerTimingParameters.js.map +1 -1
  48. package/dist/cjs/protocol/DeviceCommissioner.d.ts.map +1 -1
  49. package/dist/cjs/protocol/DeviceCommissioner.js +1 -1
  50. package/dist/cjs/protocol/DeviceCommissioner.js.map +1 -1
  51. package/dist/cjs/protocol/ExchangeProvider.d.ts +6 -1
  52. package/dist/cjs/protocol/ExchangeProvider.d.ts.map +1 -1
  53. package/dist/cjs/protocol/ExchangeProvider.js +4 -2
  54. package/dist/cjs/protocol/ExchangeProvider.js.map +1 -1
  55. package/dist/cjs/protocol/MRP.d.ts +1 -1
  56. package/dist/cjs/protocol/MRP.d.ts.map +1 -1
  57. package/dist/cjs/protocol/MRP.js +1 -2
  58. package/dist/cjs/protocol/MRP.js.map +1 -1
  59. package/dist/cjs/protocol/MessageExchange.d.ts +14 -1
  60. package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
  61. package/dist/cjs/protocol/MessageExchange.js +31 -5
  62. package/dist/cjs/protocol/MessageExchange.js.map +1 -1
  63. package/dist/cjs/session/pase/PaseClient.d.ts.map +1 -1
  64. package/dist/cjs/session/pase/PaseClient.js +1 -1
  65. package/dist/cjs/session/pase/PaseClient.js.map +1 -1
  66. package/dist/cjs/session/pase/PaseServer.d.ts.map +1 -1
  67. package/dist/cjs/session/pase/PaseServer.js +11 -2
  68. package/dist/cjs/session/pase/PaseServer.js.map +1 -1
  69. package/dist/esm/action/client/ClientInteraction.d.ts.map +1 -1
  70. package/dist/esm/action/client/ClientInteraction.js +16 -2
  71. package/dist/esm/action/client/ClientInteraction.js.map +1 -1
  72. package/dist/esm/action/client/ClientRequest.d.ts +8 -1
  73. package/dist/esm/action/client/ClientRequest.d.ts.map +1 -1
  74. package/dist/esm/action/server/AttributeReadResponse.d.ts.map +1 -1
  75. package/dist/esm/action/server/AttributeReadResponse.js +27 -13
  76. package/dist/esm/action/server/AttributeReadResponse.js.map +1 -1
  77. package/dist/esm/action/server/AttributeWriteResponse.d.ts.map +1 -1
  78. package/dist/esm/action/server/AttributeWriteResponse.js +28 -11
  79. package/dist/esm/action/server/AttributeWriteResponse.js.map +1 -1
  80. package/dist/esm/action/server/CommandInvokeResponse.d.ts.map +1 -1
  81. package/dist/esm/action/server/CommandInvokeResponse.js +28 -11
  82. package/dist/esm/action/server/CommandInvokeResponse.js.map +1 -1
  83. package/dist/esm/action/server/EventReadResponse.d.ts.map +1 -1
  84. package/dist/esm/action/server/EventReadResponse.js +28 -11
  85. package/dist/esm/action/server/EventReadResponse.js.map +1 -1
  86. package/dist/esm/certificate/DeviceAttestationValidator.d.ts.map +1 -1
  87. package/dist/esm/certificate/DeviceAttestationValidator.js +16 -13
  88. package/dist/esm/certificate/DeviceAttestationValidator.js.map +1 -1
  89. package/dist/esm/certificate/kinds/Certificate.d.ts +7 -0
  90. package/dist/esm/certificate/kinds/Certificate.d.ts.map +1 -1
  91. package/dist/esm/certificate/kinds/Certificate.js +30 -1
  92. package/dist/esm/certificate/kinds/Certificate.js.map +1 -1
  93. package/dist/esm/codec/MessagePrivacy.d.ts +1 -1
  94. package/dist/esm/codec/MessagePrivacy.d.ts.map +1 -1
  95. package/dist/esm/codec/MessagePrivacy.js +4 -2
  96. package/dist/esm/codec/MessagePrivacy.js.map +1 -1
  97. package/dist/esm/events/OccurrenceManager.d.ts +2 -2
  98. package/dist/esm/events/OccurrenceManager.d.ts.map +1 -1
  99. package/dist/esm/events/OccurrenceManager.js.map +1 -1
  100. package/dist/esm/interaction/AttributeDataEncoder.d.ts.map +1 -1
  101. package/dist/esm/interaction/AttributeDataEncoder.js.map +1 -1
  102. package/dist/esm/peer/Peer.d.ts +5 -0
  103. package/dist/esm/peer/Peer.d.ts.map +1 -1
  104. package/dist/esm/peer/Peer.js +1 -0
  105. package/dist/esm/peer/Peer.js.map +1 -1
  106. package/dist/esm/peer/PeerConnection.d.ts +6 -1
  107. package/dist/esm/peer/PeerConnection.d.ts.map +1 -1
  108. package/dist/esm/peer/PeerConnection.js +23 -5
  109. package/dist/esm/peer/PeerConnection.js.map +1 -1
  110. package/dist/esm/peer/PeerExchangeProvider.d.ts.map +1 -1
  111. package/dist/esm/peer/PeerExchangeProvider.js +3 -0
  112. package/dist/esm/peer/PeerExchangeProvider.js.map +1 -1
  113. package/dist/esm/peer/PeerTimingParameters.d.ts +9 -0
  114. package/dist/esm/peer/PeerTimingParameters.d.ts.map +1 -1
  115. package/dist/esm/peer/PeerTimingParameters.js +4 -2
  116. package/dist/esm/peer/PeerTimingParameters.js.map +1 -1
  117. package/dist/esm/protocol/DeviceCommissioner.d.ts.map +1 -1
  118. package/dist/esm/protocol/DeviceCommissioner.js +2 -1
  119. package/dist/esm/protocol/DeviceCommissioner.js.map +1 -1
  120. package/dist/esm/protocol/ExchangeProvider.d.ts +6 -1
  121. package/dist/esm/protocol/ExchangeProvider.d.ts.map +1 -1
  122. package/dist/esm/protocol/ExchangeProvider.js +4 -2
  123. package/dist/esm/protocol/ExchangeProvider.js.map +1 -1
  124. package/dist/esm/protocol/MRP.d.ts +1 -1
  125. package/dist/esm/protocol/MRP.d.ts.map +1 -1
  126. package/dist/esm/protocol/MRP.js +1 -2
  127. package/dist/esm/protocol/MRP.js.map +1 -1
  128. package/dist/esm/protocol/MessageExchange.d.ts +14 -1
  129. package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
  130. package/dist/esm/protocol/MessageExchange.js +31 -5
  131. package/dist/esm/protocol/MessageExchange.js.map +1 -1
  132. package/dist/esm/session/pase/PaseClient.d.ts.map +1 -1
  133. package/dist/esm/session/pase/PaseClient.js +2 -2
  134. package/dist/esm/session/pase/PaseClient.js.map +1 -1
  135. package/dist/esm/session/pase/PaseServer.d.ts.map +1 -1
  136. package/dist/esm/session/pase/PaseServer.js +12 -2
  137. package/dist/esm/session/pase/PaseServer.js.map +1 -1
  138. package/package.json +5 -5
  139. package/src/action/client/ClientInteraction.ts +19 -1
  140. package/src/action/client/ClientRequest.ts +9 -1
  141. package/src/action/server/AttributeReadResponse.ts +39 -18
  142. package/src/action/server/AttributeWriteResponse.ts +35 -19
  143. package/src/action/server/CommandInvokeResponse.ts +36 -15
  144. package/src/action/server/EventReadResponse.ts +42 -16
  145. package/src/certificate/DeviceAttestationValidator.ts +17 -13
  146. package/src/certificate/kinds/Certificate.ts +43 -0
  147. package/src/codec/MessagePrivacy.ts +6 -4
  148. package/src/events/OccurrenceManager.ts +2 -2
  149. package/src/interaction/AttributeDataEncoder.ts +3 -3
  150. package/src/peer/Peer.ts +7 -0
  151. package/src/peer/PeerConnection.ts +33 -6
  152. package/src/peer/PeerExchangeProvider.ts +4 -0
  153. package/src/peer/PeerTimingParameters.ts +15 -2
  154. package/src/protocol/DeviceCommissioner.ts +2 -1
  155. package/src/protocol/ExchangeProvider.ts +12 -2
  156. package/src/protocol/MRP.ts +5 -6
  157. package/src/protocol/MessageExchange.ts +44 -6
  158. package/src/session/pase/PaseClient.ts +2 -6
  159. package/src/session/pase/PaseServer.ts +12 -2
@@ -11,7 +11,7 @@ import { InvokeResult } from "#action/response/InvokeResult.js";
11
11
  import { AccessControl, hasRemoteActor } from "#action/server/AccessControl.js";
12
12
  import { DataResponse, FallbackLimits } from "#action/server/DataResponse.js";
13
13
  import { Diagnostic, InternalError, Logger } from "@matter/general";
14
- import { CommandModel, DataModelPath, ElementTag, FabricIndex as FabricIndexField } from "@matter/model";
14
+ import { AccessLevel, CommandModel, DataModelPath, ElementTag, FabricIndex as FabricIndexField } from "@matter/model";
15
15
  import {
16
16
  CommandPath,
17
17
  EndpointNumber,
@@ -214,9 +214,10 @@ export class CommandInvokeResponse<
214
214
  limits = command.limits;
215
215
  }
216
216
 
217
- // Validate access. Order here prescribed by 1.4 core spec 8.4.3.2
217
+ // Order prescribed by core spec 8.8.3.2: an Operate-privilege pass gates element-existence disclosure,
218
+ // existence checks follow, then the actual-privilege pass gates the invoke.
218
219
  // We need some fallback location if cluster is not defined
219
- const location = {
220
+ const location: AccessControl.Location = {
220
221
  ...(cluster?.location ?? {
221
222
  path: DataModelPath.none,
222
223
  endpoint: endpointId,
@@ -225,20 +226,13 @@ export class CommandInvokeResponse<
225
226
  owningFabric: this.session.fabric,
226
227
  };
227
228
 
229
+ let access: { session: AccessControl.RemoteActorSession; location: AccessControl.Location } | undefined;
228
230
  if (hasRemoteActor(this.session)) {
229
- const permission = this.session.authorityAt(limits.writeLevel, location);
230
- switch (permission) {
231
- case AccessControl.Authority.Granted:
232
- break;
231
+ access = { session: this.session, location };
233
232
 
234
- case AccessControl.Authority.Unauthorized:
235
- return this.#addStatus(path, commandRef, Status.UnsupportedAccess);
236
-
237
- case AccessControl.Authority.Restricted:
238
- return this.#addStatus(path, commandRef, Status.AccessRestricted);
239
-
240
- default:
241
- throw new InternalError(`Unsupported authorization state ${permission}`);
233
+ const denial = this.#authorize(access.session, AccessLevel.Operate, location);
234
+ if (denial !== undefined) {
235
+ return this.#addStatus(path, commandRef, denial);
242
236
  }
243
237
  }
244
238
 
@@ -252,6 +246,13 @@ export class CommandInvokeResponse<
252
246
  return this.#addStatus(path, commandRef, Status.UnsupportedCommand);
253
247
  }
254
248
 
249
+ if (access !== undefined) {
250
+ const denial = this.#authorize(access.session, limits.writeLevel, access.location);
251
+ if (denial !== undefined) {
252
+ return this.#addStatus(path, commandRef, denial);
253
+ }
254
+ }
255
+
255
256
  if (hasRemoteActor(this.session)) {
256
257
  if (limits.largeMessage && !this.session.largeMessage) {
257
258
  this.#errorCount++;
@@ -369,6 +370,26 @@ export class CommandInvokeResponse<
369
370
  }
370
371
  }
371
372
 
373
+ /**
374
+ * Validate access at {@link level}. Returns undefined if granted; otherwise the status to report.
375
+ */
376
+ #authorize(session: AccessControl.RemoteActorSession, level: AccessLevel, location: AccessControl.Location) {
377
+ const permission = session.authorityAt(level, location);
378
+ switch (permission) {
379
+ case AccessControl.Authority.Granted:
380
+ return undefined;
381
+
382
+ case AccessControl.Authority.Unauthorized:
383
+ return Status.UnsupportedAccess;
384
+
385
+ case AccessControl.Authority.Restricted:
386
+ return Status.AccessRestricted;
387
+
388
+ default:
389
+ throw new InternalError(`Unsupported authorization state ${permission}`);
390
+ }
391
+ }
392
+
372
393
  #addResponse(chunk: InvokeResult.Data) {
373
394
  if (this.#chunk) {
374
395
  this.#chunk.push(chunk);
@@ -12,7 +12,7 @@ import { AccessControl, hasRemoteActor } from "#action/server/AccessControl.js";
12
12
  import { DataResponse, FallbackLimits } from "#action/server/DataResponse.js";
13
13
  import { NumberedOccurrence } from "#events/Occurrence.js";
14
14
  import { InternalError, isObject, Logger } from "@matter/general";
15
- import { DataModelPath, ElementTag, EventModel } from "@matter/model";
15
+ import { AccessLevel, DataModelPath, ElementTag, EventModel } from "@matter/model";
16
16
  import {
17
17
  EventNumber,
18
18
  EventPath,
@@ -185,10 +185,12 @@ export class EventReadResponse<
185
185
  limits = event.limits;
186
186
  }
187
187
 
188
- // Validate access. Order here prescribed by 1.4 core spec 8.4.3.2
189
- // We need some fallback location if cluster is not defined
188
+ // Order prescribed by core spec 8.4.3.2: a View-privilege pass gates element-existence disclosure,
189
+ // existence checks follow, then the actual-privilege pass gates the data.
190
+ let access: { session: AccessControl.RemoteActorSession; location: AccessControl.Location } | undefined;
190
191
  if (hasRemoteActor(this.session)) {
191
- const location = {
192
+ // We need some fallback location if cluster is not defined
193
+ const location: AccessControl.Location = {
192
194
  ...(cluster?.location ?? {
193
195
  path: DataModelPath.none,
194
196
  endpoint: endpointId,
@@ -196,19 +198,11 @@ export class EventReadResponse<
196
198
  }),
197
199
  owningFabric: this.session.fabric,
198
200
  };
199
- const permission = this.session.authorityAt(limits.readLevel, location);
200
- switch (permission) {
201
- case AccessControl.Authority.Granted:
202
- break;
203
-
204
- case AccessControl.Authority.Unauthorized:
205
- return this.#asStatus(path, Status.UnsupportedAccess);
206
-
207
- case AccessControl.Authority.Restricted:
208
- return this.#asStatus(path, Status.AccessRestricted);
201
+ access = { session: this.session, location };
209
202
 
210
- default:
211
- throw new InternalError(`Unsupported authorization state ${permission}`);
203
+ const denied = this.#authorize(access.session, path, AccessLevel.View, location);
204
+ if (denied !== undefined) {
205
+ return denied;
212
206
  }
213
207
  }
214
208
 
@@ -222,6 +216,13 @@ export class EventReadResponse<
222
216
  return this.#asStatus(path, Status.UnsupportedEvent);
223
217
  }
224
218
 
219
+ if (access !== undefined) {
220
+ const denied = this.#authorize(access.session, path, limits.readLevel, access.location);
221
+ if (denied !== undefined) {
222
+ return denied;
223
+ }
224
+ }
225
+
225
226
  if (this.#currentEndpoint !== endpoint) {
226
227
  this.#currentEndpoint = endpoint;
227
228
  this.#currentCluster = cluster;
@@ -354,6 +355,31 @@ export class EventReadResponse<
354
355
  }
355
356
  }
356
357
 
358
+ /**
359
+ * Validate access at {@link level}. Returns undefined if granted; otherwise a status report to return.
360
+ */
361
+ #authorize(
362
+ session: AccessControl.RemoteActorSession,
363
+ path: ReadResult.ConcreteEventPath,
364
+ level: AccessLevel,
365
+ location: AccessControl.Location,
366
+ ) {
367
+ const permission = session.authorityAt(level, location);
368
+ switch (permission) {
369
+ case AccessControl.Authority.Granted:
370
+ return undefined;
371
+
372
+ case AccessControl.Authority.Unauthorized:
373
+ return this.#asStatus(path, Status.UnsupportedAccess);
374
+
375
+ case AccessControl.Authority.Restricted:
376
+ return this.#asStatus(path, Status.AccessRestricted);
377
+
378
+ default:
379
+ throw new InternalError(`Unsupported authorization state ${permission}`);
380
+ }
381
+ }
382
+
357
383
  /**
358
384
  * Add a status value.
359
385
  */
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { Bytes, Crypto, EcdsaSignature, MaybePromise, PublicKey } from "@matter/general";
7
+ import { Bytes, Crypto, Diagnostic, EcdsaSignature, MaybePromise, PublicKey } from "@matter/general";
8
8
  import { MATTER_EPOCH_OFFSET_S, VendorId } from "@matter/types";
9
9
  import { TlvAttestation } from "../common/OperationalCredentialsTypes.js";
10
10
  import { DclCertificateService } from "../dcl/DclCertificateService.js";
@@ -59,6 +59,10 @@ export class DeviceAttestationError extends CommissioningError {
59
59
  }
60
60
  }
61
61
 
62
+ function idHex(id?: number) {
63
+ return id === undefined ? "undefined" : `0x${Diagnostic.hex(id, 4).toUpperCase()}`;
64
+ }
65
+
62
66
  export namespace DeviceAttestationValidator {
63
67
  export interface Context {
64
68
  crypto: Crypto;
@@ -131,7 +135,7 @@ export namespace DeviceAttestationValidator {
131
135
  if (dac.cert.subject.vendorId !== pai.cert.subject.vendorId) {
132
136
  throw new DeviceAttestationError(
133
137
  DeviceAttestationCheck.VendorIdMismatch,
134
- `DAC vendorId ${dac.cert.subject.vendorId} does not match PAI vendorId ${pai.cert.subject.vendorId}`,
138
+ `DAC vendorId ${idHex(dac.cert.subject.vendorId)} does not match PAI vendorId ${idHex(pai.cert.subject.vendorId)}`,
135
139
  );
136
140
  }
137
141
 
@@ -270,7 +274,7 @@ export namespace DeviceAttestationValidator {
270
274
  if (paaVendorId !== undefined && paaVendorId !== pai.cert.subject.vendorId) {
271
275
  throw new DeviceAttestationError(
272
276
  DeviceAttestationCheck.VendorIdMismatch,
273
- `PAA vendorId ${paaVendorId} does not match PAI vendorId ${pai.cert.subject.vendorId}`,
277
+ `PAA vendorId ${idHex(paaVendorId)} does not match PAI vendorId ${idHex(pai.cert.subject.vendorId)}`,
274
278
  );
275
279
  }
276
280
 
@@ -356,7 +360,7 @@ export namespace DeviceAttestationValidator {
356
360
  if (cdContent.vendorId !== data.vendorId) {
357
361
  throw new DeviceAttestationError(
358
362
  DeviceAttestationCheck.CertificationDeclarationFieldMismatch,
359
- `CD vendor_id ${cdContent.vendorId} does not match BasicInformation VendorID ${data.vendorId}`,
363
+ `CD vendor_id ${idHex(cdContent.vendorId)} does not match BasicInformation VendorID ${idHex(data.vendorId)}`,
360
364
  );
361
365
  }
362
366
 
@@ -364,7 +368,7 @@ export namespace DeviceAttestationValidator {
364
368
  if (!cdContent.produceIdArray.includes(data.productId)) {
365
369
  throw new DeviceAttestationError(
366
370
  DeviceAttestationCheck.CertificationDeclarationFieldMismatch,
367
- `CD product_id_array does not contain BasicInformation ProductID ${data.productId}`,
371
+ `CD product_id_array [${cdContent.produceIdArray.map(idHex).join(", ")}] does not contain BasicInformation ProductID ${idHex(data.productId)}`,
368
372
  );
369
373
  }
370
374
 
@@ -383,27 +387,27 @@ export namespace DeviceAttestationValidator {
383
387
  if (dac.cert.subject.vendorId !== cdContent.dacOriginVendorId) {
384
388
  throw new DeviceAttestationError(
385
389
  DeviceAttestationCheck.CertificationDeclarationFieldMismatch,
386
- "DAC vendorId does not match CD dac_origin_vendor_id",
390
+ `DAC vendorId ${idHex(dac.cert.subject.vendorId)} does not match CD dac_origin_vendor_id ${idHex(cdContent.dacOriginVendorId)}`,
387
391
  );
388
392
  }
389
393
  if (pai.cert.subject.vendorId !== cdContent.dacOriginVendorId) {
390
394
  throw new DeviceAttestationError(
391
395
  DeviceAttestationCheck.CertificationDeclarationFieldMismatch,
392
- "PAI vendorId does not match CD dac_origin_vendor_id",
396
+ `PAI vendorId ${idHex(pai.cert.subject.vendorId)} does not match CD dac_origin_vendor_id ${idHex(cdContent.dacOriginVendorId)}`,
393
397
  );
394
398
  }
395
399
  const dacProductId = dac.cert.subject.productId;
396
400
  if (dacProductId !== undefined && dacProductId !== cdContent.dacOriginProductId) {
397
401
  throw new DeviceAttestationError(
398
402
  DeviceAttestationCheck.CertificationDeclarationFieldMismatch,
399
- "DAC productId does not match CD dac_origin_product_id",
403
+ `DAC productId ${idHex(dacProductId)} does not match CD dac_origin_product_id ${idHex(cdContent.dacOriginProductId)}`,
400
404
  );
401
405
  }
402
406
  const paiProductId = pai.cert.subject.productId;
403
407
  if (paiProductId !== undefined && paiProductId !== cdContent.dacOriginProductId) {
404
408
  throw new DeviceAttestationError(
405
409
  DeviceAttestationCheck.CertificationDeclarationFieldMismatch,
406
- "PAI productId does not match CD dac_origin_product_id",
410
+ `PAI productId ${idHex(paiProductId)} does not match CD dac_origin_product_id ${idHex(cdContent.dacOriginProductId)}`,
407
411
  );
408
412
  }
409
413
  } else {
@@ -411,27 +415,27 @@ export namespace DeviceAttestationValidator {
411
415
  if (dac.cert.subject.vendorId !== cdContent.vendorId) {
412
416
  throw new DeviceAttestationError(
413
417
  DeviceAttestationCheck.CertificationDeclarationFieldMismatch,
414
- "DAC vendorId does not match CD vendor_id",
418
+ `DAC vendorId ${idHex(dac.cert.subject.vendorId)} does not match CD vendor_id ${idHex(cdContent.vendorId)}`,
415
419
  );
416
420
  }
417
421
  if (pai.cert.subject.vendorId !== cdContent.vendorId) {
418
422
  throw new DeviceAttestationError(
419
423
  DeviceAttestationCheck.CertificationDeclarationFieldMismatch,
420
- "PAI vendorId does not match CD vendor_id",
424
+ `PAI vendorId ${idHex(pai.cert.subject.vendorId)} does not match CD vendor_id ${idHex(cdContent.vendorId)}`,
421
425
  );
422
426
  }
423
427
  const dacProductId = dac.cert.subject.productId;
424
428
  if (dacProductId !== undefined && !cdContent.produceIdArray.includes(dacProductId)) {
425
429
  throw new DeviceAttestationError(
426
430
  DeviceAttestationCheck.CertificationDeclarationFieldMismatch,
427
- "DAC productId not found in CD product_id_array",
431
+ `DAC productId ${idHex(dacProductId)} not found in CD product_id_array [${cdContent.produceIdArray.map(idHex).join(", ")}]`,
428
432
  );
429
433
  }
430
434
  const paiProductId = pai.cert.subject.productId;
431
435
  if (paiProductId !== undefined && !cdContent.produceIdArray.includes(paiProductId)) {
432
436
  throw new DeviceAttestationError(
433
437
  DeviceAttestationCheck.CertificationDeclarationFieldMismatch,
434
- "PAI productId not found in CD product_id_array",
438
+ `PAI productId ${idHex(paiProductId)} not found in CD product_id_array [${cdContent.produceIdArray.map(idHex).join(", ")}]`,
435
439
  );
436
440
  }
437
441
  }
@@ -166,6 +166,23 @@ export abstract class Certificate<CT extends MatterCertificate> {
166
166
  }
167
167
  }
168
168
 
169
+ /**
170
+ * Extract a VendorID or ProductID encoded via the "fallback method" (Matter spec 6.2.2.2): the
171
+ * prefix `Mvid:`/`Mpid:` followed by 4 uppercase hexadecimal characters, anywhere within a
172
+ * `commonName`. Returns the leftmost correctly-encoded value, or `undefined` if the prefix is
173
+ * absent. Throws if the prefix appears but no correctly-encoded value exists (spec 6.2.2.2.1).
174
+ */
175
+ export function parseMatterFallbackVidPid(commonName: string, prefix: "Mvid:" | "Mpid:"): number | undefined {
176
+ const match = commonName.match(new RegExp(`${prefix}([0-9A-F]{4})`));
177
+ if (match !== null) {
178
+ return parseInt(match[1], 16);
179
+ }
180
+ if (commonName.includes(prefix)) {
181
+ throw new CertificateError(`commonName contains ${prefix} not followed by 4 uppercase hex digits`);
182
+ }
183
+ return undefined;
184
+ }
185
+
169
186
  export namespace Certificate {
170
187
  /**
171
188
  * Create a Certificate Signing Request (CSR) in ASN.1 DER format.
@@ -285,6 +302,21 @@ export namespace Certificate {
285
302
  }
286
303
  }
287
304
 
305
+ // Spec 6.2.2.2: an OID VID/PID anywhere in the field disables fallback parsing for that field.
306
+ if (result.vendorId === undefined && result.productId === undefined) {
307
+ const commonName = (result.commonName ?? result.commonNamePs) as string | undefined;
308
+ if (commonName !== undefined) {
309
+ const vendorId = parseMatterFallbackVidPid(commonName, "Mvid:");
310
+ if (vendorId !== undefined) {
311
+ result.vendorId = vendorId;
312
+ }
313
+ const productId = parseMatterFallbackVidPid(commonName, "Mpid:");
314
+ if (productId !== undefined) {
315
+ result.productId = productId;
316
+ }
317
+ }
318
+ }
319
+
288
320
  return result;
289
321
  }
290
322
 
@@ -677,10 +709,21 @@ function matterToX509(cert: Unsigned<MatterCertificate>): X509.UnsignedCertifica
677
709
  */
678
710
  function astOfDistinguishedName(data: { [field: string]: any }) {
679
711
  const ast = {} as { [field: string]: any[] };
712
+
713
+ // Spec 6.2.2.2 forbids mixing fallback (Mvid:/Mpid: in commonName) and OID VID/PID in one field.
714
+ const commonName = (data.commonName ?? data.commonNamePs) as string | undefined;
715
+ const usesFallbackVidPid =
716
+ commonName !== undefined &&
717
+ (parseMatterFallbackVidPid(commonName, "Mvid:") !== undefined ||
718
+ parseMatterFallbackVidPid(commonName, "Mpid:") !== undefined);
719
+
680
720
  Object.entries(data).forEach(([key, value]) => {
681
721
  if (value === undefined) {
682
722
  return;
683
723
  }
724
+ if (usesFallbackVidPid && (key === "vendorId" || key === "productId")) {
725
+ return;
726
+ }
684
727
  switch (key) {
685
728
  case "commonName":
686
729
  ast.commonName = X520.CommonName(value as string);
@@ -8,19 +8,21 @@ import {
8
8
  Bytes,
9
9
  Crypto,
10
10
  CRYPTO_AEAD_MIC_LENGTH_BYTES,
11
+ CRYPTO_PRIVACY_NONCE_LENGTH_BYTES,
11
12
  CRYPTO_SYMMETRIC_KEY_LENGTH,
12
13
  CryptoInputError,
13
14
  MaybePromise,
14
15
  } from "@matter/general";
15
16
 
16
- /** HKDF info string for deriving a privacy key from an encryption key (Matter spec §4.8.2). */
17
+ /** HKDF info string for deriving a privacy key from an encryption key (Matter spec §4.9.1). */
17
18
  const PRIVACY_KEY_INFO = Bytes.fromString("PrivacyKey");
18
19
 
19
- const NONCE_MIC_OFFSET = CRYPTO_AEAD_MIC_LENGTH_BYTES - 11;
20
- const NONCE_MIC_LENGTH = 11;
20
+ const NONCE_SESSION_ID_LENGTH = 2;
21
+ const NONCE_MIC_LENGTH = CRYPTO_PRIVACY_NONCE_LENGTH_BYTES - NONCE_SESSION_ID_LENGTH;
22
+ const NONCE_MIC_OFFSET = CRYPTO_AEAD_MIC_LENGTH_BYTES - NONCE_MIC_LENGTH;
21
23
 
22
24
  /**
23
- * Matter message privacy (spec §4.8): obfuscation of the packet header for privacy-enhanced messages.
25
+ * Matter message privacy (spec §4.9): obfuscation of the packet header for privacy-enhanced messages.
24
26
  */
25
27
  export namespace MessagePrivacy {
26
28
  /** Derive a privacy key from an encryption key: HKDF(key, salt=[], info="PrivacyKey", 16). */
@@ -18,11 +18,11 @@ import {
18
18
  } from "@matter/general";
19
19
  import {
20
20
  EventNumber,
21
+ EventPath,
21
22
  FabricIndex,
22
23
  Priority,
23
24
  resolveEventName,
24
25
  TlvEventFilter,
25
- TlvEventPath,
26
26
  TypeFromSchema,
27
27
  } from "@matter/types";
28
28
  import { EventStore, OccurrenceSummary } from "./EventStore.js";
@@ -151,7 +151,7 @@ export class OccurrenceManager {
151
151
  * @deprecated
152
152
  */
153
153
  query(
154
- eventPath: TypeFromSchema<typeof TlvEventPath>,
154
+ eventPath: EventPath,
155
155
  filters?: TypeFromSchema<typeof TlvEventFilter>[],
156
156
  filterForFabricIndex?: FabricIndex,
157
157
  ): MaybePromise<NumberedOccurrence[]> {
@@ -7,10 +7,10 @@ import { Diagnostic, MatterFlowError } from "@matter/general";
7
7
  import {
8
8
  ArraySchema,
9
9
  AttributeId,
10
+ AttributePath,
10
11
  ClusterId,
11
12
  EndpointNumber,
12
13
  NodeId,
13
- TlvAttributePath,
14
14
  TlvAttributeReport,
15
15
  TlvAttributeReportData,
16
16
  TlvDataReport,
@@ -245,10 +245,10 @@ export function compressAttributeDataReportTags(data: AttributeReportPayload[])
245
245
 
246
246
  /** Helper method to compress one path and preserve the state for the next path. */
247
247
  function compressPath(
248
- path: TypeFromSchema<typeof TlvAttributePath>,
248
+ path: AttributePath,
249
249
  dataVersion: number | undefined,
250
250
  lastFullPath: FullAttributePath | undefined,
251
- ): { path: TypeFromSchema<typeof TlvAttributePath>; lastFullPath?: FullAttributePath } {
251
+ ): { path: AttributePath; lastFullPath?: FullAttributePath } {
252
252
  const { nodeId, endpointId, clusterId, attributeId } = path;
253
253
 
254
254
  // Should never happen but typing likes it better that way
package/src/peer/Peer.ts CHANGED
@@ -579,6 +579,7 @@ export class Peer {
579
579
 
580
580
  done: PeerConnection(this, this.#context, {
581
581
  network: options?.network,
582
+ additionalMrpDelay: options?.additionalMrpDelay,
582
583
  timing: options?.timing,
583
584
  requiredTransport: options?.requiredTransport,
584
585
  preferredTransport: options?.preferredTransport,
@@ -621,6 +622,12 @@ export namespace Peer {
621
622
  */
622
623
  network?: string;
623
624
 
625
+ /**
626
+ * Per-call override for the peer-medium MRP retransmission margin. When omitted the margin derives from
627
+ * the peer's network medium, independent of any {@link network} throttle override.
628
+ */
629
+ additionalMrpDelay?: Duration;
630
+
624
631
  /**
625
632
  * A timeout relative to beginning of connection process.
626
633
  *
@@ -109,6 +109,9 @@ export async function PeerConnection(
109
109
 
110
110
  // Reserve network communication slot
111
111
  let network = context.networks.select(peer, options?.network);
112
+ const mediumProfile = context.networks.forPeer(peer);
113
+ const peerAdditionalMrpDelay =
114
+ options?.additionalMrpDelay ?? mediumProfile.connect?.additionalMrpDelay ?? mediumProfile.additionalMrpDelay;
112
115
  if (network.connect) {
113
116
  network = network.connect;
114
117
  }
@@ -303,8 +306,7 @@ export async function PeerConnection(
303
306
  return;
304
307
  }
305
308
  const variants = expandAddresses(fallback);
306
- // The interned first variant is the fallback marker; reference equality
307
- // must match what comes back out of pendingAddresses.
309
+ // Intern so attempts/pendingAddresses key on one canonical object; fallback identity is matched by value.
308
310
  attemptingFallback = addresses.add(variants[0]);
309
311
  for (const variant of variants) {
310
312
  pendingAddresses.add(variant);
@@ -406,7 +408,7 @@ export async function PeerConnection(
406
408
 
407
409
  // If this is not the fallback address but we're still attempting to connect to the fallback, it means that
408
410
  // we've discovered addresses that do not include the fallback; terminate the fallback attempt
409
- if (attemptingFallback && address !== attemptingFallback) {
411
+ if (attemptingFallback && !ServerAddress.isEqual(address, attemptingFallback)) {
410
412
  deleteAddress(
411
413
  attemptingFallback,
412
414
  "Aborting attempt to last known address because device reports address change",
@@ -456,7 +458,13 @@ export async function PeerConnection(
456
458
  isInitiator: true,
457
459
  });
458
460
 
459
- await using exchange = PeerConnection.createExchange(peer, context.exchanges, unsecuredSession, network);
461
+ await using exchange = PeerConnection.createExchange(
462
+ peer,
463
+ context.exchanges,
464
+ unsecuredSession,
465
+ network,
466
+ peerAdditionalMrpDelay,
467
+ );
460
468
 
461
469
  info(
462
470
  Diagnostic.via(`${peer.address.toString()}${exchange.via}`),
@@ -470,7 +478,7 @@ export async function PeerConnection(
470
478
  }),
471
479
  Diagnostic.asFlags({
472
480
  [network.id]: true,
473
- fallback: address === attemptingFallback,
481
+ fallback: attemptingFallback !== undefined && ServerAddress.isEqual(address, attemptingFallback),
474
482
  }),
475
483
  );
476
484
 
@@ -492,6 +500,11 @@ export async function PeerConnection(
492
500
  return;
493
501
  }
494
502
 
503
+ if (exchange.retransmissionRestartSaving < context.timing.kickMinRestartSaving) {
504
+ debug(via, address, `Suppressing "${origin}" kick, restart would save too little time`);
505
+ return;
506
+ }
507
+
495
508
  const threshold =
496
509
  origin === "discover"
497
510
  ? context.timing.kickRestartCooldown.addressChange
@@ -691,6 +704,13 @@ export namespace PeerConnection {
691
704
  export interface Options {
692
705
  abort?: AbortSignal;
693
706
  network?: string;
707
+
708
+ /**
709
+ * Per-call override for the peer-medium MRP retransmission margin. When omitted the margin derives from
710
+ * the peer's network medium, independent of any {@link network} throttle override.
711
+ */
712
+ additionalMrpDelay?: Duration;
713
+
694
714
  kicker?: Observable<[KickOrigin]>;
695
715
 
696
716
  /** See {@link Peer.ConnectOptions.requiredTransport}. */
@@ -718,10 +738,17 @@ export namespace PeerConnection {
718
738
  exchanges: ExchangeManager,
719
739
  session: Session,
720
740
  network: NetworkProfile,
741
+ peerAdditionalMrpDelay?: Duration,
721
742
  protocol = SECURE_CHANNEL_PROTOCOL_ID,
722
743
  addressOverride?: ServerAddressUdp,
723
744
  ) {
724
- return exchanges.initiateExchangeForSession(session, protocol, { onSend, onReceive, network, addressOverride });
745
+ return exchanges.initiateExchangeForSession(session, protocol, {
746
+ onSend,
747
+ onReceive,
748
+ network,
749
+ peerAdditionalMrpDelay,
750
+ addressOverride,
751
+ });
725
752
 
726
753
  function onSend(_message: Message, retransmission: number) {
727
754
  if (retransmission) {
@@ -51,6 +51,7 @@ export class PeerExchangeProvider extends ExchangeProvider {
51
51
  await this.#peer.connect({
52
52
  abort: options?.abort,
53
53
  network: options?.network,
54
+ additionalMrpDelay: options?.additionalMrpDelay,
54
55
  connectionTimeout: options?.connectionTimeout,
55
56
  requiredTransport: options?.requiredTransport,
56
57
  preferredTransport: options?.preferredTransport,
@@ -72,6 +73,8 @@ export class PeerExchangeProvider extends ExchangeProvider {
72
73
  }
73
74
 
74
75
  const network = this.#context.networks.select(this.#peer, options?.network);
76
+ const peerAdditionalMrpDelay =
77
+ options?.additionalMrpDelay ?? this.#context.networks.forPeer(this.#peer).additionalMrpDelay;
75
78
  const slot = await network.semaphore.obtainSlot(abort);
76
79
 
77
80
  try {
@@ -112,6 +115,7 @@ export class PeerExchangeProvider extends ExchangeProvider {
112
115
  this.#context.exchanges,
113
116
  session,
114
117
  network,
118
+ peerAdditionalMrpDelay,
115
119
  options?.protocol ?? INTERACTION_PROTOCOL_ID,
116
120
  options?.addressOverride,
117
121
  );
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { Duration, merge as mergeObjects, Minutes, Seconds } from "@matter/general";
7
+ import { Duration, merge as mergeObjects, Millis, Minutes, Seconds } from "@matter/general";
8
8
 
9
9
  /**
10
10
  * Parameters that control network timing for Matter sessions controlled by matter.js.
@@ -72,6 +72,16 @@ export interface PeerTimingParameters {
72
72
  */
73
73
  kickMinRetransmissions: number;
74
74
 
75
+ /**
76
+ * Minimum time a kick-initiated restart must shave off the next retransmission to be worthwhile.
77
+ *
78
+ * Restarting resets MRP backoff to its base interval, so it only helps once backoff has grown well
79
+ * beyond that base. A kick whose restart would save less than this is suppressed — for an idle/sleepy
80
+ * peer the base interval is already large, so a restart gains nothing and needlessly tears down the
81
+ * in-flight exchange.
82
+ */
83
+ kickMinRestartSaving: Duration;
84
+
75
85
  /**
76
86
  * Per-trigger cooldowns for kick-initiated CASE exchange restarts.
77
87
  *
@@ -151,10 +161,12 @@ export namespace PeerTimingParameters {
151
161
  return result;
152
162
  }
153
163
 
164
+ const maxInitialContactRetryInterval = Minutes(2);
165
+
154
166
  // TODO - tune these
155
167
  export const defaults: PeerTimingParameters = {
156
168
  defaultConnectionTimeout: Seconds(90),
157
- maxDelayBetweenInitialContactRetries: Minutes(2),
169
+ maxDelayBetweenInitialContactRetries: maxInitialContactRetryInterval,
158
170
 
159
171
  // We assume 30s processing time on peer for single Sigma actions, so give one IP a bit of time
160
172
  // to have a chance before potentially adding a load with a second try
@@ -164,6 +176,7 @@ export namespace PeerTimingParameters {
164
176
  delayAfterUnhandledError: Minutes(2),
165
177
  kickThrottleInterval: Seconds(3),
166
178
  kickMinRetransmissions: 2,
179
+ kickMinRestartSaving: Millis(maxInitialContactRetryInterval / 2),
167
180
  kickRestartCooldown: {
168
181
  addressChange: Minutes(30),
169
182
  connect: Minutes(10),
@@ -12,6 +12,7 @@ import { SecureChannelProtocol } from "#securechannel/SecureChannelProtocol.js";
12
12
  import { PaseServer } from "#session/pase/PaseServer.js";
13
13
  import { SessionManager } from "#session/SessionManager.js";
14
14
  import {
15
+ CRYPTO_PBKDF_ITERATIONS_MIN,
15
16
  Environment,
16
17
  Environmental,
17
18
  InternalError,
@@ -117,7 +118,7 @@ export class DeviceCommissioner {
117
118
 
118
119
  this.#context.secureChannelProtocol.setPaseCommissioner(
119
120
  await PaseServer.fromPin(this.#context.sessions, this.#context.commissioningConfig.values.passcode, {
120
- iterations: 1000,
121
+ iterations: CRYPTO_PBKDF_ITERATIONS_MIN,
121
122
  salt: this.#context.fabrics.crypto.randomBytes(32),
122
123
  }),
123
124
  );