@matter/protocol 0.14.0-alpha.0-20250525-d6ada0d45 → 0.14.0-alpha.0-20250528-ad0054c84

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.
@@ -4,9 +4,10 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { Bytes, Crypto, Logger, PublicKey, UnexpectedDataError } from "#general";
8
- import { SessionManager } from "#session/SessionManager.js";
9
- import { NodeId, ProtocolStatusCode, SECURE_CHANNEL_PROTOCOL_ID } from "#types";
7
+ import { Bytes, Crypto, CryptoDecryptError, Logger, PublicKey, UnexpectedDataError } from "#general";
8
+ import { TlvSessionParameters } from "#session/pase/PaseMessages.js";
9
+ import { ResumptionRecord, SessionManager } from "#session/SessionManager.js";
10
+ import { NodeId, ProtocolStatusCode, SECURE_CHANNEL_PROTOCOL_ID, TypeFromSchema } from "#types";
10
11
  import { TlvOperationalCertificate } from "../../certificate/CertificateManager.js";
11
12
  import { FabricManager, FabricNotFoundError } from "../../fabric/FabricManager.js";
12
13
  import { MessageExchange } from "../../protocol/MessageExchange.js";
@@ -21,6 +22,7 @@ import {
21
22
  RESUME2_MIC_NONCE,
22
23
  TBE_DATA2_NONCE,
23
24
  TBE_DATA3_NONCE,
25
+ TlvCaseSigma1,
24
26
  TlvEncryptedDataSigma2,
25
27
  TlvEncryptedDataSigma3,
26
28
  TlvSignedData,
@@ -44,7 +46,7 @@ export class CaseServer implements ProtocolHandler {
44
46
  async onNewExchange(exchange: MessageExchange) {
45
47
  const messenger = new CaseServerMessenger(exchange);
46
48
  try {
47
- await this.handleSigma1(messenger);
49
+ await this.#handleSigma1(messenger);
48
50
  } catch (error) {
49
51
  logger.error("An error occurred during the commissioning", error);
50
52
 
@@ -61,211 +63,272 @@ export class CaseServer implements ProtocolHandler {
61
63
  }
62
64
  }
63
65
 
64
- private async handleSigma1(messenger: CaseServerMessenger) {
66
+ async #handleSigma1(messenger: CaseServerMessenger) {
65
67
  logger.info(`Received pairing request from ${messenger.getChannelName()}`);
66
- // Generate pairing info
67
- const responderRandom = Crypto.getRandom();
68
68
 
69
- // Read and process sigma 1
69
+ // Initialize context with information from peer
70
70
  const { sigma1Bytes, sigma1 } = await messenger.readSigma1();
71
- const {
72
- initiatorSessionId: peerSessionId,
73
- resumptionId: peerResumptionId,
74
- initiatorResumeMic: peerResumeMic,
75
- destinationId,
76
- initiatorRandom: peerRandom,
77
- initiatorEcdhPublicKey: peerEcdhPublicKey,
78
- initiatorSessionParams,
79
- } = sigma1;
80
-
81
- // Try to resume a previous session
82
- const resumptionId = Crypto.getRandomData(16);
83
-
84
71
  const resumptionRecord =
85
- peerResumptionId !== undefined && peerResumeMic !== undefined
86
- ? this.#sessions.findResumptionRecordById(peerResumptionId)
72
+ sigma1.resumptionId !== undefined && sigma1.initiatorResumeMic !== undefined
73
+ ? this.#sessions.findResumptionRecordById(sigma1.resumptionId)
87
74
  : undefined;
88
- // We try to resume the session
89
- if (peerResumptionId !== undefined && peerResumeMic !== undefined && resumptionRecord !== undefined) {
90
- const { sharedSecret, fabric, peerNodeId, caseAuthenticatedTags } = resumptionRecord;
91
- const peerResumeKey = await Crypto.hkdf(
92
- sharedSecret,
93
- Bytes.concat(peerRandom, peerResumptionId),
94
- KDFSR1_KEY_INFO,
95
- );
96
- Crypto.decrypt(peerResumeKey, peerResumeMic, RESUME1_MIC_NONCE);
97
-
98
- // All good! Create secure session
99
- const responderSessionId = await this.#sessions.getNextAvailableSessionId();
100
- const secureSessionSalt = Bytes.concat(peerRandom, peerResumptionId);
101
- const secureSession = await this.#sessions.createSecureSession({
102
- sessionId: responderSessionId,
103
- fabric,
104
- peerNodeId,
105
- peerSessionId,
106
- sharedSecret,
107
- salt: secureSessionSalt,
108
- isInitiator: false,
109
- isResumption: true,
110
- peerSessionParameters: initiatorSessionParams,
111
- caseAuthenticatedTags,
112
- });
113
75
 
114
- // Generate sigma 2 resume
115
- const resumeSalt = Bytes.concat(peerRandom, resumptionId);
116
- const resumeKey = await Crypto.hkdf(sharedSecret, resumeSalt, KDFSR2_KEY_INFO);
117
- const resumeMic = Crypto.encrypt(resumeKey, new Uint8Array(0), RESUME2_MIC_NONCE);
118
- try {
119
- await messenger.sendSigma2Resume({
120
- resumptionId,
121
- resumeMic,
122
- responderSessionId,
123
- responderSessionParams: this.#sessions.sessionParameters, // responder session parameters
124
- });
125
- } catch (error) {
126
- // If we fail to send the resume, we destroy the session
127
- await secureSession.destroy(false);
128
- throw error;
129
- }
76
+ const context = new Sigma1Context(messenger, sigma1Bytes, sigma1, resumptionRecord);
130
77
 
131
- logger.info(
132
- `Session ${secureSession.id} resumed with ${messenger.getChannelName()} for Fabric ${NodeId.toHexString(
133
- fabric.nodeId,
134
- )} (index ${fabric.fabricIndex}) and PeerNode ${NodeId.toHexString(peerNodeId)}`,
135
- "with CATs",
136
- caseAuthenticatedTags,
137
- );
138
- resumptionRecord.resumptionId = resumptionId; /* Update the ID */
139
-
140
- // Wait for success on the peer side
141
- await messenger.waitForSuccess("Sigma2Resume-Success");
142
-
143
- await messenger.close();
144
- await this.#sessions.saveResumptionRecord(resumptionRecord);
145
- } else if (
146
- (peerResumptionId === undefined && peerResumeMic === undefined) ||
147
- (peerResumptionId !== undefined && peerResumeMic !== undefined && resumptionRecord === undefined)
148
- ) {
149
- // Generate sigma 2
150
- // TODO: Pass through a group id?
151
- const fabric = await this.#fabrics.findFabricFromDestinationId(destinationId, peerRandom);
152
- const { operationalCert: nodeOpCert, intermediateCACert, operationalIdentityProtectionKey } = fabric;
153
- const { publicKey: responderEcdhPublicKey, sharedSecret } =
154
- await Crypto.ecdhGeneratePublicKeyAndSecret(peerEcdhPublicKey);
155
- const sigma2Salt = Bytes.concat(
156
- operationalIdentityProtectionKey,
157
- responderRandom,
158
- responderEcdhPublicKey,
159
- await Crypto.hash(sigma1Bytes),
160
- );
161
- const sigma2Key = await Crypto.hkdf(sharedSecret, sigma2Salt, KDFSR2_INFO);
162
- const signatureData = TlvSignedData.encode({
163
- nodeOpCert,
164
- intermediateCACert,
165
- ecdhPublicKey: responderEcdhPublicKey,
166
- peerEcdhPublicKey,
167
- });
168
- const signature = await fabric.sign(signatureData);
169
- const encryptedData = TlvEncryptedDataSigma2.encode({
170
- nodeOpCert,
171
- intermediateCACert,
172
- signature,
173
- resumptionId,
174
- });
175
- const encrypted = Crypto.encrypt(sigma2Key, encryptedData, TBE_DATA2_NONCE);
176
- const responderSessionId = await this.#sessions.getNextAvailableSessionId();
177
- const sigma2Bytes = await messenger.sendSigma2({
178
- responderRandom,
78
+ // Attempt resumption
79
+ if (await this.#resume(context)) {
80
+ return;
81
+ }
82
+
83
+ // Attempt sigma2 negotiation
84
+ if (await this.#generateSigma2(context)) {
85
+ return;
86
+ }
87
+
88
+ logger.info(
89
+ `Invalid resumption ID or resume MIC received from ${messenger.getChannelName()}`,
90
+ context.peerResumptionId,
91
+ context.peerResumeMic,
92
+ );
93
+
94
+ throw new UnexpectedDataError("Invalid resumption ID or resume MIC.");
95
+ }
96
+
97
+ async #resume(cx: Sigma1Context) {
98
+ if (cx.peerResumptionId === undefined || cx.peerResumeMic === undefined || cx.resumptionRecord === undefined) {
99
+ return false;
100
+ }
101
+
102
+ const { sharedSecret, fabric, peerNodeId, caseAuthenticatedTags } = cx.resumptionRecord;
103
+ const peerResumeKey = await Crypto.hkdf(
104
+ sharedSecret,
105
+ Bytes.concat(cx.peerRandom, cx.peerResumptionId),
106
+ KDFSR1_KEY_INFO,
107
+ );
108
+
109
+ try {
110
+ Crypto.decrypt(peerResumeKey, cx.peerResumeMic, RESUME1_MIC_NONCE);
111
+ } catch (e) {
112
+ CryptoDecryptError.accept(e);
113
+
114
+ // Clear resumption and initiate negotiate new connection
115
+ cx.peerResumptionId = cx.peerResumeMic = undefined;
116
+
117
+ return false;
118
+ }
119
+
120
+ // All good! Create secure session
121
+ const responderSessionId = await this.#sessions.getNextAvailableSessionId();
122
+ const secureSessionSalt = Bytes.concat(cx.peerRandom, cx.peerResumptionId);
123
+ const secureSession = await this.#sessions.createSecureSession({
124
+ sessionId: responderSessionId,
125
+ fabric,
126
+ peerNodeId,
127
+ peerSessionId: cx.peerSessionId,
128
+ sharedSecret,
129
+ salt: secureSessionSalt,
130
+ isInitiator: false,
131
+ isResumption: true,
132
+ peerSessionParameters: cx.peerSessionParams,
133
+ caseAuthenticatedTags,
134
+ });
135
+
136
+ // Generate sigma 2 resume
137
+ const resumeSalt = Bytes.concat(cx.peerRandom, cx.localResumptionId);
138
+ const resumeKey = await Crypto.hkdf(sharedSecret, resumeSalt, KDFSR2_KEY_INFO);
139
+ const resumeMic = Crypto.encrypt(resumeKey, new Uint8Array(0), RESUME2_MIC_NONCE);
140
+ try {
141
+ await cx.messenger.sendSigma2Resume({
142
+ resumptionId: cx.localResumptionId,
143
+ resumeMic,
179
144
  responderSessionId,
180
- responderEcdhPublicKey,
181
- encrypted,
182
145
  responderSessionParams: this.#sessions.sessionParameters, // responder session parameters
183
146
  });
147
+ } catch (error) {
148
+ // If we fail to send the resume, we destroy the session
149
+ await secureSession.destroy(false);
150
+ throw error;
151
+ }
184
152
 
185
- // Read and process sigma 3
186
- const {
187
- sigma3Bytes,
188
- sigma3: { encrypted: peerEncrypted },
189
- } = await messenger.readSigma3();
190
- const sigma3Salt = Bytes.concat(
191
- operationalIdentityProtectionKey,
192
- await Crypto.hash([sigma1Bytes, sigma2Bytes]),
193
- );
194
- const sigma3Key = await Crypto.hkdf(sharedSecret, sigma3Salt, KDFSR3_INFO);
195
- const peerDecryptedData = Crypto.decrypt(sigma3Key, peerEncrypted, TBE_DATA3_NONCE);
196
- const {
197
- nodeOpCert: peerNewOpCert,
198
- intermediateCACert: peerIntermediateCACert,
199
- signature: peerSignature,
200
- } = TlvEncryptedDataSigma3.decode(peerDecryptedData);
201
-
202
- await fabric.verifyCredentials(peerNewOpCert, peerIntermediateCACert);
203
-
204
- const peerSignatureData = TlvSignedData.encode({
205
- nodeOpCert: peerNewOpCert,
206
- intermediateCACert: peerIntermediateCACert,
207
- ecdhPublicKey: peerEcdhPublicKey,
208
- peerEcdhPublicKey: responderEcdhPublicKey,
209
- });
210
- const {
211
- ellipticCurvePublicKey: peerPublicKey,
212
- subject: { fabricId: peerFabricId, nodeId: peerNodeId, caseAuthenticatedTags },
213
- } = TlvOperationalCertificate.decode(peerNewOpCert);
153
+ logger.info(
154
+ `Session ${secureSession.id} resumed with ${cx.messenger.getChannelName()} for Fabric ${NodeId.toHexString(
155
+ fabric.nodeId,
156
+ )} (index ${fabric.fabricIndex}) and PeerNode ${NodeId.toHexString(peerNodeId)}`,
157
+ "with CATs",
158
+ caseAuthenticatedTags,
159
+ );
160
+ cx.resumptionRecord.resumptionId = cx.localResumptionId; /* Update the ID */
214
161
 
215
- if (fabric.fabricId !== peerFabricId) {
216
- throw new UnexpectedDataError(`Fabric ID mismatch: ${fabric.fabricId} !== ${peerFabricId}`);
217
- }
162
+ // Wait for success on the peer side
163
+ await cx.messenger.waitForSuccess("Sigma2Resume-Success");
218
164
 
219
- await Crypto.verify(PublicKey(peerPublicKey), peerSignatureData, peerSignature);
220
-
221
- // All good! Create secure session
222
- const secureSessionSalt = Bytes.concat(
223
- operationalIdentityProtectionKey,
224
- await Crypto.hash([sigma1Bytes, sigma2Bytes, sigma3Bytes]),
225
- );
226
- const secureSession = await this.#sessions.createSecureSession({
227
- sessionId: responderSessionId,
228
- fabric,
229
- peerNodeId,
230
- peerSessionId,
231
- sharedSecret,
232
- salt: secureSessionSalt,
233
- isInitiator: false,
234
- isResumption: false,
235
- peerSessionParameters: initiatorSessionParams,
236
- caseAuthenticatedTags,
237
- });
238
- logger.info(
239
- `Session ${secureSession.id} created with ${messenger.getChannelName()} for Fabric ${NodeId.toHexString(
240
- fabric.nodeId,
241
- )} (index ${fabric.fabricIndex}) and PeerNode ${NodeId.toHexString(peerNodeId)}`,
242
- "with CATs",
243
- caseAuthenticatedTags,
244
- );
245
- await messenger.sendSuccess();
246
-
247
- const resumptionRecord = {
248
- peerNodeId,
249
- fabric,
250
- sharedSecret,
251
- resumptionId,
252
- sessionParameters: secureSession.parameters,
253
- caseAuthenticatedTags,
254
- };
255
-
256
- await messenger.close();
257
- await this.#sessions.saveResumptionRecord(resumptionRecord);
258
- } else {
259
- logger.info(
260
- `Invalid resumption ID or resume MIC received from ${messenger.getChannelName()}`,
261
- peerResumptionId,
262
- peerResumeMic,
263
- );
264
- throw new UnexpectedDataError("Invalid resumption ID or resume MIC.");
165
+ await cx.messenger.close();
166
+ await this.#sessions.saveResumptionRecord(cx.resumptionRecord);
167
+
168
+ return true;
169
+ }
170
+
171
+ async #generateSigma2(cx: Sigma1Context) {
172
+ if (
173
+ // No resumption attempted is OK
174
+ !(cx.peerResumptionId === undefined && cx.peerResumeMic === undefined) &&
175
+ // Resumption attempted with no record on our side is OK
176
+ !(cx.peerResumptionId !== undefined && cx.peerResumeMic !== undefined && cx.resumptionRecord === undefined)
177
+ ) {
178
+ return false;
265
179
  }
