Line data Source code
1 : /*
2 : * Copyright (c) 2025 Project CHIP Authors
3 : * All rights reserved.
4 : *
5 : * Licensed under the Apache License, Version 2.0 (the "License");
6 : * you may not use this file except in compliance with the License.
7 : * You may obtain a copy of the License at
8 : *
9 : * http://www.apache.org/licenses/LICENSE-2.0
10 : *
11 : * Unless required by applicable law or agreed to in writing, software
12 : * distributed under the License is distributed on an "AS IS" BASIS,
13 : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 : * See the License for the specific language governing permissions and
15 : * limitations under the License.
16 : */
17 :
18 : #include "DeviceCommissioner.h"
19 :
20 : #include <app-common/zap-generated/ids/Attributes.h>
21 : #include <app-common/zap-generated/ids/Clusters.h>
22 : #include <app/InteractionModelEngine.h>
23 : #include <controller/CommissioningDelegate.h>
24 : #include <credentials/CHIPCert.h>
25 : #include <credentials/FabricTable.h>
26 : #include <crypto/CHIPCryptoPAL.h>
27 : #include <lib/core/CHIPCore.h>
28 : #include <lib/core/CHIPError.h>
29 : #include <lib/dnssd/Advertiser.h>
30 :
31 : using namespace ::chip;
32 : using namespace ::chip::app;
33 : using namespace ::chip::Credentials;
34 : using namespace ::chip::Crypto;
35 : using namespace chip::app::Clusters;
36 : using namespace ::chip::Credentials::JCM;
37 :
38 : namespace chip {
39 : namespace Controller {
40 : namespace JCM {
41 :
42 : /*
43 : * DeviceCommissioner public interface and override implementation
44 : */
45 0 : CHIP_ERROR DeviceCommissioner::StartJCMTrustVerification(DeviceProxy * proxy)
46 : {
47 0 : TrustVerificationError error = TrustVerificationError::kSuccess;
48 :
49 0 : mDeviceProxy = proxy;
50 :
51 0 : ChipLogProgress(Controller, "JCM: Starting Trust Verification");
52 :
53 0 : TrustVerificationStageFinished(TrustVerificationStage::kIdle, error);
54 0 : if (error != TrustVerificationError::kSuccess)
55 : {
56 0 : ChipLogError(Controller, "JCM: Failed to start Trust Verification: %s", EnumToString(error).c_str());
57 0 : return CHIP_ERROR_INTERNAL;
58 : }
59 :
60 0 : return CHIP_NO_ERROR;
61 : }
62 :
63 2 : void DeviceCommissioner::ContinueAfterUserConsent(const bool & consent)
64 : {
65 2 : TrustVerificationError error = TrustVerificationError::kSuccess;
66 :
67 2 : if (!consent)
68 : {
69 1 : error = TrustVerificationError::kUserDeniedConsent;
70 : }
71 :
72 2 : TrustVerificationStageFinished(TrustVerificationStage::kAskingUserForConsent, error);
73 2 : }
74 :
75 2 : void DeviceCommissioner::ContinueAfterVendorIDVerification(const CHIP_ERROR & err)
76 : {
77 2 : TrustVerificationError error = TrustVerificationError::kSuccess;
78 :
79 4 : if (err != CHIP_NO_ERROR)
80 : {
81 1 : error = TrustVerificationError::kVendorIdVerificationFailed;
82 : }
83 :
84 2 : TrustVerificationStageFinished(TrustVerificationStage::kPerformingVendorIDVerification, error);
85 2 : }
86 :
87 5 : CHIP_ERROR DeviceCommissioner::ParseAdminFabricIndexAndEndpointId(const ReadCommissioningInfo & info)
88 : {
89 5 : auto attributeCache = info.attributes;
90 :
91 5 : CHIP_ERROR err = attributeCache->ForEachAttribute(
92 15 : Clusters::JointFabricAdministrator::Id, [this, &attributeCache](const ConcreteAttributePath & path) {
93 : using namespace Clusters::JointFabricAdministrator::Attributes;
94 5 : AdministratorFabricIndex::TypeInfo::DecodableType administratorFabricIndex;
95 :
96 5 : VerifyOrReturnError(path.mAttributeId == AdministratorFabricIndex::Id, CHIP_NO_ERROR);
97 5 : ReturnErrorOnFailure(attributeCache->Get<AdministratorFabricIndex::TypeInfo>(path, administratorFabricIndex));
98 :
99 5 : if (!administratorFabricIndex.IsNull() && administratorFabricIndex.Value() != kUndefinedFabricIndex)
100 : {
101 5 : ChipLogProgress(Controller, "JCM: AdministratorFabricIndex: %d", administratorFabricIndex.Value());
102 5 : mInfo.adminFabricIndex = administratorFabricIndex.Value();
103 5 : mInfo.adminEndpointId = path.mEndpointId;
104 : }
105 : else
106 : {
107 0 : ChipLogError(Controller, "JCM: JF Administrator Cluster not found!");
108 0 : return CHIP_ERROR_NOT_FOUND;
109 : }
110 :
111 : // This is not an error; error checking is after iterating through the attributes
112 5 : return CHIP_NO_ERROR;
113 : });
114 :
115 10 : if (err != CHIP_NO_ERROR)
116 : {
117 0 : ChipLogError(Controller, "JCM: Failed to parse Administrator Fabric Index: %s", err.AsString());
118 0 : return err;
119 : }
120 :
121 5 : if (mInfo.adminFabricIndex == kUndefinedFabricIndex)
122 : {
123 0 : ChipLogError(Controller, "JCM: Invalid Fabric Index");
124 0 : return CHIP_ERROR_NOT_FOUND;
125 : }
126 :
127 5 : if (mInfo.adminEndpointId == kInvalidEndpointId)
128 : {
129 0 : ChipLogError(Controller, "JCM: Invalid Endpoint ID");
130 0 : return CHIP_ERROR_NOT_FOUND;
131 : }
132 :
133 5 : ChipLogProgress(Controller, "JCM: Successfully parsed the Administrator Fabric Index and Endpoint ID");
134 :
135 5 : return CHIP_NO_ERROR;
136 : }
137 :
138 4 : CHIP_ERROR DeviceCommissioner::ParseOperationalCredentials(const ReadCommissioningInfo & info)
139 : {
140 4 : auto attributeCache = info.attributes;
141 :
142 : CHIP_ERROR err =
143 4 : attributeCache->ForEachAttribute(OperationalCredentials::Id, [this, &attributeCache](const ConcreteAttributePath & path) {
144 : using namespace chip::app::Clusters::OperationalCredentials::Attributes;
145 :
146 12 : switch (path.mAttributeId)
147 : {
148 4 : case Fabrics::Id: {
149 4 : Fabrics::TypeInfo::DecodableType fabrics;
150 4 : ReturnErrorOnFailure(attributeCache->Get<Fabrics::TypeInfo>(path, fabrics));
151 4 : bool foundMatchingFabricIndex = false;
152 :
153 4 : auto iter = fabrics.begin();
154 4 : while (iter.Next())
155 : {
156 4 : auto & fabricDescriptor = iter.GetValue();
157 :
158 4 : if (fabricDescriptor.VIDVerificationStatement.HasValue())
159 : {
160 0 : ChipLogError(
161 : Controller,
162 : "JCM: A VID Verification Statement is not supported, Joint Fabric requires an ICA on the fabric!");
163 0 : return CHIP_ERROR_CANCELLED;
164 : }
165 :
166 4 : if (fabricDescriptor.fabricIndex == mInfo.adminFabricIndex)
167 : {
168 4 : if (fabricDescriptor.rootPublicKey.size() != kP256_PublicKey_Length)
169 : {
170 0 : ChipLogError(Controller, "JCM: Fabric root key size mismatch");
171 0 : return CHIP_ERROR_KEY_NOT_FOUND;
172 : }
173 :
174 4 : mInfo.rootPublicKey.CopyFromSpan(fabricDescriptor.rootPublicKey);
175 4 : mInfo.adminVendorId = fabricDescriptor.vendorID;
176 4 : mInfo.adminFabricId = fabricDescriptor.fabricID;
177 4 : foundMatchingFabricIndex = true;
178 :
179 4 : ChipLogProgress(Controller, "JCM: Successfully parsed the Administrator Fabric Table");
180 4 : break;
181 : }
182 : }
183 4 : if (!foundMatchingFabricIndex)
184 : {
185 0 : return CHIP_ERROR_NOT_FOUND;
186 : }
187 :
188 : // Successfully parsed the Fabric Descriptor
189 4 : return CHIP_NO_ERROR;
190 : }
191 4 : case NOCs::Id: {
192 4 : NOCs::TypeInfo::DecodableType nocs;
193 4 : ReturnErrorOnFailure(attributeCache->Get<NOCs::TypeInfo>(path, nocs));
194 :
195 4 : auto iter = nocs.begin();
196 4 : while (iter.Next())
197 : {
198 4 : auto & nocStruct = iter.GetValue();
199 :
200 4 : if (nocStruct.fabricIndex == mInfo.adminFabricIndex)
201 : {
202 4 : mInfo.adminNOC.CopyFromSpan(nocStruct.noc);
203 :
204 4 : if (!nocStruct.icac.IsNull())
205 : {
206 4 : auto icac = nocStruct.icac.Value();
207 4 : mInfo.adminICAC.CopyFromSpan(icac);
208 : }
209 : else
210 : {
211 0 : ChipLogError(Controller, "JCM: ICAC not present!");
212 0 : return CHIP_ERROR_CERT_NOT_FOUND;
213 : }
214 4 : ChipLogProgress(Controller, "JCM: Successfully parsed the Administrator NOC and ICAC");
215 4 : break;
216 : }
217 : }
218 4 : return CHIP_NO_ERROR;
219 : }
220 4 : default:
221 : // Ignore other attributes; this is not an error condition
222 4 : return CHIP_NO_ERROR;
223 : }
224 :
225 : // This is not an error; error checking is after iterating through the attributes
226 : return CHIP_NO_ERROR;
227 : });
228 :
229 8 : if (err != CHIP_NO_ERROR)
230 : {
231 0 : ChipLogError(Controller, "JCM: Failed to parse Operational Credentials: %s", err.AsString());
232 0 : return err;
233 : }
234 :
235 4 : if (mInfo.rootPublicKey.AllocatedSize() == 0)
236 : {
237 0 : ChipLogError(Controller, "JCM: Root public key is empty!");
238 0 : return CHIP_ERROR_KEY_NOT_FOUND;
239 : }
240 :
241 4 : if (mInfo.adminNOC.AllocatedSize() == 0)
242 : {
243 0 : ChipLogError(Controller, "JCM: Administrator NOC is empty!");
244 0 : return CHIP_ERROR_CERT_NOT_FOUND;
245 : }
246 :
247 4 : if (mInfo.adminICAC.AllocatedSize() == 0)
248 : {
249 0 : ChipLogError(Controller, "JCM: Administrator ICAC is empty!");
250 0 : return CHIP_ERROR_CERT_NOT_FOUND;
251 : }
252 :
253 4 : ChipLogProgress(Controller, "JCM: Successfully parsed the Operational Credentials");
254 :
255 4 : return CHIP_NO_ERROR;
256 : }
257 :
258 3 : CHIP_ERROR DeviceCommissioner::ParseTrustedRoot(const ReadCommissioningInfo & info)
259 : {
260 3 : auto attributeCache = info.attributes;
261 :
262 : CHIP_ERROR err =
263 3 : attributeCache->ForEachAttribute(OperationalCredentials::Id, [this, &attributeCache](const ConcreteAttributePath & path) {
264 : using namespace chip::app::Clusters::OperationalCredentials::Attributes;
265 :
266 9 : switch (path.mAttributeId)
267 : {
268 3 : case TrustedRootCertificates::Id: {
269 3 : bool foundMatchingRcac = false;
270 3 : TrustedRootCertificates::TypeInfo::DecodableType trustedCAs;
271 3 : ReturnErrorOnFailure(attributeCache->Get<TrustedRootCertificates::TypeInfo>(path, trustedCAs));
272 :
273 3 : auto iter = trustedCAs.begin();
274 3 : while (iter.Next())
275 : {
276 3 : auto & trustedCA = iter.GetValue();
277 3 : P256PublicKeySpan trustedCAPublicKeySpan;
278 :
279 3 : ReturnErrorOnFailure(ExtractPublicKeyFromChipCert(trustedCA, trustedCAPublicKeySpan));
280 3 : P256PublicKey trustedCAPublicKey{ trustedCAPublicKeySpan };
281 :
282 3 : if (mInfo.rootPublicKey.AllocatedSize() != kP256_PublicKey_Length)
283 : {
284 0 : ChipLogError(Controller, "JCM: Fabric root key size mismatch");
285 0 : return CHIP_ERROR_KEY_NOT_FOUND;
286 : }
287 :
288 3 : P256PublicKeySpan rootPubKeySpan(mInfo.rootPublicKey.Get());
289 3 : P256PublicKey fabricTableRootPublicKey{ rootPubKeySpan };
290 :
291 3 : if (trustedCAPublicKey.Matches(fabricTableRootPublicKey) && trustedCA.size())
292 : {
293 3 : mInfo.adminRCAC.CopyFromSpan(trustedCA);
294 : // Successfully found the matching RCAC
295 3 : foundMatchingRcac = true;
296 3 : break;
297 : }
298 6 : }
299 3 : if (!foundMatchingRcac)
300 : {
301 : // Since a matching RCAC was not found, we cannot proceed
302 0 : return CHIP_ERROR_CERT_NOT_FOUND;
303 : }
304 :
305 : // Successfully parsed the Trusted Root Certificates
306 3 : return CHIP_NO_ERROR;
307 : }
308 6 : default:
309 : // Ignore other attributes; this is not an error condition
310 6 : return CHIP_NO_ERROR;
311 : }
312 : return CHIP_NO_ERROR;
313 : });
314 :
315 3 : if (mInfo.adminRCAC.AllocatedSize() == 0)
316 : {
317 0 : ChipLogError(Controller, "JCM: Did not find a matching RCAC!");
318 0 : mInfo.adminFabricIndex = kUndefinedFabricIndex;
319 0 : return CHIP_ERROR_CERT_NOT_FOUND;
320 : }
321 :
322 6 : if (err != CHIP_NO_ERROR)
323 : {
324 0 : ChipLogError(Controller, "JCM: Failed to parse Trusted Root Certificates: %s", err.AsString());
325 : }
326 :
327 3 : return err;
328 : }
329 :
330 2 : CHIP_ERROR DeviceCommissioner::ParseExtraCommissioningInfo(ReadCommissioningInfo & info, const CommissioningParameters & params)
331 : {
332 : using namespace OperationalCredentials::Attributes;
333 :
334 2 : CHIP_ERROR err = CHIP_NO_ERROR;
335 :
336 : #if CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC
337 : if (!params.GetUseJCM().ValueOr(false))
338 : {
339 : return chip::Controller::DeviceCommissioner::ParseExtraCommissioningInfo(info, params);
340 : }
341 : #endif // CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC
342 :
343 2 : err = ParseAdminFabricIndexAndEndpointId(info);
344 4 : if (err != CHIP_NO_ERROR)
345 : {
346 0 : ChipLogError(Controller, "JCM: Failed to find Administrator Fabric Index and Endpoint ID");
347 0 : return err;
348 : }
349 :
350 2 : err = ParseOperationalCredentials(info);
351 4 : if (err != CHIP_NO_ERROR)
352 : {
353 0 : ChipLogError(Controller, "JCM: Failed to find Fabric Descriptor Information");
354 0 : return err;
355 : }
356 :
357 2 : err = ParseTrustedRoot(info);
358 4 : if (err != CHIP_NO_ERROR)
359 : {
360 0 : ChipLogError(Controller, "JCM: Failed to find Trusted Root Certificate");
361 0 : return err;
362 : }
363 :
364 2 : return chip::Controller::DeviceCommissioner::ParseExtraCommissioningInfo(info, params);
365 : }
366 :
367 1 : TrustVerificationError DeviceCommissioner::VerifyAdministratorInformation()
368 : {
369 1 : ChipLogProgress(Controller, "JCM: Verify joint fabric administrator endpoint and fabric index");
370 :
371 1 : if (mInfo.adminEndpointId == kInvalidEndpointId)
372 : {
373 0 : ChipLogError(Controller, "JCM: Administrator endpoint ID not found!");
374 0 : return TrustVerificationError::kInvalidAdministratorEndpointId;
375 : }
376 :
377 1 : if (mInfo.adminFabricIndex == kUndefinedFabricIndex)
378 : {
379 0 : ChipLogError(Controller, "JCM: Administrator fabric index not found!");
380 0 : return TrustVerificationError::kInvalidAdministratorFabricIndex;
381 : }
382 :
383 1 : CATValues cats;
384 1 : auto nocSpan = mInfo.adminNOC.Span();
385 1 : CHIP_ERROR err = ExtractCATsFromOpCert(nocSpan, cats);
386 :
387 2 : if ((err != CHIP_NO_ERROR) || !cats.ContainsIdentifier(kAdminCATIdentifier))
388 : {
389 0 : return TrustVerificationError::kInvalidAdministratorCAT;
390 : }
391 :
392 1 : ChipLogProgress(Controller, "JCM: Administrator endpoint ID: %d", mInfo.adminEndpointId);
393 1 : ChipLogProgress(Controller, "JCM: Administrator fabric index: %d", mInfo.adminFabricIndex);
394 1 : ChipLogProgress(Controller, "JCM: Administrator vendor ID: %d", mInfo.adminVendorId);
395 1 : ChipLogProgress(Controller, "JCM: Administrator fabric ID: %llu", static_cast<unsigned long long>(mInfo.adminFabricId));
396 :
397 1 : return TrustVerificationError::kSuccess;
398 : }
399 :
400 2 : void DeviceCommissioner::OnVendorIdVerificationComplete(const CHIP_ERROR & err)
401 : {
402 2 : ContinueAfterVendorIDVerification(err);
403 2 : }
404 :
405 1 : CHIP_ERROR DeviceCommissioner::OnLookupOperationalTrustAnchor(VendorId vendorID, CertificateKeyId & subjectKeyId,
406 : ByteSpan & globallyTrustedRootSpan)
407 : {
408 1 : if (mTrustVerificationDelegate != nullptr)
409 : {
410 1 : return mTrustVerificationDelegate->OnLookupOperationalTrustAnchor(vendorID, subjectKeyId, globallyTrustedRootSpan);
411 : }
412 :
413 0 : return CHIP_ERROR_INTERNAL;
414 : }
415 :
416 2 : TrustVerificationError DeviceCommissioner::PerformVendorIDVerificationProcedure()
417 : {
418 3 : auto getSession = [this]() { return this->mDeviceProxy->GetSecureSession(); };
419 :
420 2 : CHIP_ERROR err = VerifyVendorId(mDeviceProxy->GetExchangeManager(), getSession, &mInfo);
421 4 : if (err != CHIP_NO_ERROR)
422 : {
423 0 : ChipLogError(Controller, "Failed to send Verify VendorId: %s", ErrorStr(err));
424 0 : ContinueAfterVendorIDVerification(err);
425 0 : return TrustVerificationError::kVendorIdVerificationFailed;
426 : }
427 :
428 2 : return TrustVerificationError::kAsync; // Indicate that this is an async operation
429 : }
430 :
431 2 : TrustVerificationError DeviceCommissioner::AskUserForConsent()
432 : {
433 2 : ChipLogProgress(Controller, "JCM: Asking user for consent");
434 2 : if (mTrustVerificationDelegate == nullptr)
435 : {
436 0 : ChipLogError(Controller, "JCM: TrustVerificationDelegate is not set");
437 0 : return TrustVerificationError::kTrustVerificationDelegateNotSet; // Indicate that the delegate is not set
438 : }
439 :
440 2 : mTrustVerificationDelegate->OnAskUserForConsent(*this, mInfo);
441 2 : return TrustVerificationError::kAsync; // Indicate that this is an async operation
442 : }
443 :
444 6 : void DeviceCommissioner::PerformTrustVerificationStage(const TrustVerificationStage & nextStage)
445 : {
446 6 : TrustVerificationError error = TrustVerificationError::kSuccess;
447 :
448 6 : switch (nextStage)
449 : {
450 1 : case TrustVerificationStage::kVerifyingAdministratorInformation:
451 1 : error = VerifyAdministratorInformation();
452 1 : break;
453 2 : case TrustVerificationStage::kPerformingVendorIDVerification:
454 2 : error = PerformVendorIDVerificationProcedure();
455 2 : break;
456 2 : case TrustVerificationStage::kAskingUserForConsent:
457 2 : error = AskUserForConsent();
458 2 : break;
459 1 : case TrustVerificationStage::kComplete:
460 1 : error = TrustVerificationError::kSuccess;
461 1 : break;
462 0 : case TrustVerificationStage::kError:
463 0 : error = TrustVerificationError::kInternalError;
464 0 : break;
465 0 : default:
466 0 : ChipLogError(Controller, "JCM: Invalid stage: %d", static_cast<int>(nextStage));
467 0 : error = TrustVerificationError::kInternalError;
468 0 : break;
469 : }
470 :
471 6 : if (error != TrustVerificationError::kAsync)
472 : {
473 2 : TrustVerificationStageFinished(nextStage, error);
474 : }
475 6 : }
476 :
477 6 : TrustVerificationStage DeviceCommissioner::GetNextTrustVerificationStage(const TrustVerificationStage & currentStage)
478 : {
479 6 : TrustVerificationStage nextStage = TrustVerificationStage::kIdle;
480 :
481 6 : switch (currentStage)
482 : {
483 1 : case TrustVerificationStage::kIdle:
484 1 : nextStage = TrustVerificationStage::kVerifyingAdministratorInformation;
485 1 : break;
486 2 : case TrustVerificationStage::kVerifyingAdministratorInformation:
487 2 : nextStage = TrustVerificationStage::kPerformingVendorIDVerification;
488 2 : break;
489 2 : case TrustVerificationStage::kPerformingVendorIDVerification:
490 2 : nextStage = TrustVerificationStage::kAskingUserForConsent;
491 2 : break;
492 1 : case TrustVerificationStage::kAskingUserForConsent:
493 1 : nextStage = TrustVerificationStage::kComplete;
494 1 : break;
495 0 : default:
496 0 : ChipLogError(Controller, "JCM: Invalid stage: %d", static_cast<int>(currentStage));
497 0 : nextStage = TrustVerificationStage::kError;
498 0 : break;
499 : }
500 :
501 6 : return nextStage;
502 : }
503 :
504 0 : void DeviceCommissioner::OnTrustVerificationComplete(TrustVerificationError error)
505 : {
506 0 : if (error == TrustVerificationError::kSuccess)
507 : {
508 0 : ChipLogProgress(Controller, "JCM: Administrator Device passed JCM Trust Verification");
509 :
510 0 : CommissioningStageComplete(CHIP_NO_ERROR);
511 : }
512 : else
513 : {
514 0 : ChipLogError(Controller, "JCM: Failed in verifying JCM Trust Verification: err %s", EnumToString(error).c_str());
515 :
516 0 : CommissioningDelegate::CommissioningReport report;
517 0 : report.Set<TrustVerificationError>(error);
518 :
519 0 : CommissioningStageComplete(CHIP_ERROR_INTERNAL, report);
520 0 : }
521 0 : }
522 :
523 0 : void DeviceCommissioner::CleanupCommissioning(DeviceProxy * proxy, NodeId nodeId, const CompletionStatus & completionStatus)
524 : {
525 0 : chip::Controller::DeviceCommissioner::CleanupCommissioning(proxy, nodeId, completionStatus);
526 :
527 0 : mInfo.Cleanup();
528 0 : }
529 :
530 0 : bool DeviceCommissioner::HasValidCommissioningMode(const Dnssd::CommissionNodeData & nodeData)
531 : {
532 :
533 : #if CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC
534 : if (GetCommissioningParameters().GetUseJCM().ValueOr(false))
535 : {
536 : if (nodeData.commissioningMode != to_underlying(Dnssd::CommissioningMode::kEnabledJointFabric))
537 : {
538 : ChipLogProgress(Controller, "Discovered device has a commissioning mode (%u) that is not supported by JCM.",
539 : static_cast<unsigned>(nodeData.commissioningMode));
540 : return false;
541 : }
542 : }
543 : else
544 : {
545 : return chip::Controller::DeviceCommissioner::HasValidCommissioningMode(nodeData);
546 : }
547 :
548 : return true;
549 : #else
550 0 : return false;
551 : #endif // CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC
552 : }
553 :
554 : } // namespace JCM
555 : } // namespace Controller
556 : } // namespace chip
|