diff --git a/scripts/tests/cirque_tests.sh b/scripts/tests/cirque_tests.sh index 9e6c7e3a45e736..03673318592c69 100755 --- a/scripts/tests/cirque_tests.sh +++ b/scripts/tests/cirque_tests.sh @@ -44,6 +44,7 @@ CIRQUE_TESTS=( "SplitCommissioningTest" "CommissioningFailureTest" "CommissioningFailureOnReportTest" + "CustomCommissioningTest" ) BOLD_GREEN_TEXT="\033[1;32m" diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn index 4f4c09cef32ad5..ff499e5a6c55c6 100644 --- a/src/controller/python/BUILD.gn +++ b/src/controller/python/BUILD.gn @@ -209,6 +209,9 @@ chip_python_wheel_action("chip-core") { "chip/clusters/Attribute.py", "chip/clusters/Command.py", "chip/clusters/__init__.py", + "chip/commissioning/__init__.py", + "chip/commissioning/commissioning_flow_blocks.py", + "chip/commissioning/pase.py", "chip/configuration/__init__.py", "chip/discovery/__init__.py", "chip/discovery/library_handle.py", @@ -270,6 +273,7 @@ chip_python_wheel_action("chip-core") { "chip.ble", "chip.ble.commissioning", "chip.configuration", + "chip.commissioning", "chip.clusters", "chip.utils", "chip.discovery", diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp index f0d9b1fa21dfdd..852f9ffc7083b9 100644 --- a/src/controller/python/OpCredsBinding.cpp +++ b/src/controller/python/OpCredsBinding.cpp @@ -36,6 +36,7 @@ #include #include +#include #include #include #include @@ -57,6 +58,8 @@ const chip::Credentials::AttestationTrustStore * GetTestFileAttestationTrustStor return &attestationTrustStore; } + +chip::Python::DummyOperationalCredentialsIssuer sDummyOperationalCredentialsIssuer; } // namespace namespace chip { @@ -319,6 +322,68 @@ void pychip_OnCommissioningStatusUpdate(chip::PeerId peerId, chip::Controller::C return sTestCommissioner.OnCommissioningStatusUpdate(peerId, stageCompleted, err); } +/** + * By using pychip_OpCreds_AllocateControllerForCustomCommissioningFlow, we will not using the commissioning flow from the + * Controller class. + * + */ +PyChipError pychip_OpCreds_AllocateControllerForCustomCommissioningFlow(chip::Controller::DeviceCommissioner ** outDevCtrl, + uint8_t * serializedEphemeralKey, + uint32_t serializedEphemeralKeyLen, uint8_t * noc, + uint32_t nocLen, uint8_t * icac, uint32_t icacLen, + uint8_t * rcac, uint32_t rcacLen, + chip::VendorId adminVendorId, bool enableServerInteractions) +{ + ChipLogDetail(Controller, "Creating New Device Controller"); + + auto devCtrl = std::make_unique(); + VerifyOrReturnError(devCtrl != nullptr, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + + chip::Crypto::P256Keypair ephemeralKey; + chip::Crypto::P256SerializedKeypair keyPair; + memcpy(keyPair.Bytes(), serializedEphemeralKey, serializedEphemeralKeyLen); + CHIP_ERROR err = ephemeralKey.Deserialize(keyPair); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + + ReturnErrorCodeIf(nocLen > Controller::kMaxCHIPDERCertLength, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + ReturnErrorCodeIf(icacLen > Controller::kMaxCHIPDERCertLength, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + ReturnErrorCodeIf(rcacLen > Controller::kMaxCHIPDERCertLength, ToPyChipError(CHIP_ERROR_NO_MEMORY)); + + Controller::SetupParams initParams; + initParams.pairingDelegate = &sPairingDelegate; + initParams.operationalCredentialsDelegate = &sDummyOperationalCredentialsIssuer; + initParams.operationalKeypair = &ephemeralKey; + initParams.controllerRCAC = ByteSpan(rcac, rcacLen); + initParams.controllerICAC = ByteSpan(icac, icacLen); + initParams.controllerNOC = ByteSpan(noc, nocLen); + initParams.enableServerInteractions = enableServerInteractions; + initParams.controllerVendorId = adminVendorId; + initParams.permitMultiControllerFabrics = true; + + err = Controller::DeviceControllerFactory::GetInstance().SetupCommissioner(initParams, *devCtrl); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + + // Setup IPK in Group Data Provider for controller after Commissioner init which sets-up the fabric table entry + uint8_t compressedFabricId[sizeof(uint64_t)] = { 0 }; + chip::MutableByteSpan compressedFabricIdSpan(compressedFabricId); + + err = devCtrl->GetCompressedFabricIdBytes(compressedFabricIdSpan); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + + ChipLogProgress(Support, "Setting up group data for Fabric Index %u with Compressed Fabric ID:", + static_cast(devCtrl->GetFabricIndex())); + ChipLogByteSpan(Support, compressedFabricIdSpan); + + chip::ByteSpan defaultIpk = chip::GroupTesting::DefaultIpkValue::GetDefaultIpk(); + err = + chip::Credentials::SetSingleIpkEpochKey(&sGroupDataProvider, devCtrl->GetFabricIndex(), defaultIpk, compressedFabricIdSpan); + VerifyOrReturnError(err == CHIP_NO_ERROR, ToPyChipError(err)); + + *outDevCtrl = devCtrl.release(); + + return ToPyChipError(CHIP_NO_ERROR); +} + PyChipError pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Controller::DeviceCommissioner ** outDevCtrl, FabricId fabricId, chip::NodeId nodeId, chip::VendorId adminVendorId, const char * paaTrustStorePath, bool useTestCommissioner, diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index 5a66aab1224a98..133d67afcf0ffa 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -198,10 +198,10 @@ def numTotalSessions(self) -> int: DiscoveryFilterType = discovery.FilterType -class ChipDeviceController(): +class ChipDeviceControllerBase(): activeList = set() - def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, nodeId: int, adminVendorId: int, catTags: typing.List[int] = [], paaTrustStorePath: str = "", useTestCommissioner: bool = False, fabricAdmin: FabricAdmin = None, name: str = None): + def __init__(self, name: str = ''): self.state = DCState.NOT_INITIALIZED self.devCtrl = None self._ChipStack = builtins.chipStack @@ -209,39 +209,14 @@ def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, nodeId: int, self._InitLib() - self._dmLib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback(_IssueNOCChainCallbackPythonCallback) - devCtrl = c_void_p(None) - c_catTags = (c_uint32 * len(catTags))() - - for i, item in enumerate(catTags): - c_catTags[i] = item - - self._dmLib.pychip_OpCreds_AllocateController.argtypes = [c_void_p, POINTER( - c_void_p), c_uint64, c_uint64, c_uint16, c_char_p, c_bool, c_bool, POINTER(c_uint32), c_uint32] - self._dmLib.pychip_OpCreds_AllocateController.restype = PyChipError - - # TODO(erjiaqing@): Figure out how to control enableServerInteractions for a single device controller (node) - self._ChipStack.Call( - lambda: self._dmLib.pychip_OpCreds_AllocateController(c_void_p( - opCredsContext), pointer(devCtrl), fabricId, nodeId, adminVendorId, c_char_p(None if len(paaTrustStorePath) == 0 else str.encode(paaTrustStorePath)), useTestCommissioner, self._ChipStack.enableServerInteractions, c_catTags, len(catTags)) - ).raise_on_error() - self.devCtrl = devCtrl - self._fabricAdmin = fabricAdmin - self._fabricId = fabricId - self._nodeId = nodeId - self._caIndex = fabricAdmin.caIndex - - if name is None: - self._name = "caIndex(%x)/fabricId(0x%016X)/nodeId(0x%016X)" % (fabricAdmin.caIndex, fabricId, nodeId) - else: - self._name = name self._Cluster = ChipClusters(builtins.chipStack) self._Cluster.InitLib(self._dmLib) + def _set_dev_ctrl(self, devCtrl): def HandleCommissioningComplete(nodeid, err): if err.is_success: print("Commissioning complete") @@ -283,20 +258,20 @@ def HandlePASEEstablishmentComplete(err: PyChipError): self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback( self.devCtrl, self.cbHandleCommissioningCompleteFunct) + self.devCtrl = devCtrl + self.state = DCState.IDLE self._isActive = True # Validate FabricID/NodeID followed from NOC Chain self._fabricId = self.GetFabricIdInternal() - assert self._fabricId == fabricId self._nodeId = self.GetNodeIdInternal() - assert self._nodeId == nodeId - ChipDeviceController.activeList.add(self) + def _finish_init(self): + self.state = DCState.IDLE + self._isActive = True - @property - def fabricAdmin(self) -> FabricAdmin: - return self._fabricAdmin + ChipDeviceController.activeList.add(self) @property def nodeId(self) -> int: @@ -306,10 +281,6 @@ def nodeId(self) -> int: def fabricId(self) -> int: return self._fabricId - @property - def caIndex(self) -> int: - return self._caIndex - @property def name(self) -> str: return self._name @@ -434,20 +405,6 @@ def EstablishPASESessionIP(self, ipaddr: str, setupPinCode: int, nodeid: int): self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid) ) - def Commission(self, nodeid): - self.CheckIsActive() - self._ChipStack.commissioningCompleteEvent.clear() - self.state = DCState.COMMISSIONING - - self._ChipStack.CallAsync( - lambda: self._dmLib.pychip_DeviceController_Commission( - self.devCtrl, nodeid) - ) - if not self._ChipStack.commissioningCompleteEvent.isSet(): - # Error 50 is a timeout - return False - return self._ChipStack.commissioningEventRes == 0 - def GetTestCommissionerUsed(self): return self._ChipStack.Call( lambda: self._dmLib.pychip_TestCommissionerUsed() @@ -472,116 +429,11 @@ def CheckTestCommissionerCallbacks(self): def CheckTestCommissionerPaseConnection(self, nodeid): return self._dmLib.pychip_TestPaseConnection(nodeid) - def CommissionOnNetwork(self, nodeId: int, setupPinCode: int, filterType: DiscoveryFilterType = DiscoveryFilterType.NONE, filter: typing.Any = None): - ''' - Does the routine for OnNetworkCommissioning, with a filter for mDNS discovery. - Supported filters are: - - DiscoveryFilterType.NONE - DiscoveryFilterType.SHORT_DISCRIMINATOR - DiscoveryFilterType.LONG_DISCRIMINATOR - DiscoveryFilterType.VENDOR_ID - DiscoveryFilterType.DEVICE_TYPE - DiscoveryFilterType.COMMISSIONING_MODE - DiscoveryFilterType.INSTANCE_NAME - DiscoveryFilterType.COMMISSIONER - DiscoveryFilterType.COMPRESSED_FABRIC_ID - - The filter can be an integer, a string or None depending on the actual type of selected filter. - ''' - self.CheckIsActive() - - # IP connection will run through full commissioning, so we need to wait - # for the commissioning complete event, not just any callback. - self.state = DCState.COMMISSIONING - - # Convert numerical filters to string for passing down to binding. - if isinstance(filter, int): - filter = str(filter) - - self._ChipStack.commissioningCompleteEvent.clear() - - self._ChipStack.CallAsync( - lambda: self._dmLib.pychip_DeviceController_OnNetworkCommission( - self.devCtrl, nodeId, setupPinCode, int(filterType), str(filter).encode("utf-8") + b"\x00" if filter is not None else None) - ) - if not self._ChipStack.commissioningCompleteEvent.isSet(): - # Error 50 is a timeout - return False - return self._ChipStack.commissioningEventRes == 0 - - def CommissionWithCode(self, setupPayload: str, nodeid: int): - self.CheckIsActive() - - setupPayload = setupPayload.encode() + b'\0' - - # IP connection will run through full commissioning, so we need to wait - # for the commissioning complete event, not just any callback. - self.state = DCState.COMMISSIONING - - self._ChipStack.commissioningCompleteEvent.clear() - - self._ChipStack.CallAsync( - lambda: self._dmLib.pychip_DeviceController_ConnectWithCode( - self.devCtrl, setupPayload, nodeid) - ) - if not self._ChipStack.commissioningCompleteEvent.isSet(): - # Error 50 is a timeout - return False - return self._ChipStack.commissioningEventRes == 0 - - def CommissionIP(self, ipaddr: str, setupPinCode: int, nodeid: int): - """ DEPRECATED, DO NOT USE! Use `CommissionOnNetwork` or `CommissionWithCode` """ - self.CheckIsActive() - - # IP connection will run through full commissioning, so we need to wait - # for the commissioning complete event, not just any callback. - self.state = DCState.COMMISSIONING - - self._ChipStack.commissioningCompleteEvent.clear() - - self._ChipStack.CallAsync( - lambda: self._dmLib.pychip_DeviceController_ConnectIP( - self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid) - ) - if not self._ChipStack.commissioningCompleteEvent.isSet(): - # Error 50 is a timeout - return False - return self._ChipStack.commissioningEventRes == 0 - def NOCChainCallback(self, nocChain): self._ChipStack.callbackRes = nocChain self._ChipStack.completeEvent.set() return - def CommissionThread(self, discriminator, setupPinCode, nodeId, threadOperationalDataset: bytes): - ''' Commissions a Thread device over BLE - ''' - self.SetThreadOperationalDataset(threadOperationalDataset) - return self.ConnectBLE(discriminator, setupPinCode, nodeId) - - def CommissionWiFi(self, discriminator, setupPinCode, nodeId, ssid: str, credentials: str): - ''' Commissions a WiFi device over BLE - ''' - self.SetWiFiCredentials(ssid, credentials) - return self.ConnectBLE(discriminator, setupPinCode, nodeId) - - def SetWiFiCredentials(self, ssid: str, credentials: str): - self.CheckIsActive() - - self._ChipStack.Call( - lambda: self._dmLib.pychip_DeviceController_SetWiFiCredentials( - ssid.encode("utf-8"), credentials.encode("utf-8")) - ).raise_on_error() - - def SetThreadOperationalDataset(self, threadOperationalDataset): - self.CheckIsActive() - - self._ChipStack.Call( - lambda: self._dmLib.pychip_DeviceController_SetThreadOperationalDataset( - threadOperationalDataset, len(threadOperationalDataset)) - ).raise_on_error() - def ResolveNode(self, nodeid): self.CheckIsActive() @@ -1250,16 +1102,6 @@ def SetBlockingCB(self, blockingCB): self._ChipStack.blockingCB = blockingCB - def IssueNOCChain(self, csr: Clusters.OperationalCredentials.Commands.CSRResponse, nodeId: int): - """Issue an NOC chain using the associated OperationalCredentialsDelegate. - The NOC chain will be provided in TLV cert format.""" - self.CheckIsActive() - - return self._ChipStack.CallAsync( - lambda: self._dmLib.pychip_DeviceController_IssueNOCChain( - self.devCtrl, py_object(self), csr.NOCSRElements, len(csr.NOCSRElements), nodeId) - ) - # ----- Private Members ----- def _InitLib(self): if self._dmLib is None: @@ -1427,3 +1269,203 @@ def _InitLib(self): self._dmLib.pychip_DeviceController_GetLogFilter = [None] self._dmLib.pychip_DeviceController_GetLogFilter = c_uint8 + + self._dmLib.pychip_OpCreds_AllocateController.argtypes = [c_void_p, POINTER( + c_void_p), c_uint64, c_uint64, c_uint16, c_char_p, c_bool, c_bool, POINTER(c_uint32), c_uint32] + self._dmLib.pychip_OpCreds_AllocateController.restype = PyChipError + + self._dmLib.pychip_OpCreds_AllocateControllerForCustomCommissioningFlow.argtypes = [ + POINTER(c_void_p), c_char_p, c_uint32, c_char_p, c_uint32, c_char_p, c_uint32, c_char_p, c_uint32, c_uint16, c_bool] + self._dmLib.pychip_OpCreds_AllocateControllerForCustomCommissioningFlow.restype = PyChipError + + +class ChipDeviceController(ChipDeviceControllerBase): + def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, nodeId: int, adminVendorId: int, catTags: typing.List[int] = [], paaTrustStorePath: str = "", useTestCommissioner: bool = False, fabricAdmin: FabricAdmin = None, name: str = None): + super().__init__("caIndex(%x)/fabricId(0x%016X)/nodeId(0x%016X)" % (fabricAdmin.caIndex, fabricId, nodeId) if name is None else name) + + self._dmLib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback(_IssueNOCChainCallbackPythonCallback) + + devCtrl = c_void_p(None) + + c_catTags = (c_uint32 * len(catTags))() + + for i, item in enumerate(catTags): + c_catTags[i] = item + + self._dmLib.pychip_OpCreds_AllocateController.argtypes = [c_void_p, POINTER( + c_void_p), c_uint64, c_uint64, c_uint16, c_char_p, c_bool, c_bool, POINTER(c_uint32), c_uint32] + self._dmLib.pychip_OpCreds_AllocateController.restype = PyChipError + + # TODO(erjiaqing@): Figure out how to control enableServerInteractions for a single device controller (node) + self._ChipStack.Call( + lambda: self._dmLib.pychip_OpCreds_AllocateController(c_void_p( + opCredsContext), pointer(devCtrl), fabricId, nodeId, adminVendorId, c_char_p(None if len(paaTrustStorePath) == 0 else str.encode(paaTrustStorePath)), useTestCommissioner, self._ChipStack.enableServerInteractions, c_catTags, len(catTags)) + ).raise_on_error() + + self._fabricAdmin = fabricAdmin + self._fabricId = fabricId + self._nodeId = nodeId + self._caIndex = fabricAdmin.caIndex + + self._set_dev_ctrl(devCtrl=devCtrl) + + self._finish_init() + + assert self._fabricId == fabricId + assert self._nodeId == nodeId + + @property + def caIndex(self) -> int: + return self._caIndex + + @property + def fabricAdmin(self) -> FabricAdmin: + return self._fabricAdmin + + def Commission(self, nodeid): + self.CheckIsActive() + self._ChipStack.commissioningCompleteEvent.clear() + self.state = DCState.COMMISSIONING + + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_Commission( + self.devCtrl, nodeid) + ) + if not self._ChipStack.commissioningCompleteEvent.isSet(): + # Error 50 is a timeout + return False + return self._ChipStack.commissioningEventRes == 0 + + def CommissionThread(self, discriminator, setupPinCode, nodeId, threadOperationalDataset: bytes): + ''' Commissions a Thread device over BLE + ''' + self.SetThreadOperationalDataset(threadOperationalDataset) + return self.ConnectBLE(discriminator, setupPinCode, nodeId) + + def CommissionWiFi(self, discriminator, setupPinCode, nodeId, ssid: str, credentials: str): + ''' Commissions a WiFi device over BLE + ''' + self.SetWiFiCredentials(ssid, credentials) + return self.ConnectBLE(discriminator, setupPinCode, nodeId) + + def SetWiFiCredentials(self, ssid: str, credentials: str): + self.CheckIsActive() + + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_SetWiFiCredentials( + ssid.encode("utf-8"), credentials.encode("utf-8")) + ).raise_on_error() + + def SetThreadOperationalDataset(self, threadOperationalDataset): + self.CheckIsActive() + + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_SetThreadOperationalDataset( + threadOperationalDataset, len(threadOperationalDataset)) + ).raise_on_error() + + def CommissionOnNetwork(self, nodeId: int, setupPinCode: int, filterType: DiscoveryFilterType = DiscoveryFilterType.NONE, filter: typing.Any = None): + ''' + Does the routine for OnNetworkCommissioning, with a filter for mDNS discovery. + Supported filters are: + + DiscoveryFilterType.NONE + DiscoveryFilterType.SHORT_DISCRIMINATOR + DiscoveryFilterType.LONG_DISCRIMINATOR + DiscoveryFilterType.VENDOR_ID + DiscoveryFilterType.DEVICE_TYPE + DiscoveryFilterType.COMMISSIONING_MODE + DiscoveryFilterType.INSTANCE_NAME + DiscoveryFilterType.COMMISSIONER + DiscoveryFilterType.COMPRESSED_FABRIC_ID + + The filter can be an integer, a string or None depending on the actual type of selected filter. + ''' + self.CheckIsActive() + + # IP connection will run through full commissioning, so we need to wait + # for the commissioning complete event, not just any callback. + self.state = DCState.COMMISSIONING + + # Convert numerical filters to string for passing down to binding. + if isinstance(filter, int): + filter = str(filter) + + self._ChipStack.commissioningCompleteEvent.clear() + + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_OnNetworkCommission( + self.devCtrl, nodeId, setupPinCode, int(filterType), str(filter).encode("utf-8") + b"\x00" if filter is not None else None) + ) + if not self._ChipStack.commissioningCompleteEvent.isSet(): + # Error 50 is a timeout + return False + return self._ChipStack.commissioningEventRes == 0 + + def CommissionWithCode(self, setupPayload: str, nodeid: int): + self.CheckIsActive() + + setupPayload = setupPayload.encode() + b'\0' + + # IP connection will run through full commissioning, so we need to wait + # for the commissioning complete event, not just any callback. + self.state = DCState.COMMISSIONING + + self._ChipStack.commissioningCompleteEvent.clear() + + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_ConnectWithCode( + self.devCtrl, setupPayload, nodeid) + ) + if not self._ChipStack.commissioningCompleteEvent.isSet(): + # Error 50 is a timeout + return False + return self._ChipStack.commissioningEventRes == 0 + + def CommissionIP(self, ipaddr: str, setupPinCode: int, nodeid: int): + """ DEPRECATED, DO NOT USE! Use `CommissionOnNetwork` or `CommissionWithCode` """ + self.CheckIsActive() + + # IP connection will run through full commissioning, so we need to wait + # for the commissioning complete event, not just any callback. + self.state = DCState.COMMISSIONING + + self._ChipStack.commissioningCompleteEvent.clear() + + self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_ConnectIP( + self.devCtrl, ipaddr.encode("utf-8"), setupPinCode, nodeid) + ) + if not self._ChipStack.commissioningCompleteEvent.isSet(): + # Error 50 is a timeout + return False + return self._ChipStack.commissioningEventRes == 0 + + def IssueNOCChain(self, csr: Clusters.OperationalCredentials.Commands.CSRResponse, nodeId: int): + """Issue an NOC chain using the associated OperationalCredentialsDelegate. + The NOC chain will be provided in TLV cert format.""" + self.CheckIsActive() + + return self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_IssueNOCChain( + self.devCtrl, py_object(self), csr.NOCSRElements, len(csr.NOCSRElements), nodeId) + ) + + +class BareChipDeviceController(ChipDeviceControllerBase): + ''' A bare device controller without commissioner support. + ''' + + def __init__(self, ephemeralKey: bytes, noc: bytes, icac: bytes, rcac: bytes, adminVendorId: int, name: str = None): + super().__init__(f"ctrl(v/{adminVendorId})" if name is None else name) + + devCtrl = c_void_p(None) + + self._ChipStack.Call( + lambda: self._dmLib.pychip_OpCreds_AllocateControllerForCustomCommissioningFlow( + c_void_p(devCtrl), ephemeralKey, len(ephemeralKey), noc, len(noc), icac, len(icac), rcac, len(rcac), adminVendorId, self._ChipStack.enableServerInteractions) + ).raise_on_error() + + self._set_dev_ctrl(devCtrl) + + self._finish_init() diff --git a/src/controller/python/chip/commissioning/DummyOperationalCredentialsIssuer.h b/src/controller/python/chip/commissioning/DummyOperationalCredentialsIssuer.h new file mode 100644 index 00000000000000..c16f9ca94a2589 --- /dev/null +++ b/src/controller/python/chip/commissioning/DummyOperationalCredentialsIssuer.h @@ -0,0 +1,71 @@ +/* + * + * Copyright (c) 2021-2022 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @file + * This file contains class definition of an example operational certificate + * issuer for CHIP devices. The class can be used as a guideline on how to + * construct your own certificate issuer. It can also be used in tests and tools + * if a specific signing authority is not required. + * + * NOTE: This class stores the encryption key in clear storage. This is not suited + * for production use. This should only be used in test tools. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace Python { + +class DLL_EXPORT DummyOperationalCredentialsIssuer : public Controller::OperationalCredentialsDelegate +{ +public: + // + // Constructor to create an instance of this object that vends out operational credentials for a given fabric. + // + // An index should be provided to numerically identify this instance relative to others in a multi-fabric deployment. This is + // needed given the interactions of this object with persistent storage. Consequently, the index is used to scope the entries + // read/written to/from storage. + // + // It is recommended that this index track the fabric index within which this issuer is operating. + // + DummyOperationalCredentialsIssuer() {} + ~DummyOperationalCredentialsIssuer() override {} + + CHIP_ERROR GenerateNOCChain(const ByteSpan & csrElements, const ByteSpan & csrNonce, const ByteSpan & attestationSignature, + const ByteSpan & attestationChallenge, const ByteSpan & DAC, const ByteSpan & PAI, + Callback::Callback * onCompletion) override + { + return CHIP_ERROR_NOT_IMPLEMENTED; + } + + void SetNodeIdForNextNOCRequest(NodeId nodeId) override {} + + void SetFabricIdForNextNOCRequest(FabricId fabricId) override {} +}; + +} // namespace Python +} // namespace chip diff --git a/src/controller/python/chip/commissioning/__init__.py b/src/controller/python/chip/commissioning/__init__.py new file mode 100644 index 00000000000000..68a0bdb92f9b40 --- /dev/null +++ b/src/controller/python/chip/commissioning/__init__.py @@ -0,0 +1,142 @@ +# +# Copyright (c) 2023 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from typing import * + +import abc +import dataclasses +import enum +import os + +from chip import clusters as Clusters + + +ROOT_ENDPOINT_ID = 0 + + +@dataclasses.dataclass +class CommissioneeInfo: + endpoints: Set[int] + is_thread_device: bool = False + is_wifi_device: bool = False + is_ethernet_device: bool = False + + +class RegulatoryLocationType(enum.IntEnum): + INDOOR = 0 + OUTDOOR = 1 + INDOOR_OUTDOOR = 2 + + +@dataclasses.dataclass +class RegulatoryConfig: + location_type: RegulatoryLocationType + country_code: str + + +@dataclasses.dataclass +class PaseParameters: + setup_pin: int + temporary_nodeid: int + + +@dataclasses.dataclass +class PaseOverBLEParameters(PaseParameters): + discriminator: int + + def __str__(self): + return f"BLE:0x{self.discriminator:03x}" + + +@dataclasses.dataclass +class PaseOverIPParameters(PaseParameters): + address: str + + def __str__(self): + return f"IP:{self.address}" + + +@dataclasses.dataclass +class WiFiCredentials: + ssid: bytes + passphrase: bytes + + +@dataclasses.dataclass +class Parameters: + pase_param: Union[PaseOverBLEParameters, PaseOverIPParameters] + regulatory_config: RegulatoryConfig + fabric_label: str + commissionee_info: CommissioneeInfo + wifi_credentials: WiFiCredentials + thread_credentials: bytes + failsafe_expiry_length_seconds: int = 600 + + +class NetworkCommissioningFeatureMap(enum.IntEnum): + WIFI_NETWORK_FEATURE_MAP = 1 + THREAD_NETWORK_FEATURE_MAP = 2 + + +class CommissionFailure(Exception): + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return f"CommissionFailure({self.msg})" + + +@dataclasses.dataclass +class GetCommissioneeCredentialsRequest: + dac: bytes + pai: bytes + attestation_signature: bytes + attestation_challenge: bytes + attestation_elements: bytes + csr_signature: bytes + csr_challenge: bytes + csr_elements: bytes + vendor_id: int + product_id: int + + +@dataclasses.dataclass +class GetCommissioneeCredentialsResponse: + rcac: bytes + noc: bytes + icac: bytes + ipk: bytes + case_admin_node: int + admin_vendor_id: int + node_id: int + fabric_id: int + + +class CredentialProvider: + async def get_attestation_nonce(self) -> bytes: + return os.urandom(32) + + async def get_csr_nonce(self) -> bytes: + return os.urandom(32) + + async def get_commissionee_credentials(self, request: GetCommissioneeCredentialsRequest) -> GetCommissioneeCredentialsResponse: + pass + + +class ExampleCredentialProvider: + async def get_commissionee_credentials(self, request: GetCommissioneeCredentialsRequest) -> GetCommissioneeCredentialsResponse: + pass diff --git a/src/controller/python/chip/commissioning/commissioning_flow_blocks.py b/src/controller/python/chip/commissioning/commissioning_flow_blocks.py new file mode 100644 index 00000000000000..b8f233a9295f17 --- /dev/null +++ b/src/controller/python/chip/commissioning/commissioning_flow_blocks.py @@ -0,0 +1,182 @@ +# +# Copyright (c) 2023 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from chip import ChipDeviceCtrl +from chip import commissioning +from chip import clusters as Clusters + +import logging + +from . import pase + + +class CommissioningFlowBlocks: + def __init__(self, devCtrl: ChipDeviceCtrl.ChipDeviceControllerBase, credential_provider: commissioning.CredentialProvider, logger: logging.Logger): + self._devCtrl = devCtrl + self._logger = logger + self._credential_provider = credential_provider + + async def arm_failsafe(self, parameter: commissioning.PaseParameters, device: pase.Session): + response = await self._devCtrl.SendCommand(device.node_id, commissioning.ROOT_ENDPOINT_ID, Clusters.GeneralCommissioning.Commands.ArmFailSafe( + expiryLengthSeconds=parameter.failsafe_expiry_length_seconds + )) + if response.errorCode != 0: + raise commissioning.CommissionFailure(repr(response)) + + async def operational_credentials_commissioning(self, parameter: commissioning.Parameters, device: pase.Session): + self._logger.info("Getting Remote Device Info") + device_info = (await self._devCtrl.ReadAttribute(device.node_id, [ + (commissioning.ROOT_ENDPOINT_ID, Clusters.BasicInformation.Attributes.VendorID), + (commissioning.ROOT_ENDPOINT_ID, Clusters.BasicInformation.Attributes.ProductID)], returnClusterObject=True))[commissioning.ROOT_ENDPOINT_ID][Clusters.BasicInformation] + + self._logger.info("Getting AttestationNonce") + attestation_nonce = await self._credential_provider.get_attestation_nonce() + + self._logger.info("Sending AttestationRequest") + attestation_elements = await self._devCtrl.SendCommand(device.node_id, commissioning.ROOT_ENDPOINT_ID, Clusters.OperationalCredentials.Commands.AttestationRequest( + attestationNonce=attestation_nonce + )) + + self._logger.info("Getting CertificateChain - DAC") + dac = await self._devCtrl.SendCommand(device.node_id, commissioning.ROOT_ENDPOINT_ID, Clusters.OperationalCredentials.Commands.CertificateChainRequest( + certificateType=1 + )) + + self._logger.info("Getting CertificateChain - PAI") + pai = await self._devCtrl.SendCommand(device.node_id, commissioning.ROOT_ENDPOINT_ID, Clusters.OperationalCredentials.Commands.CertificateChainRequest( + certificateType=2 + )) + + self._logger.info("Getting CSR Nonce") + csr_nonce = await self._credential_provider.get_csr_nonce() + + self._logger.info("Getting OpCSRRequest") + csr = await self._devCtrl.SendCommand(device.node_id, commissioning.ROOT_ENDPOINT_ID, Clusters.OperationalCredentials.Commands.CSRRequest( + CSRNonce=csr_nonce + )) + + self._logger.info("Getting device certificate") + commissionee_credentials = await self._credential_provider.get_commissionee_credentials( + commissioning.GetCommissioneeCredentialsRequest( + dac=dac, pai=pai, + attestation_challenge=attestation_nonce, + attestation_elements=attestation_elements.attestationElements, + attestation_signature=attestation_elements.signature, + csr_challenge=csr_nonce, + csr_elements=csr.NOCSRElements, + csr_signature=csr.attestationSignature, + vendor_id=device_info.vendorID, + product_id=device_info.productID)) + + self._logger.info("Adding Trusted Root Certificate") + response = await self._devCtrl.SendCommand(device.node_id, commissioning.ROOT_ENDPOINT_ID, Clusters.OperationalCredentials.Commands.AddTrustedRootCertificate( + rootCertificate=commissionee_credentials.rcac + )) + + self._logger.info("Adding Operational Certificate") + response = await self._devCtrl.SendCommand(device.node_id, commissioning.ROOT_ENDPOINT_ID, Clusters.OperationalCredentials.Commands.AddNOC( + NOCValue=commissionee_credentials.noc, + ICACValue=commissionee_credentials.icac, + IPKValue=commissionee_credentials.ipk, + caseAdminSubject=commissionee_credentials.case_admin_node, + adminVendorId=commissionee_credentials.admin_vendor_id + )) + if response.statusCode != 0: + raise commissioning.CommissionFailure(repr(response)) + + self._logger.info("Setting fabric label") + response = await self._devCtrl.SendCommand(device.node_id, commissioning.ROOT_ENDPOINT_ID, Clusters.OperationalCredentials.Commands.UpdateFabricLabel( + label=parameter.fabric_label + )) + if response.statusCode != 0: + raise commissioning.CommissionFailure(repr(response)) + + self._logger.info(f"Device peer id: {commissionee_credentials.fabric_id:016x}:{commissionee_credentials.node_id:016x}") + + return commissionee_credentials.node_id + + async def network_commissioning_thread(self, parameter: commissioning.Parameters, device: pase.Session): + if not parameter.thread_credentials: + raise TypeError("The device requires a Thread network dataset") + + self._logger.info("Adding Thread network") + response = await self._devCtrl.SendCommand(nodeid=device.node_id, endpoint=commissioning.ROOT_ENDPOINT_ID, payload=Clusters.NetworkCommissioning.Commands.AddOrUpdateThreadNetwork( + operationalDataset=parameter.thread_credentials)) + if response.networkingStatus != Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatus.kSuccess: + raise commissioning.CommissionFailure(f"Unexpected result for adding network: {response.networkingStatus}") + + network_list = (await self._devCtrl.ReadAttribute(nodeid=device.node_id, attributes=[(commissioning.ROOT_ENDPOINT_ID, Clusters.NetworkCommissioning.Attributes.Networks)], returnClusterObject=True))[commissioning.ROOT_ENDPOINT_ID][Clusters.NetworkCommissioning].networks + network_id = network_list[response.networkIndex].networkID + + self._logger.info("Enabling Thread network") + response = await self._devCtrl.SendCommand(nodeid=device.node_id, endpoint=commissioning.ROOT_ENDPOINT_ID, payload=Clusters.NetworkCommissioning.Commands.ConnectNetwork(networkID=network_id), interactionTimeoutMs=self._devCtrl.ComputeRoundTripTimeout(device.node_id, upperLayerProcessingTimeoutMs=30000)) + if response.networkingStatus != Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatus.kSuccess: + raise commissioning.CommissionFailure(f"Unexpected result for enabling network: {response.networkingStatus}") + + self._logger.info(f"Thread network commissioning finished") + + async def network_commissioning_wifi(self, parameter: commissioning.Parameters, device: 'ExampleCustomMatterCommissioningFlow.PASEContextManager.SessionProperty'): + if not parameter.wifi_credentials: + raise TypeError("The device requires WiFi credentials") + + self._logger.info("Adding WiFi network") + response = await self._devCtrl.SendCommand(nodeid=device.node_id, endpoint=commissioning.ROOT_ENDPOINT_ID, payload=Clusters.NetworkCommissioning.Commands.AddOrUpdateWiFiNetwork(ssid=parameter.wifi_credentials.ssid, credentials=parameter.wifi_credentials.passphrase)) + if response.networkingStatus != Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatus.kSuccess: + raise commissioning.CommissionFailure(f"Unexpected result for adding network: {response.networkingStatus}") + + network_list = (await self._devCtrl.ReadAttribute(nodeid=device.node_id, attributes=[(commissioning.ROOT_ENDPOINT_ID, Clusters.NetworkCommissioning.Attributes.Networks)], returnClusterObject=True))[commissioning.ROOT_ENDPOINT_ID][Clusters.NetworkCommissioning].networks + network_id = network_list[response.networkIndex].networkID + + self._logger.info("Enabling WiFi network") + response = await self._devCtrl.SendCommand(nodeid=device.node_id, endpoint=commissioning.ROOT_ENDPOINT_ID, payload=Clusters.NetworkCommissioning.Commands.ConnectNetwork(networkID=network_id), interactionTimeoutMs=self._devCtrl.ComputeRoundTripTimeout(device.node_id, upperLayerProcessingTimeoutMs=30000)) + if response.networkingStatus != Clusters.NetworkCommissioning.Enums.NetworkCommissioningStatus.kSuccess: + raise commissioning.CommissionFailure(f"Unexpected result for enabling network: {response.networkingStatus}") + + self._logger.info(f"WiFi network commissioning finished") + + async def network_commissioning(self, parameter: commissioning.Parameters, device: pase.Session): + clusters = await self._devCtrl.ReadAttribute(nodeid=device.node_id, attributes=[(Clusters.Descriptor.Attributes.ServerList)], returnClusterObject=True) + if Clusters.NetworkCommissioning.id not in clusters[commissioning.ROOT_ENDPOINT_ID][Clusters.Descriptor].serverList: + self._logger.info( + f"Network commissioning cluster {commissioning.ROOT_ENDPOINT_ID} is not enabled on this device.") + return + + feature_map = await self._devCtrl.ReadAttribute(nodeid=device.node_id, attributes=[(commissioning.ROOT_ENDPOINT_ID, Clusters.NetworkCommissioning.Attributes.FeatureMap)], returnClusterObject=True) + + if parameter.commissionee_info.is_wifi_device: + if feature_map[0][Clusters.NetworkCommissioning].featureMap != commissioning.NetworkCommissioningFeatureMap.WIFI_NETWORK_FEATURE_MAP: + raise AssertionError("Device is expected to be a WiFi device") + return await self.network_commissioning_wifi(parameter=parameter, device=device) + elif parameter.commissionee_info.is_thread_device: + if feature_map[0][Clusters.NetworkCommissioning].featureMap != commissioning.NetworkCommissioningFeatureMap.THREAD_NETWORK_FEATURE_MAP: + raise AssertionError("Device is expected to be a Thread device") + return await self.network_commissioning_thread(parameter=parameter, device=device) + + async def send_regulatory_config(self, parameter: commissioning.Parameters, device: 'ExampleCustomMatterCommissioningFlow.PASEContextManager.SessionProperty'): + self._logger.info("Sending Regulatory Config") + response = await self._devCtrl.SendCommand(device.node_id, commissioning.ROOT_ENDPOINT_ID, Clusters.GeneralCommissioning.Commands.SetRegulatoryConfig( + newRegulatoryConfig=Clusters.GeneralCommissioning.Enums.RegulatoryLocationType( + parameter.regulatory_config.location_type), + countryCode=parameter.regulatory_config.country_code + )) + if response.errorCode != 0: + raise commissioning.CommissionFailure(repr(response)) + + async def complete_commission(self, node_id: int): + response = await self._devCtrl.SendCommand(node_id, commissioning.ROOT_ENDPOINT_ID, Clusters.GeneralCommissioning.Commands.CommissioningComplete()) + if response.errorCode != 0: + raise commissioning.CommissionFailure(repr(response)) diff --git a/src/controller/python/chip/commissioning/pase.py b/src/controller/python/chip/commissioning/pase.py new file mode 100644 index 00000000000000..d74661ef456d85 --- /dev/null +++ b/src/controller/python/chip/commissioning/pase.py @@ -0,0 +1,55 @@ +# +# Copyright (c) 2023 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import dataclasses + +from chip import ChipDeviceCtrl +from chip import commissioning + + +@dataclasses.dataclass +class Session: + node_id: int + device: ChipDeviceCtrl.DeviceProxyWrapper + + +class ContextManager: + def __init__(self, devCtrl: ChipDeviceCtrl.ChipDeviceControllerBase, node_id: int, is_ble: bool): + self.devCtrl = devCtrl + self.node_id = node_id + self.is_ble = is_ble + + def __enter__(self) -> Session: + return Session( + node_id=self.node_id, + device=self.devCtrl.GetConnectedDeviceSync(self.node_id, True, 1000)) + + def __exit__(self, type, value, traceback): + self.devCtrl.CloseSession(self.node_id) + if self.is_ble: + self.devCtrl.CloseBLEConnection(self.is_ble) + + +def establish_session(devCtrl: ChipDeviceCtrl.ChipDeviceControllerBase, parameter: commissioning.PaseParameters) -> ContextManager: + if isinstance(parameter, commissioning.PaseOverBLEParameters): + devCtrl.EstablishPASESessionBLE(parameter.setup_pin, parameter.discriminator, parameter.temporary_nodeid) + elif isinstance(parameter, commissioning.PaseOverIPParameters): + devCtrl.EstablishPASESessionIP(parameter.address, parameter.setup_pin, parameter.temporary_nodeid) + else: + raise TypeError("Expect PaseOverBLEParameters or PaseOverIPParameters for establishing PASE session") + return ContextManager( + devCtrl=devCtrl, node_id=parameter.temporary_nodeid, is_ble=isinstance(parameter, commissioning.PaseOverBLEParameters)) diff --git a/src/controller/python/test/test_scripts/custom_commissioning_test.py b/src/controller/python/test/test_scripts/custom_commissioning_test.py new file mode 100755 index 00000000000000..23a2ce57675d32 --- /dev/null +++ b/src/controller/python/test/test_scripts/custom_commissioning_test.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 + +# +# Copyright (c) 2021 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Commissioning test. +import os +import sys +import random +from optparse import OptionParser +from base import TestFail, TestTimeout, BaseTestHelper, FailIfNot, logger +from cluster_objects import NODE_ID, ClusterObjectTests +from network_commissioning import NetworkCommissioningTests +import asyncio +import example_custom_commissioning_flow +from chip import commissioning +from chip import clusters as Clusters +from chip import ChipDeviceCtrl + +# The thread network dataset tlv for testing, splited into T-L-V. + +TEST_THREAD_NETWORK_DATASET_TLV = "0e080000000000010000" + \ + "000300000c" + \ + "35060004001fffe0" + \ + "0208fedcba9876543210" + \ + "0708fd00000000001234" + \ + "0510ffeeddccbbaa99887766554433221100" + \ + "030e54657374696e674e6574776f726b" + \ + "0102d252" + \ + "041081cb3b2efa781cc778397497ff520fa50c0302a0ff" +# Network id, for the thread network, current a const value, will be changed to XPANID of the thread network. +TEST_THREAD_NETWORK_ID = "fedcba9876543210" +TEST_DISCRIMINATOR = 3840 + +ENDPOINT_ID = 0 +LIGHTING_ENDPOINT_ID = 1 +GROUP_ID = 0 + + +def main(): + optParser = OptionParser() + optParser.add_option( + "-t", + "--timeout", + action="store", + dest="testTimeout", + default=75, + type='int', + help="The program will return with timeout after specified seconds.", + metavar="", + ) + optParser.add_option( + "--bad-cert-issuer", + action="store_true", + dest="badCertIssuer", + default=False, + help="Simulate a bad certificate issuer, the commissioning should fail when sending OpCreds.", + ) + optParser.add_option( + "-a", + "--address", + action="store", + dest="deviceAddress", + default='', + type='str', + help="Address of the device", + metavar="", + ) + optParser.add_option( + "--setup-payload", + action="store", + dest="setupPayload", + default='', + type='str', + help="Setup Payload (manual pairing code or QR code content)", + metavar="" + ) + optParser.add_option( + "--nodeid", + action="store", + dest="nodeid", + default=1, + type=int, + help="The Node ID issued to the device", + metavar="" + ) + optParser.add_option( + '--paa-trust-store-path', + dest="paaPath", + default='', + type='str', + help="Path that contains valid and trusted PAA Root Certificates." + ) + + (options, remainingArgs) = optParser.parse_args(sys.argv[1:]) + + timeoutTicker = TestTimeout(options.testTimeout) + timeoutTicker.start() + + test = BaseTestHelper( + nodeid=112233, paaTrustStorePath=options.paaPath, testCommissioner=True) + + class BadCredentialProvider: + def __init__(self, devCtrl: ChipDeviceCtrl.ChipDeviceController): + self._devCtrl = devCtrl + + async def get_attestation_nonce(self) -> bytes: + return os.urandom(32) + + async def get_csr_nonce(self) -> bytes: + return os.urandom(32) + + async def get_commissionee_credentials(self, request: commissioning.GetCommissioneeCredentialsRequest) -> commissioning.GetCommissioneeCredentialsResponse: + node_id = random.randint(100000, 999999) + nocChain = self._devCtrl.IssueNOCChain(Clusters.OperationalCredentials.Commands.CSRResponse( + NOCSRElements=request.csr_elements, attestationSignature=request.attestation_signature), nodeId=node_id) + return commissioning.GetCommissioneeCredentialsResponse( + rcac=nocChain.rcacBytes[1:], + noc=nocChain.nocBytes[1:], + icac=nocChain.icacBytes[1:], + ipk=nocChain.ipkBytes[1:], + case_admin_node=self._devCtrl.nodeId, + admin_vendor_id=self._devCtrl.fabricAdmin.vendorId, + node_id=node_id, + fabric_id=self._devCtrl.fabricId) + + flow = example_custom_commissioning_flow.ExampleCustomMatterCommissioningFlow( + devCtrl=test.devCtrl, + credential_provider=BadCredentialProvider( + test.devCtrl) if options.badCertIssuer else example_custom_commissioning_flow.ExampleCredentialProvider(test.devCtrl), + logger=logger) + + try: + asyncio.run(flow.commission(commissioning.Parameters( + pase_param=commissioning.PaseOverIPParameters( + setup_pin=20202021, temporary_nodeid=options.nodeid, address=options.deviceAddress + ), + regulatory_config=commissioning.RegulatoryConfig( + location_type=commissioning.RegulatoryLocationType.INDOOR_OUTDOOR, country_code='US'), + fabric_label="TestFabric", + commissionee_info=commissioning.CommissioneeInfo( + endpoints={}, + is_thread_device=True, + is_ethernet_device=False, + is_wifi_device=False, + ), + wifi_credentials=None, + thread_credentials=bytes.fromhex(TEST_THREAD_NETWORK_DATASET_TLV)))) + if options.badCertIssuer: + raise AssertionError("The commission is expected to fail. (BadCredentialProvider used)") + except Exception as ex: + if options.badCertIssuer: + logger.exception("Got exception and the test is expected to fail (BadCredentialProvider used)") + else: + raise ex + + timeoutTicker.stop() + + logger.info("Test finished") + + # TODO: Python device controller cannot be shutdown clean sometimes and will block on AsyncDNSResolverSockets shutdown. + # Call os._exit(0) to force close it. + os._exit(0) + + +if __name__ == "__main__": + try: + main() + except Exception as ex: + logger.exception(ex) + TestFail("Exception occurred when running tests.") diff --git a/src/controller/python/test/test_scripts/example_custom_commissioning_flow.py b/src/controller/python/test/test_scripts/example_custom_commissioning_flow.py new file mode 100644 index 00000000000000..a029a9fa45c6ef --- /dev/null +++ b/src/controller/python/test/test_scripts/example_custom_commissioning_flow.py @@ -0,0 +1,81 @@ +# +# Copyright (c) 2023 Project CHIP Authors +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from typing import * + +import dataclasses +import random +import logging +import os + +from chip import ChipDeviceCtrl +from chip import clusters as Clusters +from chip import commissioning +from chip.commissioning import pase, commissioning_flow_blocks + + +class ExampleCustomMatterCommissioningFlow(commissioning_flow_blocks.CommissioningFlowBlocks): + def __init__(self, devCtrl: ChipDeviceCtrl.ChipDeviceControllerBase, credential_provider: commissioning.CredentialProvider, logger: logging.Logger): + super().__init__(devCtrl=devCtrl, credential_provider=credential_provider, logger=logger) + self._logger = logger + + async def commission(self, parameter: commissioning.Parameters): + with pase.establish_session(devCtrl=self._devCtrl, parameter=parameter.pase_param) as device: + self._logger.info("Sending ArmFailSafe to device") + await self.arm_failsafe(parameter=parameter, device=device) + + self._logger.info("Setting Regulatory Configuration") + await self.send_regulatory_config(parameter=parameter, device=device) + + self._logger.info("OperationalCredentials Commissioning") + case_nodeid = await self.operational_credentials_commissioning(parameter=parameter, device=device) + + if not parameter.commissionee_info.is_ethernet_device: + self._logger.info("Network Commissioning") + await self.network_commissioning(parameter=parameter, device=device) + else: + self._logger.info(f"Device is an ethernet device, network commissioning not required.") + + self._logger.info("Completing Comissioning") + await self.complete_commission(case_nodeid) + + self._logger.info("Commissioning Completed") + + +class ExampleCredentialProvider: + def __init__(self, devCtrl: ChipDeviceCtrl.ChipDeviceController): + self._devCtrl = devCtrl + + async def get_attestation_nonce(self) -> bytes: + return os.urandom(32) + + async def get_csr_nonce(self) -> bytes: + return os.urandom(32) + + async def get_commissionee_credentials(self, request: commissioning.GetCommissioneeCredentialsRequest) -> commissioning.GetCommissioneeCredentialsResponse: + node_id = random.randint(100000, 999999) + nocChain = self._devCtrl.IssueNOCChain(Clusters.OperationalCredentials.Commands.CSRResponse( + NOCSRElements=request.csr_elements, attestationSignature=request.attestation_signature), nodeId=node_id) + return commissioning.GetCommissioneeCredentialsResponse( + rcac=nocChain.rcacBytes, + noc=nocChain.nocBytes, + icac=nocChain.icacBytes, + ipk=nocChain.ipkBytes, + case_admin_node=self._devCtrl.nodeId, + admin_vendor_id=self._devCtrl.fabricAdmin.vendorId, + node_id=node_id, + fabric_id=self._devCtrl.fabricId) diff --git a/src/test_driver/linux-cirque/CustomCommissioningTest.py b/src/test_driver/linux-cirque/CustomCommissioningTest.py new file mode 100755 index 00000000000000..acd94ef3f00a37 --- /dev/null +++ b/src/test_driver/linux-cirque/CustomCommissioningTest.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +Copyright (c) 2021 Project CHIP Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import logging +import os +import pprint +import time +import sys + +from helper.CHIPTestBase import CHIPVirtualHome + +logger = logging.getLogger('MobileDeviceTest') +logger.setLevel(logging.INFO) + +sh = logging.StreamHandler() +sh.setFormatter( + logging.Formatter( + '%(asctime)s [%(name)s] %(levelname)s %(message)s')) +logger.addHandler(sh) + +CHIP_PORT = 5540 + +CIRQUE_URL = "http://localhost:5000" +CHIP_REPO = os.path.join(os.path.abspath( + os.path.dirname(__file__)), "..", "..", "..") +TEST_EXTPANID = "fedcba9876543210" +TEST_DISCRIMINATOR = 3840 +TEST_DISCRIMINATOR2 = 3584 +MATTER_DEVELOPMENT_PAA_ROOT_CERTS = "credentials/development/paa-root-certs" + +DEVICE_CONFIG = { + 'device0': { + 'type': 'MobileDevice', + 'base_image': 'connectedhomeip/chip-cirque-device-base', + 'capability': ['TrafficControl', 'Mount'], + 'rcp_mode': True, + 'docker_network': 'Ipv6', + 'traffic_control': {'latencyMs': 100}, + "mount_pairs": [[CHIP_REPO, CHIP_REPO]], + }, + 'device1': { + 'type': 'CHIPEndDevice', + 'base_image': 'connectedhomeip/chip-cirque-device-base', + 'capability': ['Thread', 'TrafficControl', 'Mount'], + 'rcp_mode': True, + 'docker_network': 'Ipv6', + 'traffic_control': {'latencyMs': 100}, + "mount_pairs": [[CHIP_REPO, CHIP_REPO]], + }, + 'device2': { + 'type': 'CHIPEndDevice', + 'base_image': 'connectedhomeip/chip-cirque-device-base', + 'capability': ['Thread', 'TrafficControl', 'Mount'], + 'rcp_mode': True, + 'docker_network': 'Ipv6', + 'traffic_control': {'latencyMs': 100}, + "mount_pairs": [[CHIP_REPO, CHIP_REPO]], + } +} + + +class TestCommissioner(CHIPVirtualHome): + def __init__(self, device_config): + super().__init__(CIRQUE_URL, device_config) + self.logger = logger + + def setup(self): + self.initialize_home() + + def test_routine(self): + self.run_controller_test() + + def run_controller_test(self): + servers = [{ + "ip": device['description']['ipv6_addr'], + "id": device['id'] + } for device in self.non_ap_devices + if device['type'] == 'CHIPEndDevice'] + req_ids = [device['id'] for device in self.non_ap_devices + if device['type'] == 'MobileDevice'] + + servers[0]['discriminator'] = TEST_DISCRIMINATOR + servers[0]['nodeid'] = 1 + servers[1]['discriminator'] = TEST_DISCRIMINATOR2 + servers[1]['nodeid'] = 2 + + for server in servers: + self.execute_device_cmd(server['id'], "CHIPCirqueDaemon.py -- run gdb -return-child-result -q -ex \"set pagination off\" -ex run -ex \"bt 25\" --args {} --thread --discriminator {}".format( + os.path.join(CHIP_REPO, "out/debug/standalone/chip-all-clusters-app"), server['discriminator'])) + + self.reset_thread_devices([server['id'] for server in servers]) + + req_device_id = req_ids[0] + + self.execute_device_cmd(req_device_id, "pip3 install {}".format(os.path.join( + CHIP_REPO, "out/debug/linux_x64_gcc/controller/python/chip_clusters-0.0-py3-none-any.whl"))) + self.execute_device_cmd(req_device_id, "pip3 install {}".format(os.path.join( + CHIP_REPO, "out/debug/linux_x64_gcc/controller/python/chip_core-0.0-cp37-abi3-linux_x86_64.whl"))) + self.execute_device_cmd(req_device_id, "pip3 install {}".format(os.path.join( + CHIP_REPO, "out/debug/linux_x64_gcc/controller/python/chip_repl-0.0-py3-none-any.whl"))) + + command = "gdb -return-child-result -q -ex run -ex bt --args python3 {} -t 150 -a {} --paa-trust-store-path {} --nodeid {}".format( + os.path.join( + CHIP_REPO, "src/controller/python/test/test_scripts/custom_commissioning_test.py"), + servers[0]['ip'], + os.path.join(CHIP_REPO, MATTER_DEVELOPMENT_PAA_ROOT_CERTS), + servers[0]['nodeid']) + ret = self.execute_device_cmd(req_device_id, command) + + self.assertEqual(ret['return_code'], '0', + "Test failed: non-zero return code") + + command = "gdb -return-child-result -q -ex run -ex bt --args python3 {} -t 150 -a {} --paa-trust-store-path {} --nodeid {} --bad-cert-issuer".format( + os.path.join( + CHIP_REPO, "src/controller/python/test/test_scripts/custom_commissioning_test.py"), + servers[1]['ip'], + os.path.join(CHIP_REPO, MATTER_DEVELOPMENT_PAA_ROOT_CERTS), + servers[1]['nodeid']) + ret = self.execute_device_cmd(req_device_id, command) + + self.assertEqual(ret['return_code'], '0', + "Test failed: non-zero return code") + + +if __name__ == "__main__": + sys.exit(TestCommissioner(DEVICE_CONFIG).run_test())