180
+
181
+ // Generate pairing info
182
+ const responderRandom = Crypto.getRandom();
183
+
184
+ // TODO: Pass through a group id?
185
+ const fabric = await this.#fabrics.findFabricFromDestinationId(cx.destinationId, cx.peerRandom);
186
+ const { operationalCert: nodeOpCert, intermediateCACert, operationalIdentityProtectionKey } = fabric;
187
+ const { publicKey: responderEcdhPublicKey, sharedSecret } = await Crypto.ecdhGeneratePublicKeyAndSecret(
188
+ cx.peerEcdhPublicKey,
189
+ );
190
+ const sigma2Salt = Bytes.concat(
191
+ operationalIdentityProtectionKey,
192
+ responderRandom,
193
+ responderEcdhPublicKey,
194
+ await Crypto.hash(cx.bytes),
195
+ );
196
+ const sigma2Key = await Crypto.hkdf(sharedSecret, sigma2Salt, KDFSR2_INFO);
197
+ const signatureData = TlvSignedData.encode({
198
+ nodeOpCert,
199
+ intermediateCACert,
200
+ ecdhPublicKey: responderEcdhPublicKey,
201
+ peerEcdhPublicKey: cx.peerEcdhPublicKey,
202
+ });
203
+ const signature = await fabric.sign(signatureData);
204
+ const encryptedData = TlvEncryptedDataSigma2.encode({
205
+ nodeOpCert,
206
+ intermediateCACert,
207
+ signature,
208
+ resumptionId: cx.localResumptionId,
209
+ });
210
+ const encrypted = Crypto.encrypt(sigma2Key, encryptedData, TBE_DATA2_NONCE);
211
+ const responderSessionId = await this.#sessions.getNextAvailableSessionId();
212
+ const sigma2Bytes = await cx.messenger.sendSigma2({
213
+ responderRandom,
214
+ responderSessionId,
215
+ responderEcdhPublicKey,
216
+ encrypted,
217
+ responderSessionParams: this.#sessions.sessionParameters, // responder session parameters
218
+ });
219
+
220
+ // Read and process sigma 3
221
+ const {
222
+ sigma3Bytes,
223
+ sigma3: { encrypted: peerEncrypted },
224
+ } = await cx.messenger.readSigma3();
225
+ const sigma3Salt = Bytes.concat(operationalIdentityProtectionKey, await Crypto.hash([cx.bytes, sigma2Bytes]));
226
+ const sigma3Key = await Crypto.hkdf(sharedSecret, sigma3Salt, KDFSR3_INFO);
227
+ const peerDecryptedData = Crypto.decrypt(sigma3Key, peerEncrypted, TBE_DATA3_NONCE);
228
+ const {
229
+ nodeOpCert: peerNewOpCert,
230
+ intermediateCACert: peerIntermediateCACert,
231
+ signature: peerSignature,
232
+ } = TlvEncryptedDataSigma3.decode(peerDecryptedData);
233
+
234
+ await fabric.verifyCredentials(peerNewOpCert, peerIntermediateCACert);
235
+
236
+ const peerSignatureData = TlvSignedData.encode({
237
+ nodeOpCert: peerNewOpCert,
238
+ intermediateCACert: peerIntermediateCACert,
239
+ ecdhPublicKey: cx.peerEcdhPublicKey,
240
+ peerEcdhPublicKey: responderEcdhPublicKey,
241
+ });
242
+ const {
243
+ ellipticCurvePublicKey: peerPublicKey,
244
+ subject: { fabricId: peerFabricId, nodeId: peerNodeId, caseAuthenticatedTags },
245
+ } = TlvOperationalCertificate.decode(peerNewOpCert);
246
+
247
+ if (fabric.fabricId !== peerFabricId) {
248
+ throw new UnexpectedDataError(`Fabric ID mismatch: ${fabric.fabricId} !== ${peerFabricId}`);
249
+ }
250
+
251
+ await Crypto.verify(PublicKey(peerPublicKey), peerSignatureData, peerSignature);
252
+
253
+ // All good! Create secure session
254
+ const secureSessionSalt = Bytes.concat(
255
+ operationalIdentityProtectionKey,
256
+ await Crypto.hash([cx.bytes, sigma2Bytes, sigma3Bytes]),
257
+ );
258
+ const secureSession = await this.#sessions.createSecureSession({
259
+ sessionId: responderSessionId,
260
+ fabric,
261
+ peerNodeId,
262
+ peerSessionId: cx.peerSessionId,
263
+ sharedSecret,
264
+ salt: secureSessionSalt,
265
+ isInitiator: false,
266
+ isResumption: false,
267
+ peerSessionParameters: cx.peerSessionParams,
268
+ caseAuthenticatedTags,
269
+ });
270
+ logger.info(
271
+ `Session ${secureSession.id} created with ${cx.messenger.getChannelName()} for Fabric ${NodeId.toHexString(
272
+ fabric.nodeId,
273
+ )} (index ${fabric.fabricIndex}) and PeerNode ${NodeId.toHexString(peerNodeId)}`,
274
+ "with CATs",
275
+ caseAuthenticatedTags,
276
+ );
277
+ await cx.messenger.sendSuccess();
278
+
279
+ const resumptionRecord = {
280
+ peerNodeId,
281
+ fabric,
282
+ sharedSecret,
283
+ resumptionId: cx.localResumptionId,
284
+ sessionParameters: secureSession.parameters,
285
+ caseAuthenticatedTags,
286
+ };
287
+
288
+ await cx.messenger.close();
289
+ await this.#sessions.saveResumptionRecord(resumptionRecord);
290
+
291
+ return true;
266
292
  }
267
293
 
268
294
  async close() {
269
295
  // Nothing to do
270
296
  }
271
297
  }
298
+
299
+ class Sigma1Context {
300
+ messenger: CaseServerMessenger;
301
+ bytes: Uint8Array;
302
+ peerSessionId: number;
303
+ peerResumptionId?: Uint8Array;
304
+ peerResumeMic?: Uint8Array;
305
+ destinationId: Uint8Array;
306
+ peerRandom: Uint8Array;
307
+ peerEcdhPublicKey: Uint8Array;
308
+ peerSessionParams?: TypeFromSchema<typeof TlvSessionParameters>;
309
+ resumptionRecord?: ResumptionRecord;
310
+
311
+ #localResumptionId?: Uint8Array;
312
+
313
+ constructor(
314
+ messenger: CaseServerMessenger,
315
+ bytes: Uint8Array,
316
+ sigma1: TypeFromSchema<typeof TlvCaseSigma1>,
317
+ resumptionRecord?: ResumptionRecord,
318
+ ) {
319
+ this.messenger = messenger;
320
+ this.bytes = bytes;
321
+ this.peerSessionId = sigma1.initiatorSessionId;
322
+ this.peerResumptionId = sigma1.resumptionId;
323
+ this.peerResumeMic = sigma1.initiatorResumeMic;
324
+ this.destinationId = sigma1.destinationId;
325
+ this.peerRandom = sigma1.initiatorRandom;
326
+ this.peerEcdhPublicKey = sigma1.initiatorEcdhPublicKey;
327
+ this.peerSessionParams = sigma1.initiatorSessionParams;
328
+ this.resumptionRecord = resumptionRecord;
329
+ }
330
+
331
+ get localResumptionId() {
332
+ return (this.#localResumptionId ??= Crypto.getRandomData(16));
333
+ }
334
+ }