From 49eeb9b6e9cefbf56d7d8528c37a88a201ba8346 Mon Sep 17 00:00:00 2001 From: Jerry Johns Date: Thu, 18 Aug 2022 09:48:09 -0700 Subject: [PATCH 1/3] CertificateAuthority + Manager support in Python (#21981) * CertificateAuthority + Manager support in Python This shifts the logic in the existing FabricAdmin that manages a given Root CA to its own CertificateAuthority class. This now permits a more spec-aligned structure that has a CertificateAuthorityManager that manages a set of CertificateAuthority instances, each associated with a single Root PK. Each of those manages a list of FabricAdmins adminstering a fabric within that CA, which in turn manage a list of ChipDeviceController instances within that fabric. These now permit passing in separate PersistentStorage instances so that it is more flexible/easier to sand-box each CA's storage constructs, which makes it easier to integrate with chip-tool's INI files. The PersistentStorage construct has been updated to permit both storage to file as well as just a 'soft' cache. * Review feedback --- scripts/tools/convert_ini.py | 70 +++-- src/controller/python/BUILD.gn | 1 + .../ChipDeviceController-ScriptBinding.cpp | 44 ++- src/controller/python/OpCredsBinding.cpp | 12 +- .../python/chip/CertificateAuthority.py | 291 ++++++++++++++++++ src/controller/python/chip/ChipDeviceCtrl.py | 8 +- src/controller/python/chip/ChipReplStartup.py | 85 ++--- src/controller/python/chip/ChipStack.py | 4 +- src/controller/python/chip/FabricAdmin.py | 183 ++++------- .../python/chip/storage/__init__.py | 162 +++++++--- .../python/test/test_scripts/base.py | 24 +- src/python_testing/TC_SC_3_6.py | 4 +- src/python_testing/matter_testing_support.py | 49 +-- 13 files changed, 610 insertions(+), 327 deletions(-) create mode 100644 src/controller/python/chip/CertificateAuthority.py diff --git a/scripts/tools/convert_ini.py b/scripts/tools/convert_ini.py index a8c17157ffd802..d1abd22c4498df 100755 --- a/scripts/tools/convert_ini.py +++ b/scripts/tools/convert_ini.py @@ -21,6 +21,8 @@ import click import typing import re +from os.path import exists +import logging def convert_ini_to_json(ini_dir: str, json_path: str): @@ -32,39 +34,55 @@ def convert_ini_to_json(ini_dir: str, json_path: str): """ python_json_store = {} - python_json_store['repl-config'] = { - 'fabricAdmins': { - '1': { - 'fabricId': 1, - 'vendorId': 65521 - }, - '2': { - 'fabricId': 2, - 'vendorId': 65521 - }, - '3': { - 'fabricId': 3, - 'vendorId': 65521 - } - } - } - - python_json_store['sdk-config'] = {} - - load_ini_into_dict(ini_file=ini_dir + '/chip_tool_config.alpha.ini', - json_dict=python_json_store['sdk-config'], replace_suffix='1') - load_ini_into_dict(ini_file=ini_dir + '/chip_tool_config.beta.ini', - json_dict=python_json_store['sdk-config'], replace_suffix='2') - load_ini_into_dict(ini_file=ini_dir + '/chip_tool_config.gamma.ini', - json_dict=python_json_store['sdk-config'], replace_suffix='3') + ini_file_paths = ['/chip_tool_config.alpha.ini', '/chip_tool_config.beta.ini', '/chip_tool_config.gamma.ini'] + counter = 1 + + for path in ini_file_paths: + full_path = ini_dir + path + if (exists(full_path)): + logging.critical(f"Found chip tool INI file at: {full_path} - Converting...") + create_repl_config_from_init(ini_file=full_path, + json_dict=python_json_store, replace_suffix=str(counter)) + counter = counter + 1 json_file = open(json_path, 'w') json.dump(python_json_store, json_file, ensure_ascii=True, indent=4) +def create_repl_config_from_init(ini_file: str, json_dict: typing.Dict, replace_suffix: str): + ''' This updates a provided JSON dictionary to create a REPL compliant configuration store that + contains the correct 'repl-config' and 'sdk-config' keys built from the provided chip-tool + INI file that contains the root public keys. The INI file will typically be named + with the word 'alpha', 'beta' or 'gamma' in the name. + + ini_file: Path to source INI file + json_dict: JSON dictionary to be updated. Multiple passes through this function using + the same dictionary is possible. + replace_suffix: The credentials in the INI file typically have keys that end with 0. This suffix + can be replaced with a different number. + ''' + if ('repl-config' not in json_dict): + json_dict['repl-config'] = {} + + if ('caList' not in json_dict['repl-config']): + json_dict['repl-config']['caList'] = {} + + json_dict['repl-config']['caList'][replace_suffix] = [ + {'fabricId': int(replace_suffix), 'vendorId': 0XFFF1} + ] + + if ('sdk-config' not in json_dict): + json_dict['sdk-config'] = {} + + load_ini_into_dict(ini_file=ini_file, json_dict=json_dict['sdk-config'], replace_suffix=replace_suffix) + + def load_ini_into_dict(ini_file: str, json_dict: typing.Dict, replace_suffix: str): - """ Loads the specific INI file into the provided dictionary. A 'replace_suffix' string + """ Loads the specific INI file containing CA credential information into the provided dictionary. A 'replace_suffix' string has to be provided to convert the existing numerical suffix to a different value. + + NOTE: This does not do any conversion of the keys into a format acceptable by the Python REPL environment. Please see + create_repl_config_from_init above if that is desired. """ config = ConfigParser() diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn index ab84ff86c864cf..21897af175f92d 100644 --- a/src/controller/python/BUILD.gn +++ b/src/controller/python/BUILD.gn @@ -185,6 +185,7 @@ chip_python_wheel_action("chip-core") { { src_dir = "." sources = [ + "chip/CertificateAuthority.py", "chip/ChipBleBase.py", "chip/ChipBleUtility.py", "chip/ChipBluezMgr.py", diff --git a/src/controller/python/ChipDeviceController-ScriptBinding.cpp b/src/controller/python/ChipDeviceController-ScriptBinding.cpp index 7a4189a52680d2..6e74ded2cd8a32 100644 --- a/src/controller/python/ChipDeviceController-ScriptBinding.cpp +++ b/src/controller/python/ChipDeviceController-ScriptBinding.cpp @@ -100,7 +100,6 @@ chip::Controller::CommissioningParameters sCommissioningParameters; chip::Controller::ScriptDevicePairingDelegate sPairingDelegate; chip::Controller::ScriptPairingDeviceDiscoveryDelegate sPairingDeviceDiscoveryDelegate; -chip::Controller::Python::StorageAdapter * sStorageAdapter = nullptr; chip::Credentials::GroupDataProviderImpl sGroupDataProvider; chip::Credentials::PersistentStorageOpCertStore sPersistentStorageOpCertStore; @@ -111,7 +110,7 @@ chip::NodeId kDefaultLocalDeviceId = chip::kTestControllerNodeId; chip::NodeId kRemoteDeviceId = chip::kTestDeviceNodeId; extern "C" { -ChipError::StorageType pychip_DeviceController_StackInit(); +ChipError::StorageType pychip_DeviceController_StackInit(Controller::Python::StorageAdapter * storageAdapter); ChipError::StorageType pychip_DeviceController_StackShutdown(); ChipError::StorageType pychip_DeviceController_NewDeviceController(chip::Controller::DeviceCommissioner ** outDevCtrl, @@ -205,43 +204,40 @@ chip::ChipError::StorageType pychip_InteractionModel_ShutdownSubscription(Subscr // // Storage // -void pychip_Storage_InitializeStorageAdapter(chip::Controller::Python::PyObject * context, - chip::Controller::Python::SyncSetKeyValueCb setCb, - chip::Controller::Python::SetGetKeyValueCb getCb, - chip::Controller::Python::SyncDeleteKeyValueCb deleteCb); -void pychip_Storage_ShutdownAdapter(); +void * pychip_Storage_InitializeStorageAdapter(chip::Controller::Python::PyObject * context, + chip::Controller::Python::SyncSetKeyValueCb setCb, + chip::Controller::Python::SetGetKeyValueCb getCb, + chip::Controller::Python::SyncDeleteKeyValueCb deleteCb); +void pychip_Storage_ShutdownAdapter(chip::Controller::Python::StorageAdapter * storageAdapter); } -void pychip_Storage_InitializeStorageAdapter(chip::Controller::Python::PyObject * context, - chip::Controller::Python::SyncSetKeyValueCb setCb, - chip::Controller::Python::SetGetKeyValueCb getCb, - chip::Controller::Python::SyncDeleteKeyValueCb deleteCb) +void * pychip_Storage_InitializeStorageAdapter(chip::Controller::Python::PyObject * context, + chip::Controller::Python::SyncSetKeyValueCb setCb, + chip::Controller::Python::SetGetKeyValueCb getCb, + chip::Controller::Python::SyncDeleteKeyValueCb deleteCb) { - sStorageAdapter = new chip::Controller::Python::StorageAdapter(context, setCb, getCb, deleteCb); + auto ptr = new chip::Controller::Python::StorageAdapter(context, setCb, getCb, deleteCb); + return ptr; } -void pychip_Storage_ShutdownAdapter() +void pychip_Storage_ShutdownAdapter(chip::Controller::Python::StorageAdapter * storageAdapter) { - delete sStorageAdapter; + delete storageAdapter; } -chip::Controller::Python::StorageAdapter * pychip_Storage_GetStorageAdapter() +ChipError::StorageType pychip_DeviceController_StackInit(Controller::Python::StorageAdapter * storageAdapter) { - return sStorageAdapter; -} - -ChipError::StorageType pychip_DeviceController_StackInit() -{ - VerifyOrDie(sStorageAdapter != nullptr); + VerifyOrDie(storageAdapter != nullptr); FactoryInitParams factoryParams; - factoryParams.fabricIndependentStorage = sStorageAdapter; - sGroupDataProvider.SetStorageDelegate(sStorageAdapter); + factoryParams.fabricIndependentStorage = storageAdapter; + + sGroupDataProvider.SetStorageDelegate(storageAdapter); ReturnErrorOnFailure(sGroupDataProvider.Init().AsInteger()); factoryParams.groupDataProvider = &sGroupDataProvider; - ReturnErrorOnFailure(sPersistentStorageOpCertStore.Init(sStorageAdapter).AsInteger()); + ReturnErrorOnFailure(sPersistentStorageOpCertStore.Init(storageAdapter).AsInteger()); factoryParams.opCertStore = &sPersistentStorageOpCertStore; factoryParams.enableServerInteractions = true; diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp index a17f726a314bc4..fa779e3d383fc0 100644 --- a/src/controller/python/OpCredsBinding.cpp +++ b/src/controller/python/OpCredsBinding.cpp @@ -97,7 +97,6 @@ class OperationalCredentialsAdapter : public OperationalCredentialsDelegate } // namespace Controller } // namespace chip -extern chip::Controller::Python::StorageAdapter * pychip_Storage_GetStorageAdapter(); extern chip::Credentials::GroupDataProviderImpl sGroupDataProvider; extern chip::Controller::ScriptDevicePairingDelegate sPairingDelegate; @@ -291,17 +290,13 @@ struct OpCredsContext void * mPyContext; }; -void * pychip_OpCreds_InitializeDelegate(void * pyContext, uint32_t fabricCredentialsIndex) +void * pychip_OpCreds_InitializeDelegate(void * pyContext, uint32_t fabricCredentialsIndex, + Controller::Python::StorageAdapter * storageAdapter) { auto context = Platform::MakeUnique(); context->mAdapter = Platform::MakeUnique(fabricCredentialsIndex); - if (pychip_Storage_GetStorageAdapter() == nullptr) - { - return nullptr; - } - - if (context->mAdapter->Initialize(*pychip_Storage_GetStorageAdapter()) != CHIP_NO_ERROR) + if (context->mAdapter->Initialize(*storageAdapter) != CHIP_NO_ERROR) { return nullptr; } @@ -339,6 +334,7 @@ ChipError::StorageType pychip_OpCreds_AllocateController(OpCredsContext * contex { paaTrustStorePath = "./credentials/development/paa-root-certs"; } + ChipLogProgress(Support, "Using device attestation PAA trust store path %s.", paaTrustStorePath); // Initialize device attestation verifier diff --git a/src/controller/python/chip/CertificateAuthority.py b/src/controller/python/chip/CertificateAuthority.py new file mode 100644 index 00000000000000..aa9011fb60a250 --- /dev/null +++ b/src/controller/python/chip/CertificateAuthority.py @@ -0,0 +1,291 @@ +# +# 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. +# + +# Needed to use types in type hints before they are fully defined. +from __future__ import annotations + +import ctypes +from dataclasses import dataclass, field +from typing import * +from ctypes import * +from rich.pretty import pprint +import json +import logging +import builtins +import base64 +import chip.exceptions +from chip import ChipDeviceCtrl +from chip import ChipStack +from chip import FabricAdmin +from chip.storage import PersistentStorage + + +class CertificateAuthority: + ''' This represents an operational Root Certificate Authority (CA) with a root key key pair with associated public key (i.e "Root PK") . This manages + a set of FabricAdmin objects, each administering a fabric identified by a unique FabricId scoped to it. + + Each CertificateAuthority instance is tied to a 'CA index' that is used to look-up the list of fabrics already setup previously + in the provided PersistentStorage object. + + >> C++ Binding Details + + Each CertificateAuthority instance is associated with a single instance of the OperationalCredentialsAdapter. This adapter instance implements + the OperationalCredentialsDelegate and is meant to provide a Python adapter to the functions in that delegate. It relies on the in-built + ExampleOperationalCredentialsIssuer to then generate certificate material for the CA. This instance also uses the 'CA index' to + store/look-up the associated credential material from the provided PersistentStorage object. + ''' + @classmethod + def _Handle(cls): + return chip.native.GetLibraryHandle() + + @classmethod + def logger(cls): + return logging.getLogger('CertificateAuthority') + + def __init__(self, chipStack: ChipStack.ChipStack, caIndex: int, persistentStorage: PersistentStorage = None): + ''' Initializes the CertificateAuthority. This will set-up the associated C++ OperationalCredentialsAdapter + as well. + + Arguments: + chipStack: A reference to a chip.ChipStack object. + caIndex: An index used to look-up details about stored credential material and fabrics from persistent storage. + persistentStorage: An optional reference to a PersistentStorage object. If one is provided, it will pick that over + the default PersistentStorage object retrieved from the chipStack. + ''' + self.logger().warning(f"New CertificateAuthority at index {caIndex}") + + self._chipStack = chipStack + self._caIndex = caIndex + + self._Handle().pychip_OpCreds_InitializeDelegate.restype = c_void_p + self._Handle().pychip_OpCreds_InitializeDelegate.argtypes = [ctypes.py_object, ctypes.c_uint32, ctypes.c_void_p] + + if (persistentStorage is None): + persistentStorage = self._chipStack.GetStorageManager() + + self._persistentStorage = persistentStorage + + self._closure = self._chipStack.Call( + lambda: self._Handle().pychip_OpCreds_InitializeDelegate( + ctypes.py_object(self), ctypes.c_uint32(self._caIndex), self._persistentStorage.GetSdkStorageObject()) + ) + + if (self._closure is None): + raise ValueError("Encountered error initializing OpCreds adapter") + + self._isActive = True + self._activeAdmins = [] + + def LoadFabricAdminsFromStorage(self): + ''' If FabricAdmins had been setup previously, this re-creates them using information from persistent storage. + Otherwise, it initializes the REPL keys in persistent storage to sane defaults. This includes a top-level + key identifying the CA (using the associated CA Index) initialized to an empty list. + + This expects a 'caList' key to be present in the REPL config. + + Each FabricAdmin that is added there-after will insert a dictionary item into that list containing + 'fabricId' and 'vendorId' keys. + ''' + if (not(self._isActive)): + raise RuntimeError("Object isn't active") + + self.logger().warning("Loading fabric admins from storage...") + + caList = self._persistentStorage.GetReplKey(key='caList') + if (str(self._caIndex) not in caList): + caList[str(self._caIndex)] = [] + self._persistentStorage.SetReplKey(key='caList', value=caList) + + fabricAdminMetadataList = self._persistentStorage.GetReplKey(key='caList')[str(self._caIndex)] + for adminMetadata in fabricAdminMetadataList: + self.NewFabricAdmin(vendorId=int(adminMetadata['vendorId']), fabricId=int(adminMetadata['fabricId'])) + + def NewFabricAdmin(self, vendorId: int, fabricId: int): + ''' Creates a new FabricAdmin object initialized with the provided vendorId and fabricId values. + + This will update the REPL keys in persistent storage IF a 'caList' key is present. If it isn't, + will avoid making any updates. + ''' + if (not(self._isActive)): + raise RuntimeError( + f"CertificateAuthority object was previously shutdown and is no longer valid!") + + if (vendorId is None or fabricId is None): + raise ValueError(f"Invalid values for fabricId and vendorId") + + for existingAdmin in self._activeAdmins: + if (existingAdmin.fabricId == fabricId): + raise ValueError(f"Provided fabricId of {fabricId} collides with an existing FabricAdmin instance!") + + fabricAdmin = FabricAdmin.FabricAdmin(self, vendorId=vendorId, fabricId=fabricId) + + caList = self._persistentStorage.GetReplKey('caList') + if (caList is not None): + replFabricEntry = {'fabricId': fabricId, 'vendorId': vendorId} + + if (replFabricEntry not in caList[str(self._caIndex)]): + caList[str(self._caIndex)].append(replFabricEntry) + + self._persistentStorage.SetReplKey(key='caList', value=caList) + + self._activeAdmins.append(fabricAdmin) + + return fabricAdmin + + def Shutdown(self): + ''' Shuts down all active FabricAdmin objects managed by this CertificateAuthority before + shutting itself down. + + You cannot interact with this object there-after. + ''' + if (self._isActive): + for admin in self._activeAdmins: + admin.Shutdown() + + self._activeAdmins = [] + self._Handle().pychip_OpCreds_FreeDelegate.argtypes = [ctypes.c_void_p] + self._chipStack.Call( + lambda: self._Handle().pychip_OpCreds_FreeDelegate( + ctypes.c_void_p(self._closure)) + ) + + self._isActive = False + + def GetOpCredsContext(self): + ''' Returns a pointer to the underlying C++ OperationalCredentialsAdapter. + ''' + if (not(self._isActive)): + raise RuntimeError("Object isn't active") + + return self._closure + + @property + def caIndex(self) -> int: + return self._caIndex + + @property + def adminList(self) -> list[FabricAdmin.FabricAdmin]: + return self._activeAdmins + + def __del__(self): + self.Shutdown() + + +class CertificateAuthorityManager: + ''' Manages a set of CertificateAuthority instances. + ''' + @classmethod + def _Handle(cls): + return chip.native.GetLibraryHandle() + + @classmethod + def logger(cls): + return logging.getLogger('CertificateAuthorityManager') + + def __init__(self, chipStack: ChipStack.ChipStack, persistentStorage: PersistentStorage = None): + ''' Initializes the manager. + + chipStack: Reference to a chip.ChipStack object that is used to initialize + CertificateAuthority instances. + + persistentStorage: If provided, over-rides the default instance in the provided chipStack + when initializing CertificateAuthority instances. + ''' + self._activeCaIndexList = [] + self._chipStack = chipStack + + if (persistentStorage is None): + persistentStorage = self._chipStack.GetStorageManager() + + self._persistentStorage = persistentStorage + self._activeCaList = [] + self._isActive = True + + def _AllocateNextCaIndex(self): + ''' Allocate the next un-used CA index. + ''' + nextCaIndex = 1 + for ca in self._activeCaList: + nextCaIndex = ca.caIndex + 1 + return nextCaIndex + + def LoadAuthoritiesFromStorage(self): + ''' Loads any existing CertificateAuthority instances present in persistent storage. + If the 'caList' key is not present in the REPL config, it will create one. + ''' + if (not(self._isActive)): + raise RuntimeError("Object is not active") + + self.logger().warning("Loading certificate authorities from storage...") + + # + # Persist details to storage (read modify write). + # + caList = self._persistentStorage.GetReplKey('caList') + if (caList is None): + caList = {} + + for caIndex in caList: + ca = self.NewCertificateAuthority(int(caIndex)) + ca.LoadFabricAdminsFromStorage() + + def NewCertificateAuthority(self, caIndex: int = None): + ''' Creates a new CertificateAuthority instance with the provided CA Index and the PersistentStorage + instance previously setup in the constructor. + + This will write to the REPL keys in persistent storage to setup an empty list for the 'CA Index' + item. + ''' + if (not(self._isActive)): + raise RuntimeError("Object is not active") + + if (caIndex is None): + caIndex = self._AllocateNextCaIndex() + + # + # Persist details to storage (read modify write). + # + caList = self._persistentStorage.GetReplKey('caList') + if (caList is None): + caList = {} + + if (str(caIndex) not in caList): + caList[str(caIndex)] = [] + self._persistentStorage.SetReplKey(key='caList', value=caList) + + ca = CertificateAuthority(chipStack=self._chipStack, caIndex=caIndex, persistentStorage=self._persistentStorage) + self._activeCaList.append(ca) + + return ca + + def Shutdown(self): + ''' Shuts down all active CertificateAuthority instances tracked by this manager, before shutting itself down. + + You cannot interact with this object there-after. + ''' + for ca in self._activeCaList: + ca.Shutdown() + + self._activeCaList = [] + self._isActive = False + + @property + def activeCaList(self) -> List[CertificateAuthority]: + return self._activeCaList + + def __del__(self): + self.Shutdown() diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index 30064a1695ace7..edac6bc8f516dc 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -181,10 +181,10 @@ def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, nodeId: int, self._fabricAdmin = fabricAdmin self._fabricId = fabricId self._nodeId = nodeId - self._adminIndex = fabricAdmin.adminIndex + self._caIndex = fabricAdmin.caIndex if name is None: - self._name = "adminIndex(%x)/fabricId(0x%016X)/nodeId(0x%016X)" % (fabricAdmin.adminIndex, fabricId, nodeId) + self._name = "caIndex(%x)/fabricId(0x%016X)/nodeId(0x%016X)" % (fabricAdmin.caIndex, fabricId, nodeId) else: self._name = name @@ -258,8 +258,8 @@ def fabricId(self) -> int: return self._fabricId @property - def adminIndex(self) -> int: - return self._adminIndex + def caIndex(self) -> int: + return self._caIndex @property def name(self) -> str: diff --git a/src/controller/python/chip/ChipReplStartup.py b/src/controller/python/chip/ChipReplStartup.py index cb38cc37032c92..519c292ba9d9d8 100644 --- a/src/controller/python/chip/ChipReplStartup.py +++ b/src/controller/python/chip/ChipReplStartup.py @@ -12,6 +12,7 @@ import argparse import builtins import chip.FabricAdmin +import chip.CertificateAuthority import chip.native from chip.utils import CommissioningBuildingBlocks import atexit @@ -19,60 +20,6 @@ _fabricAdmins = None -def LoadFabricAdmins(): - global _fabricAdmins - - # - # Shutdown any fabric admins we had before as well as active controllers. This ensures we - # relinquish some resources if this is called multiple times (e.g in a Jupyter notebook) - # - chip.FabricAdmin.FabricAdmin.ShutdownAll() - ChipDeviceCtrl.ChipDeviceController.ShutdownAll() - - _fabricAdmins = [] - storageMgr = builtins.chipStack.GetStorageManager() - - console = Console() - - try: - adminList = storageMgr.GetReplKey('fabricAdmins') - except KeyError: - console.print( - "\n[purple]No previous fabric admins discovered in persistent storage - creating a new one...") - - # - # Initialite a FabricAdmin with a VendorID of TestVendor1 (0xfff1) - # - _fabricAdmins.append(chip.FabricAdmin.FabricAdmin(0XFFF1)) - return _fabricAdmins - - console.print('\n') - - for k in adminList: - console.print( - f"[purple]Restoring FabricAdmin from storage to manage FabricId {adminList[k]['fabricId']}, AdminIndex {k}...") - _fabricAdmins.append(chip.FabricAdmin.FabricAdmin(vendorId=int(adminList[k]['vendorId']), - fabricId=adminList[k]['fabricId'], adminIndex=int(k))) - - console.print( - '\n[blue]Fabric Admins have been loaded and are available at [red]fabricAdmins') - return _fabricAdmins - - -def CreateDefaultDeviceController(): - global _fabricAdmins - - if (len(_fabricAdmins) == 0): - raise RuntimeError("Was called before calling LoadFabricAdmins()") - - console = Console() - - console.print('\n') - console.print( - f"[purple]Creating default device controller on fabric {_fabricAdmins[0]._fabricId}...") - return _fabricAdmins[0].NewController() - - def ReplInit(debug): # # Install the pretty printer that rich provides to replace the existing @@ -105,9 +52,11 @@ def ReplInit(debug): logging.getLogger().setLevel(logging.WARN) +certificateAuthorityManager = None + + def StackShutdown(): - chip.FabricAdmin.FabricAdmin.ShutdownAll() - ChipDeviceCtrl.ChipDeviceController.ShutdownAll() + certificateAuthorityManager.Shutdown() builtins.chipStack.Shutdown() @@ -145,12 +94,30 @@ def mattersetdebug(enableDebugMode: bool = True): ReplInit(args.debug) chipStack = ChipStack(persistentStoragePath=args.storagepath) -fabricAdmins = LoadFabricAdmins() -devCtrl = CreateDefaultDeviceController() +certificateAuthorityManager = chip.CertificateAuthority.CertificateAuthorityManager(chipStack, chipStack.GetStorageManager()) +certificateAuthorityManager.LoadAuthoritiesFromStorage() + +if (len(certificateAuthorityManager.activeCaList) == 0): + ca = certificateAuthorityManager.NewCertificateAuthority() + ca.NewFabricAdmin(vendorId=0xFFF1, fabricId=1) +elif (len(certificateAuthorityManager.activeCaList[0].adminList) == 0): + certificateAuthorityManager.activeCaList[0].NewFabricAdmin(vendorId=0xFFF1, fabricId=1) + +caList = certificateAuthorityManager.activeCaList + +devCtrl = caList[0].adminList[0].NewController() builtins.devCtrl = devCtrl atexit.register(StackShutdown) console.print( - '\n\n[blue]Default CHIP Device Controller has been initialized to manage [bold red]fabricAdmins[0][blue], and is available as [bold red]devCtrl') + '\n\n[blue]The following objects have been created:') + +console.print( + '''\t[red]certificateAuthorityManager[blue]:\tManages a list of CertificateAuthority instances. +\t[red]caList[blue]:\t\t\t\tThe list of CertificateAuthority instances. +\t[red]caList\[n]\[m][blue]:\t\t\tA specific FabricAdmin object at index m for the nth CertificateAuthority instance.''') + +console.print( + f'\n\n[blue]Default CHIP Device Controller (NodeId: {devCtrl.nodeId}): has been initialized to manage [bold red]caList[0].adminList[0][blue] (FabricId = {caList[0].adminList[0].fabricId}), and is available as [bold red]devCtrl') diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py index 5120e68fb8b3b2..cfdeeaef9a5785 100644 --- a/src/controller/python/chip/ChipStack.py +++ b/src/controller/python/chip/ChipStack.py @@ -266,7 +266,7 @@ def HandleChipThreadRun(callback): self._persistentStorage = PersistentStorage(persistentStoragePath) # Initialize the chip stack. - res = self._ChipStackLib.pychip_DeviceController_StackInit() + res = self._ChipStackLib.pychip_DeviceController_StackInit(self._persistentStorage.GetSdkStorageObject()) if res != 0: raise self.ErrorToException(res) @@ -440,7 +440,7 @@ def _loadLib(self): self._ChipStackLib = chip.native.GetLibraryHandle() self._chipDLLPath = chip.native.FindNativeLibraryPath() - self._ChipStackLib.pychip_DeviceController_StackInit.argtypes = [] + self._ChipStackLib.pychip_DeviceController_StackInit.argtypes = [c_void_p] self._ChipStackLib.pychip_DeviceController_StackInit.restype = c_uint32 self._ChipStackLib.pychip_DeviceController_StackShutdown.argtypes = [] self._ChipStackLib.pychip_DeviceController_StackShutdown.restype = c_uint32 diff --git a/src/controller/python/chip/FabricAdmin.py b/src/controller/python/chip/FabricAdmin.py index d44175819ba16a..216da4ebad7646 100644 --- a/src/controller/python/chip/FabricAdmin.py +++ b/src/controller/python/chip/FabricAdmin.py @@ -30,70 +30,29 @@ import chip.exceptions from chip import ChipDeviceCtrl import copy +from .storage import PersistentStorage +from chip.CertificateAuthority import CertificateAuthority class FabricAdmin: - ''' Administers a specific fabric as identified by the tuple of RCAC subject public key and Fabric ID. - The Fabric ID can be passed into the constructor while the RCAC and ICAC are generated. - The Fabric ID *does not* have to be unique across multiple FabricAdmin instances as - it is scoped to the key pair used by the root CA and whose public key is in the RCAC. - - Each admin is identified by an 'admin index' that is unique to the running - process. This is used to store credential information to disk so that - it can be easily loaded later if neccessary (see 'Persistence' below for more details) - - When vending ChipDeviceController instances on a given fabric, each controller instance - is associated with a unique fabric index. In the underlying FabricTable, each FabricInfo - instance can be treated as unique identities that can collide on the same logical fabric. - - >> C++ Binding Details - - Each instance of the fabric admin is associated with a single instance - of the OperationalCredentialsAdapter. This adapter instance implements - the OperationalCredentialsDelegate and is meant to provide a Python - adapter to the functions in that delegate so that the fabric admin - can in turn, provide users the ability to generate their own NOCs for devices - on the network (not implemented yet). For now, it relies on the in-built - ExampleOperationalCredentialsIssuer to do that. - - TODO: Add support for FabricAdmin to permit callers to hook up their own GenerateNOC - logic. - - >> Persistence - - Specifically, each instance persists its fabric ID and admin - index to storage. This is in addition to the persistence built into the ExampleOperationalCredentialsIssuer that persists details - about the RCAC/ICAC and associated keys as well. This facilitates re-construction of a fabric admin on subsequent - boot for a given fabric and ensuring it automatically picks up the right ICAC/RCAC details as well. + ''' Administers a fabric associated with a unique FabricID under a given CertificateAuthority + instance. ''' - - activeAdminIndexList = set() - activeAdmins = set() - @classmethod def _Handle(cls): return chip.native.GetLibraryHandle() - def AllocateNextAdminIndex(self): - ''' Allocate the next un-used admin index. - ''' - nextAdminIndex = 1 - while nextAdminIndex in FabricAdmin.activeAdminIndexList: - nextAdminIndex = nextAdminIndex + 1 - return nextAdminIndex - - def __init__(self, vendorId: int, adminIndex: int = None, fabricId: int = 1): - ''' Creates a valid FabricAdmin object with valid RCAC/ICAC, and registers itself as an OperationalCredentialsDelegate - for other parts of the system (notably, DeviceController) to vend NOCs. - - vendorId: Valid operational Vendor ID associated with this fabric. - adminIndex: Local index to be associated with this fabric. This is NOT the fabric index. Each controller on the fabric - is assigned a unique fabric index. + @classmethod + def logger(cls): + return logging.getLogger('FabricAdmin') - If omitted, one will be automatically assigned. + def __init__(self, certificateAuthority: CertificateAuthority, vendorId: int, fabricId: int = 1): + ''' Initializes the object. - fabricId: Fabric ID to be associated with this fabric. This is scoped to the public key of the resultant - root generated by the underlying ExampleOperationalCredentialsIssuer. + certificateAuthority: CertificateAuthority instance that will be used to vend NOCs for both + DeviceControllers and commissionable nodes on this fabric. + vendorId: Valid operational Vendor ID associated with this fabric. + fabricId: Fabric ID to be associated with this fabric. ''' self._handle = chip.native.GetLibraryHandle() @@ -101,104 +60,68 @@ def __init__(self, vendorId: int, adminIndex: int = None, fabricId: int = 1): raise ValueError( f"Invalid VendorID ({vendorId}) provided!") + if (fabricId is None or fabricId == 0): + raise ValueError( + f"Invalid FabricId ({fabricId}) provided!") + self._vendorId = vendorId self._fabricId = fabricId + self._certificateAuthority = certificateAuthority - if (adminIndex is None): - self._adminIndex = self.AllocateNextAdminIndex() - else: - if (adminIndex in FabricAdmin.activeAdminIndexList): - raise ValueError( - f"AdminIndex {adminIndex} is already being managed by an existing FabricAdmin object!") - - self._adminIndex = adminIndex - - FabricAdmin.activeAdminIndexList.add(self._adminIndex) - - print( - f"New FabricAdmin: FabricId: 0x{self._fabricId:016X}, AdminIndex: {self._adminIndex}, VendorId = 0x{self.vendorId:04X}") - self._Handle().pychip_OpCreds_InitializeDelegate.restype = c_void_p + self.logger().warning(f"New FabricAdmin: FabricId: 0x{self._fabricId:016X}, VendorId = 0x{self.vendorId:04X}") - self.closure = builtins.chipStack.Call( - lambda: self._Handle().pychip_OpCreds_InitializeDelegate( - ctypes.py_object(self), ctypes.c_uint32(self._adminIndex)) - ) - - if (self.closure is None): - raise ValueError("Encountered error initializing OpCreds adapter") - - # - # Persist details to storage (read modify write). - # - try: - adminList = builtins.chipStack.GetStorageManager().GetReplKey('fabricAdmins') - except KeyError: - adminList = {str(self._adminIndex): {'fabricId': self._fabricId}} - builtins.chipStack.GetStorageManager().SetReplKey('fabricAdmins', adminList) + self._isActive = True + self._activeControllers = [] - adminList[str(self._adminIndex)] = {'fabricId': self._fabricId, 'vendorId': self.vendorId} - builtins.chipStack.GetStorageManager().SetReplKey('fabricAdmins', adminList) + def NewController(self, nodeId: int = None, paaTrustStorePath: str = "", useTestCommissioner: bool = False): + ''' Create a new chip.ChipDeviceCtrl.ChipDeviceController instance on this fabric. - self._isActive = True - self.nextControllerId = 112233 + When vending ChipDeviceController instances on a given fabric, each controller instance + is associated with a unique fabric index local to the running process. In the underlying FabricTable, each FabricInfo + instance can be treated as unique identities that can collide on the same logical fabric. - FabricAdmin.activeAdmins.add(self) + nodeId: NodeID to be assigned to the controller. Automatically allocates one starting from 112233 if one + is not provided. - def NewController(self, nodeId: int = None, paaTrustStorePath: str = "", useTestCommissioner: bool = False): - ''' Vend a new controller on this fabric seeded with the right fabric details. + paaTrustStorePath: Path to the PAA trust store. If one isn't provided, a suitable default is selected. + useTestCommissioner: If a test commmisioner is to be created. ''' if (not(self._isActive)): raise RuntimeError( f"FabricAdmin object was previously shutdown and is no longer valid!") + nodeIdList = [controller.nodeId for controller in self._activeControllers] if (nodeId is None): - nodeId = self.nextControllerId - self.nextControllerId = self.nextControllerId + 1 + if (len(nodeIdList) != 0): + nodeId = max(nodeIdList) + 1 + else: + nodeId = 112233 + else: + if (nodeId in nodeIdList): + raise RuntimeError(f"Provided NodeId {nodeId} collides with an existing controller instance!") - print( - f"Allocating new controller with FabricId: 0x{self._fabricId:016X}, NodeId: 0x{nodeId:016X}") + self.logger().warning( + f"Allocating new controller with CaIndex: {self._certificateAuthority.caIndex}, FabricId: 0x{self._fabricId:016X}, NodeId: 0x{nodeId:016X}") controller = ChipDeviceCtrl.ChipDeviceController( - self.closure, self._fabricId, nodeId, self.vendorId, paaTrustStorePath, useTestCommissioner, fabricAdmin=self) - return controller - - def ShutdownAll(): - ''' Shuts down all active fabrics, but without deleting them from storage. - ''' - activeAdmins = copy.copy(FabricAdmin.activeAdmins) + self._certificateAuthority.GetOpCredsContext(), self._fabricId, nodeId, self._vendorId, paaTrustStorePath, useTestCommissioner, fabricAdmin=self) - for admin in activeAdmins: - admin.Shutdown(False) - - FabricAdmin.activeAdmins.clear() + self._activeControllers.append(controller) + return controller - def Shutdown(self, deleteFromStorage: bool = True): - ''' Shutdown this fabric and free up its resources. This is important since relying - solely on the destructor will not guarantee relishining of C++-side resources. + def Shutdown(self): + ''' Shutdown all active controllers on the fabric before shutting down the fabric itself. - deleteFromStorage: Whether to delete this fabric's details from persistent storage. + You cannot interact with this object there-after. ''' if (self._isActive): - builtins.chipStack.Call( - lambda: self._Handle().pychip_OpCreds_FreeDelegate( - ctypes.c_void_p(self.closure)) - ) - - FabricAdmin.activeAdminIndexList.remove(self._adminIndex) - - if (deleteFromStorage): - adminList = builtins.chipStack.GetStorageManager().GetReplKey('fabricAdmins') - del(adminList[str(self._adminIndex)]) - if (len(adminList) == 0): - adminList = None + for controller in self._activeControllers: + controller.Shutdown() - builtins.chipStack.GetStorageManager().SetReplKey('fabricAdmins', adminList) - - FabricAdmin.activeAdmins.remove(self) self._isActive = False def __del__(self): - self.Shutdown(False) + self.Shutdown() @property def vendorId(self) -> int: @@ -209,5 +132,9 @@ def fabricId(self) -> int: return self._fabricId @property - def adminIndex(self) -> int: - return self._adminIndex + def caIndex(self) -> int: + return self._certificateAuthority.caIndex + + @property + def certificateAuthority(self) -> CertificateAuthority: + return self._certificateAuthority diff --git a/src/controller/python/chip/storage/__init__.py b/src/controller/python/chip/storage/__init__.py index 362abda084075c..ad51c75754f9f4 100644 --- a/src/controller/python/chip/storage/__init__.py +++ b/src/controller/python/chip/storage/__init__.py @@ -51,11 +51,7 @@ def _OnSyncGetKeyValueCb(storageObj, key: str, value, size, is_found): this method to the requirements of PersistentStorageDelegate::SyncGetKeyValue. ''' - try: - keyValue = storageObj.GetSdkKey(key.decode("utf-8")) - except Exception as ex: - keyValue = None - + keyValue = storageObj.GetSdkKey(key.decode("utf-8")) if (keyValue is not None): sizeOfValue = size[0] sizeToCopy = min(sizeOfValue, len(keyValue)) @@ -85,37 +81,94 @@ def _OnSyncDeleteKeyValueCb(storageObj, key): class PersistentStorage: + ''' Class that provided persistent storage to back both native Python and + SDK configuration key/value pairs. + + Configuration native to the Python libraries is organized under the top-level + 'repl-config' key while configuration native to the SDK and owned by the various + C++ logic is organized under the top-level 'sdk-config' key. + + This interfaces with a C++ adapter that implements the PersistentStorageDelegate interface + and can be passed into C++ logic that needs an instance of that interface. + ''' + @classmethod + def logger(cls): + return logging.getLogger('PersistentStorage') + + def __init__(self, path: str = None, jsonData: Dict = None): + ''' Initializes the object with either a path to a JSON file that contains the configuration OR + a JSON dictionary that contains an in-memory representation of the configuration. + + In either case, if there are no valid configurations that already exist, empty Python + and SDK configuration records will be created upon construction. + ''' + if (path is None and jsonData is None): + raise ValueError("Need to provide at least one of path or jsonData") + + if (path is not None and jsonData is not None): + raise ValueError("Can't provide both a valid path and jsonData") + + if (path is not None): + self.logger().warn(f"Initializing persistent storage from file: {path}") + else: + self.logger().warn(f"Initializing persistent storage from dict") - def __init__(self, path: str): - self._path = path self._handle = chip.native.GetLibraryHandle() self._isActive = True + self._path = path - try: - self._file = open(path, 'r') - self._file.seek(0, 2) - size = self._file.tell() - self._file.seek(0) + if (self._path): + try: + self._file = open(path, 'r') + self._file.seek(0, 2) + size = self._file.tell() + self._file.seek(0) - if (size != 0): - logging.critical(f"Loading configuration from {path}...") - self.jsonData = json.load(self._file) - else: - logging.warn( - f"No valid configuration present at {path} - clearing out configuration") - self.jsonData = {'repl-config': {}, 'sdk-config': {}} + if (size != 0): + self.logger().warn(f"Loading configuration from {path}...") + self._jsonData = json.load(self._file) + else: + self._jsonData = {} - except Exception as ex: - logging.error(ex) - logging.warn( - f"Could not load configuration from {path} - resetting configuration...") - self.jsonData = {'repl-config': {}, 'sdk-config': {}} + except Exception as ex: + logging.error(ex) + logging.critical(f"Could not load configuration from {path} - resetting configuration...") + self._jsonData = {} + else: + self._jsonData = jsonData + + if ('sdk-config' not in self._jsonData): + logging.warn(f"No valid SDK configuration present - clearing out configuration") + self._jsonData['sdk-config'] = {} + + if ('repl-config' not in self._jsonData): + logging.warn(f"No valid REPL configuration present - clearing out configuration") + self._jsonData['repl-config'] = {} + # Clear out the file so that calling 'Commit' will re-open the file at that time in write mode. self._file = None - self._handle.pychip_Storage_InitializeStorageAdapter(ctypes.py_object( + + self._handle.pychip_Storage_InitializeStorageAdapter.restype = c_void_p + self._handle.pychip_Storage_InitializeStorageAdapter.argtypes = [ctypes.py_object, + _SyncSetKeyValueCbFunct, _SyncGetKeyValueCbFunct, _SyncDeleteKeyValueCbFunct] + + self._closure = self._handle.pychip_Storage_InitializeStorageAdapter(ctypes.py_object( self), _OnSyncSetKeyValueCb, _OnSyncGetKeyValueCb, _OnSyncDeleteKeyValueCb) - def Sync(self): + def GetSdkStorageObject(self): + ''' Returns a ctypes c_void_p reference to the SDK-side adapter instance. + ''' + return self._closure + + def Commit(self): + ''' Commits the cached JSON configuration to file (if one was provided in the constructor). + Otherwise, this is a no-op. + ''' + self.logger().info("Committing...") + + if (self._path is None): + return + if (self._file is None): try: self._file = open(self._path, 'w') @@ -126,28 +179,38 @@ def Sync(self): return self._file.seek(0) - json.dump(self.jsonData, self._file, ensure_ascii=True, indent=4) + json.dump(self._jsonData, self._file, ensure_ascii=True, indent=4) self._file.truncate() self._file.flush() def SetReplKey(self, key: str, value): - logging.info(f"SetReplKey: {key} = {value}") + ''' Set a REPL key to a specific value. Creates the key if one doesn't exist already. + ''' + self.logger().info(f"SetReplKey: {key} = {value}") if (key is None or key == ''): raise ValueError("Invalid Key") if (value is None): - del(self.jsonData['repl-config'][key]) + del(self._jsonData['repl-config'][key]) else: - self.jsonData['repl-config'][key] = value + self._jsonData['repl-config'][key] = value - self.Sync() + self.Commit() def GetReplKey(self, key: str): - return copy.deepcopy(self.jsonData['repl-config'][key]) + ''' Retrieves the value of a REPL key. Returns 'None' if the key + doesn't exist. + ''' + if (key not in self._jsonData['repl-config']): + return None + + return copy.deepcopy(self._jsonData['repl-config'][key]) def SetSdkKey(self, key: str, value: bytes): - logging.info(f"SetSdkKey: {key} = {value}") + ''' Set an SDK key to a specific value. Creates the key if one doesn't exist already. + ''' + self.logger().info(f"SetSdkKey: {key} = {value}") if (key is None or key == ''): raise ValueError("Invalid Key") @@ -155,28 +218,45 @@ def SetSdkKey(self, key: str, value: bytes): if (value is None): raise ValueError('value is not expected to be None') else: - self.jsonData['sdk-config'][key] = base64.b64encode( + self._jsonData['sdk-config'][key] = base64.b64encode( value).decode("utf-8") - self.Sync() + self.Commit() def GetSdkKey(self, key: str): - return base64.b64decode(self.jsonData['sdk-config'][key]) + ''' Returns the SDK key if one exist. Otherwise, returns 'None'. + ''' + if (key not in self._jsonData['sdk-config']): + return None + + return base64.b64decode(self._jsonData['sdk-config'][key]) def DeleteSdkKey(self, key: str): - del(self.jsonData['sdk-config'][key]) - self.Sync() + ''' Deletes an SDK key if one exists. + ''' + self.logger().info(f"DeleteSdkKey: {key}") - def GetUnderlyingStorageAdapter(self): - return self._storageAdapterObj + del(self._jsonData['sdk-config'][key]) + self.Commit() def Shutdown(self): + ''' Shuts down the object by free'ing up the associated adapter instance. + + You cannot interact with this object there-after. + ''' + self._handle.pychip_Storage_ShutdownAdapter.argtypes = [c_void_p] builtins.chipStack.Call( - lambda: self._handle.pychip_Storage_ShutdownAdapter() + lambda: self._handle.pychip_Storage_ShutdownAdapter(self._closure) ) self._isActive = False + @property + def jsonData(self) -> Dict: + ''' Returns a copy of the internal cached JSON data. + ''' + return copy.deepcopy(self._jsonData) + def __del__(self): if (self._isActive): builtins.chipStack.Call( diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py index 90bc43cb064781..3b9f3d9288a7ba 100644 --- a/src/controller/python/test/test_scripts/base.py +++ b/src/controller/python/test/test_scripts/base.py @@ -36,6 +36,7 @@ from chip.ChipStack import * import chip.native import chip.FabricAdmin +import chip.CertificateAuthority import copy import secrets import faulthandler @@ -193,8 +194,9 @@ def __init__(self, nodeid: int, paaTrustStorePath: str, testCommissioner: bool = chip.native.Init() self.chipStack = ChipStack('/tmp/repl_storage.json') - self.fabricAdmin = chip.FabricAdmin.FabricAdmin(vendorId=0XFFF1, - fabricId=1, adminIndex=1) + self.certificateAuthorityManager = chip.CertificateAuthority.CertificateAuthorityManager(chipStack=self.chipStack) + self.certificateAuthority = self.certificateAuthorityManager.NewCertificateAuthority() + self.fabricAdmin = self.certificateAuthority.NewFabricAdmin(vendorId=0xFFF1, fabricId=1) self.devCtrl = self.fabricAdmin.NewController( nodeid, paaTrustStorePath, testCommissioner) self.controllerNodeId = nodeid @@ -463,7 +465,8 @@ async def TestAddUpdateRemoveFabric(self, nodeid: int): self.logger.info("Waiting for attribute read for CommissionedFabrics") startOfTestFabricCount = await self._GetCommissonedFabricCount(nodeid) - tempFabric = chip.FabricAdmin.FabricAdmin(vendorId=0xFFF1) + tempCertificateAuthority = self.certificateAuthorityManager.NewCertificateAuthority() + tempFabric = tempCertificateAuthority.NewFabricAdmin(vendorId=0xFFF1, fabricId=1) tempDevCtrl = tempFabric.NewController(self.controllerNodeId, self.paaTrustStorePath) self.logger.info("Starting AddNOC using same node ID") @@ -628,8 +631,7 @@ async def TestMultiFabric(self, ip: str, setuppin: int, nodeid: int): await self.devCtrl.SendCommand(nodeid, 0, Clusters.AdministratorCommissioning.Commands.OpenBasicCommissioningWindow(180), timedRequestTimeoutMs=10000) self.logger.info("Creating 2nd Fabric Admin") - self.fabricAdmin2 = chip.FabricAdmin.FabricAdmin(vendorId=0xFFF1, - fabricId=2, adminIndex=2) + self.fabricAdmin2 = self.certificateAuthority.NewFabricAdmin(vendorId=0xFFF1, fabricId=2) self.logger.info("Creating Device Controller on 2nd Fabric") self.devCtrl2 = self.fabricAdmin2.NewController( @@ -646,15 +648,15 @@ async def TestMultiFabric(self, ip: str, setuppin: int, nodeid: int): self.logger.info( "Shutting down controllers & fabrics and re-initing stack...") - ChipDeviceCtrl.ChipDeviceController.ShutdownAll() - chip.FabricAdmin.FabricAdmin.ShutdownAll() + self.certificateAuthorityManager.Shutdown() self.logger.info("Shutdown completed, starting new controllers...") - self.fabricAdmin = chip.FabricAdmin.FabricAdmin(vendorId=0XFFF1, - fabricId=1, adminIndex=1) - fabricAdmin2 = chip.FabricAdmin.FabricAdmin(vendorId=0xFFF1, - fabricId=2, adminIndex=2) + self.certificateAuthorityManager = chip.CertificateAuthority.CertificateAuthorityManager(chipStack=self.chipStack) + self.certificateAuthority = self.certificateAuthorityManager.NewCertificateAuthority() + self.fabricAdmin = self.certificateAuthority.NewFabricAdmin(vendorId=0xFFF1, fabricId=1) + + fabricAdmin2 = self.certificateAuthority.NewFabricAdmin(vendorId=0xFFF1, fabricId=2) self.devCtrl = self.fabricAdmin.NewController( self.controllerNodeId, self.paaTrustStorePath) diff --git a/src/python_testing/TC_SC_3_6.py b/src/python_testing/TC_SC_3_6.py index ede817f8de4628..2c258c620d86a1 100644 --- a/src/python_testing/TC_SC_3_6.py +++ b/src/python_testing/TC_SC_3_6.py @@ -18,6 +18,7 @@ from matter_testing_support import MatterBaseTest, default_matter_test_main, async_test_body import chip.clusters as Clusters import chip.FabricAdmin +import chip.CertificateAuthority import logging from mobly import asserts from chip.utils import CommissioningBuildingBlocks @@ -137,7 +138,8 @@ async def test_TC_SC_3_6(self): for i in range(num_fabrics_to_commission - 1): admin_index = 2 + i logging.info("Commissioning fabric %d/%d" % (admin_index, num_fabrics_to_commission)) - new_fabric_admin = chip.FabricAdmin.FabricAdmin(vendorId=0xFFF1, adminIndex=admin_index) + new_certificate_authority = self.certificate_authority_manager.NewCertificateAuthority() + new_fabric_admin = new_certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=1) new_admin_ctrl = new_fabric_admin.NewController(nodeId=dev_ctrl.nodeId) new_admin_ctrl.name = all_names.pop(0) client_list.append(new_admin_ctrl) diff --git a/src/python_testing/matter_testing_support.py b/src/python_testing/matter_testing_support.py index 6064d46fda0dcd..607f24a057b9c3 100644 --- a/src/python_testing/matter_testing_support.py +++ b/src/python_testing/matter_testing_support.py @@ -25,6 +25,7 @@ import chip.logging import chip.native import chip.FabricAdmin +import chip.CertificateAuthority from chip.utils import CommissioningBuildingBlocks import builtins from typing import Optional, List, Tuple @@ -45,7 +46,6 @@ from mobly import utils from mobly.test_runner import TestRunner - # TODO: Add utility to commission a device if needed # TODO: Add utilities to keep track of controllers/fabrics @@ -157,7 +157,6 @@ class MatterStackState: def __init__(self, config: MatterTestConfig): self._logger = logger self._config = config - self._fabric_admins = [] if not hasattr(builtins, "chipStack"): chip.native.Init(bluetoothAdapter=config.ble_interface_id) @@ -180,22 +179,17 @@ def _init_stack(self, already_initialized: bool, **kwargs): builtins.chipStack = self._chip_stack self._storage = self._chip_stack.GetStorageManager() + self._certificate_authority_manager = chip.CertificateAuthority.CertificateAuthorityManager(chipStack=self._chip_stack) + self._certificate_authority_manager.LoadAuthoritiesFromStorage() - try: - admin_list = self._storage.GetReplKey('fabricAdmins') - found_admin_list = True - except KeyError: - found_admin_list = False - - if not found_admin_list: - self._logger.warn("No previous fabric administrative data found in persistent data: initializing a new one") - self._fabric_admins.append(chip.FabricAdmin.FabricAdmin(self._config.admin_vendor_id)) - else: - for admin_idx in admin_list: - self._logger.info( - f"Restoring FabricAdmin from storage to manage FabricId {admin_list[admin_idx]['fabricId']}, AdminIndex {admin_idx}") - self._fabric_admins.append(chip.FabricAdmin.FabricAdmin(vendorId=int(admin_list[admin_idx]['vendorId']), - fabricId=admin_list[admin_idx]['fabricId'], adminIndex=int(admin_idx))) + if (len(self._certificate_authority_manager.activeCaList) == 0): + self._logger.warn( + "Didn't find any CertificateAuthorities in storage -- creating a new CertificateAuthority + FabricAdmin...") + ca = self._certificate_authority_manager.NewCertificateAuthority() + ca.NewFabricAdmin(vendorId=0xFFF1, fabricId=0xFFF1) + elif (len(self._certificate_authority_manager.activeCaList[0].adminList) == 0): + self._logger.warn("Didn't find any FabricAdmins in storage -- creating a new one...") + self._certificate_authority_manager.activeCaList[0].NewFabricAdmin(vendorId=0xFFF1, fabricId=0xFFF1) # TODO: support getting access to chip-tool credentials issuer's data @@ -204,14 +198,17 @@ def Shutdown(self): # Unfortunately, all the below are singleton and possibly # managed elsewhere so we have to be careful not to touch unless # we initialized ourselves. - ChipDeviceCtrl.ChipDeviceController.ShutdownAll() - chip.FabricAdmin.FabricAdmin.ShutdownAll() + self._certificate_authority_manager.Shutdown() global_chip_stack = builtins.chipStack global_chip_stack.Shutdown() @property - def fabric_admins(self): - return self._fabric_admins + def certificate_authorities(self): + return self._certificate_authority_manager.activeCaList + + @property + def certificate_authority_manager(self): + return self._certificate_authority_manager @property def storage(self) -> PersistentStorage: @@ -251,6 +248,10 @@ def default_controller(self) -> ChipDeviceCtrl: def matter_stack(self) -> MatterStackState: return unstash_globally(self.user_params.get("matter_stack")) + @property + def certificate_authority_manager(self) -> chip.CertificateAuthority.CertificateAuthorityManager: + return unstash_globally(self.user_params.get("certificate_authority_manager")) + @property def dut_node_id(self) -> int: return self.matter_test_config.dut_node_id @@ -684,12 +685,14 @@ def default_matter_test_main(argv=None): test_config.user_params["matter_stack"] = stash_globally(stack) # TODO: Steer to right FabricAdmin! - default_controller = stack.fabric_admins[0].NewController(nodeId=matter_test_config.controller_node_id, - paaTrustStorePath=str(matter_test_config.paa_trust_store_path)) + default_controller = stack.certificate_authorities[0].adminList[0].NewController(nodeId=matter_test_config.controller_node_id, + paaTrustStorePath=str(matter_test_config.paa_trust_store_path)) test_config.user_params["default_controller"] = stash_globally(default_controller) test_config.user_params["matter_test_config"] = stash_globally(matter_test_config) + test_config.user_params["certificate_authority_manager"] = stash_globally(stack.certificate_authority_manager) + # Execute the test class with the config ok = True From 756cea0497c2005f4ee3a5b6f2523e6d85a6602f Mon Sep 17 00:00:00 2001 From: Jerry Johns Date: Thu, 18 Aug 2022 20:25:15 -0700 Subject: [PATCH 2/3] Python CAT Value support for Controllers (#22019) * Python CAT Value support for Controllers * Review feedback --- src/controller/python/OpCredsBinding.cpp | 16 ++++++++-- src/controller/python/chip/ChipDeviceCtrl.py | 29 +++++++++++------ src/controller/python/chip/FabricAdmin.py | 9 +++--- .../python/test/test_scripts/base.py | 31 +++++++++++++++++++ .../test/test_scripts/mobile-device-test.py | 3 ++ 5 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp index fa779e3d383fc0..e6ed27a6da1db3 100644 --- a/src/controller/python/OpCredsBinding.cpp +++ b/src/controller/python/OpCredsBinding.cpp @@ -321,7 +321,8 @@ void pychip_OnCommissioningStatusUpdate(chip::PeerId peerId, chip::Controller::C ChipError::StorageType pychip_OpCreds_AllocateController(OpCredsContext * context, chip::Controller::DeviceCommissioner ** outDevCtrl, FabricId fabricId, chip::NodeId nodeId, chip::VendorId adminVendorId, - const char * paaTrustStorePath, bool useTestCommissioner) + const char * paaTrustStorePath, bool useTestCommissioner, + CASEAuthTag * caseAuthTags, uint32_t caseAuthTagLen) { ChipLogDetail(Controller, "Creating New Device Controller"); @@ -357,8 +358,17 @@ ChipError::StorageType pychip_OpCreds_AllocateController(OpCredsContext * contex ReturnErrorCodeIf(!rcac.Alloc(Controller::kMaxCHIPDERCertLength), CHIP_ERROR_NO_MEMORY.AsInteger()); MutableByteSpan rcacSpan(rcac.Get(), Controller::kMaxCHIPDERCertLength); - err = context->mAdapter->GenerateNOCChain(nodeId, fabricId, chip::kUndefinedCATs, ephemeralKey.Pubkey(), rcacSpan, icacSpan, - nocSpan); + CATValues catValues; + + if ((caseAuthTagLen + 1) > kMaxSubjectCATAttributeCount) + { + ChipLogError(Controller, "# of CASE Tags exceeds kMaxSubjectCATAttributeCount"); + return CHIP_ERROR_INVALID_ARGUMENT.AsInteger(); + } + + memcpy(catValues.values.data(), caseAuthTags, caseAuthTagLen * sizeof(CASEAuthTag)); + + err = context->mAdapter->GenerateNOCChain(nodeId, fabricId, catValues, ephemeralKey.Pubkey(), rcacSpan, icacSpan, nocSpan); VerifyOrReturnError(err == CHIP_NO_ERROR, err.AsInteger()); Controller::SetupParams initParams; diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py index edac6bc8f516dc..8e11f27a953b16 100644 --- a/src/controller/python/chip/ChipDeviceCtrl.py +++ b/src/controller/python/chip/ChipDeviceCtrl.py @@ -157,7 +157,7 @@ class DiscoveryFilterType(enum.IntEnum): class ChipDeviceController(): activeList = set() - def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, nodeId: int, adminVendorId: int, paaTrustStorePath: str = "", useTestCommissioner: bool = False, fabricAdmin: FabricAdmin = None, name: str = None): + 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): self.state = DCState.NOT_INITIALIZED self.devCtrl = None self._ChipStack = builtins.chipStack @@ -169,9 +169,18 @@ def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, nodeId: int, 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, POINTER(c_uint32), c_uint32] + self._dmLib.pychip_OpCreds_AllocateController.restype = c_uint32 + res = self._ChipStack.Call( - lambda: self._dmLib.pychip_OpCreds_AllocateController(ctypes.c_void_p( - opCredsContext), pointer(devCtrl), fabricId, nodeId, adminVendorId, ctypes.c_char_p(None if len(paaTrustStorePath) == 0 else str.encode(paaTrustStorePath)), useTestCommissioner) + 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, c_catTags, len(catTags)) ) if res != 0: @@ -233,7 +242,7 @@ def HandlePASEEstablishmentComplete(err): self.devCtrl, self.cbHandleCommissioningCompleteFunct) self.state = DCState.IDLE - self.isActive = True + self._isActive = True # Validate FabricID/NodeID followed from NOC Chain self._fabricId = self.GetFabricIdInternal() @@ -249,12 +258,10 @@ def fabricAdmin(self) -> FabricAdmin: @property def nodeId(self) -> int: - self.CheckIsActive() return self._nodeId @property def fabricId(self) -> int: - self.CheckIsActive() return self._fabricId @property @@ -269,11 +276,15 @@ def name(self) -> str: def name(self, new_name: str): self._name = new_name + @property + def isActive(self) -> bool: + return self._isActive + def Shutdown(self): ''' Shuts down this controller and reclaims any used resources, including the bound C++ constructor instance in the SDK. ''' - if (self.isActive): + if (self._isActive): if self.devCtrl != None: self._ChipStack.Call( lambda: self._dmLib.pychip_DeviceController_DeleteDeviceController( @@ -282,7 +293,7 @@ def Shutdown(self): self.devCtrl = None ChipDeviceController.activeList.remove(self) - self.isActive = False + self._isActive = False def ShutdownAll(): ''' Shut down all active controllers and reclaim any used resources. @@ -304,7 +315,7 @@ def ShutdownAll(): ChipDeviceController.activeList.clear() def CheckIsActive(self): - if (not self.isActive): + if (not self._isActive): raise RuntimeError( "DeviceCtrl instance was already shutdown previously!") diff --git a/src/controller/python/chip/FabricAdmin.py b/src/controller/python/chip/FabricAdmin.py index 216da4ebad7646..3cfe03ffdb7e95 100644 --- a/src/controller/python/chip/FabricAdmin.py +++ b/src/controller/python/chip/FabricAdmin.py @@ -73,7 +73,7 @@ def __init__(self, certificateAuthority: CertificateAuthority, vendorId: int, fa self._isActive = True self._activeControllers = [] - def NewController(self, nodeId: int = None, paaTrustStorePath: str = "", useTestCommissioner: bool = False): + def NewController(self, nodeId: int = None, paaTrustStorePath: str = "", useTestCommissioner: bool = False, catTags: List[int] = []): ''' Create a new chip.ChipDeviceCtrl.ChipDeviceController instance on this fabric. When vending ChipDeviceController instances on a given fabric, each controller instance @@ -85,12 +85,13 @@ def NewController(self, nodeId: int = None, paaTrustStorePath: str = "", useTest paaTrustStorePath: Path to the PAA trust store. If one isn't provided, a suitable default is selected. useTestCommissioner: If a test commmisioner is to be created. + catTags: A list of 32-bit CAT tags that will added to the NOC generated for this controller. ''' if (not(self._isActive)): raise RuntimeError( f"FabricAdmin object was previously shutdown and is no longer valid!") - nodeIdList = [controller.nodeId for controller in self._activeControllers] + nodeIdList = [controller.nodeId for controller in self._activeControllers if controller.isActive] if (nodeId is None): if (len(nodeIdList) != 0): nodeId = max(nodeIdList) + 1 @@ -103,8 +104,8 @@ def NewController(self, nodeId: int = None, paaTrustStorePath: str = "", useTest self.logger().warning( f"Allocating new controller with CaIndex: {self._certificateAuthority.caIndex}, FabricId: 0x{self._fabricId:016X}, NodeId: 0x{nodeId:016X}") - controller = ChipDeviceCtrl.ChipDeviceController( - self._certificateAuthority.GetOpCredsContext(), self._fabricId, nodeId, self._vendorId, paaTrustStorePath, useTestCommissioner, fabricAdmin=self) + controller = ChipDeviceCtrl.ChipDeviceController(opCredsContext=self._certificateAuthority.GetOpCredsContext(), fabricId=self._fabricId, nodeId=nodeId, + adminVendorId=self._vendorId, paaTrustStorePath=paaTrustStorePath, useTestCommissioner=useTestCommissioner, fabricAdmin=self, catTags=catTags) self._activeControllers.append(controller) return controller diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py index 3b9f3d9288a7ba..8488b2e80b6ce6 100644 --- a/src/controller/python/test/test_scripts/base.py +++ b/src/controller/python/test/test_scripts/base.py @@ -386,6 +386,37 @@ def TestFailsafe(self, nodeid: int): return True return False + async def TestControllerCATValues(self, nodeid: int): + ''' This tests controllers using CAT Values + ''' + + # Allocate a new controller instance with a CAT tag. + newController = self.fabricAdmin.NewController(nodeId=300, catTags=[0x00010001]) + + # Read out an attribute using the new controller. It has no privileges, so this should fail with an UnsupportedAccess error. + res = await newController.ReadAttribute(nodeid=nodeid, attributes=[(0, Clusters.AccessControl.Attributes.Acl)]) + if(res[0][Clusters.AccessControl][Clusters.AccessControl.Attributes.Acl].Reason.status != IM.Status.UnsupportedAccess): + self.logger.error(f"1: Received data instead of an error:{res}") + return False + + # Do a read-modify-write operation on the ACL list to add the CAT tag to the ACL list. + aclList = (await self.devCtrl.ReadAttribute(nodeid, [(0, Clusters.AccessControl.Attributes.Acl)]))[0][Clusters.AccessControl][Clusters.AccessControl.Attributes.Acl] + origAclList = copy.deepcopy(aclList) + aclList[0].subjects.append(0xFFFFFFFD00010001) + await self.devCtrl.WriteAttribute(nodeid, [(0, Clusters.AccessControl.Attributes.Acl(aclList))]) + + # Read out the attribute again - this time, it should succeed. + res = await newController.ReadAttribute(nodeid=nodeid, attributes=[(0, Clusters.AccessControl.Attributes.Acl)]) + if (type(res[0][Clusters.AccessControl][Clusters.AccessControl.Attributes.Acl][0]) != Clusters.AccessControl.Structs.AccessControlEntry): + self.logger.error(f"2: Received something other than data:{res}") + return False + + # Write back the old entry to reset ACL list back. + await self.devCtrl.WriteAttribute(nodeid, [(0, Clusters.AccessControl.Attributes.Acl(origAclList))]) + newController.Shutdown() + + return True + async def TestMultiControllerFabric(self, nodeid: int): ''' This tests having multiple controller instances on the same fabric. ''' diff --git a/src/controller/python/test/test_scripts/mobile-device-test.py b/src/controller/python/test/test_scripts/mobile-device-test.py index ffea217fa526c9..99f17aabe27363 100755 --- a/src/controller/python/test/test_scripts/mobile-device-test.py +++ b/src/controller/python/test/test_scripts/mobile-device-test.py @@ -77,6 +77,9 @@ def ethernet_commissioning(test: BaseTestHelper, discriminator: int, setup_pin: logger.info("Testing multi-controller setup on the same fabric") FailIfNot(asyncio.run(test.TestMultiControllerFabric(nodeid=device_nodeid)), "Failed the multi-controller test") + logger.info("Testing CATs used on controllers") + FailIfNot(asyncio.run(test.TestControllerCATValues(nodeid=device_nodeid)), "Failed the controller CAT test") + ok = asyncio.run(test.TestMultiFabric(ip=address, setuppin=20202021, nodeid=1)) From cb23aaa0ab5449c5b68cbefdfbb0e69f56ada9d5 Mon Sep 17 00:00:00 2001 From: Tennessee Carmel-Veilleux Date: Fri, 19 Aug 2022 18:20:12 -0400 Subject: [PATCH 3/3] Introduce initial TC-RR-1.1 (#22032) * Introduce initial TC-RR-1.1 - TC-RR-1.1 is a critical test to validate multi-fabric behavior is stable and actually works. The test, broadly, validates most of the minimas of the core elements of the spec, including ACL entries, certificate sizes, number of CASE sessions and subscriptions, number of paths, etc. Issue #21736 - This PR introduces the core test and all associated minor changes to infrastructure to make it work. - Still TODO: - More extensive cert size maximization (closer to 400 TLV bytes) - Add controller and commissionee CAT tags (test is 95% equivalent to test plan, but a couple ACL fields differ because of this, in ways that don't detract from proving what needs proving - Validation that local/peer session IDs have not changed. This is not technically needed with the SDK as-is based on the methodology but it would future-proof the test against some future optimizations that may change subscription behavior in a way that the test would not validate CASE sessions remain. - Clean-up more after the test, so that a factory reset before/after is not needed. Testing done: - Passes on Linux against all-clusters, all-clusters-minimal and lighting app, with both minimal mdns and Avahi. - Passes on some other platforms (not named here) To run within SDK (from scratch: the build steps can be skipped thereafter): - In one terminal: - Build chip-lighting-app linux - `clear && rm -f kvs1 && out/debug/standalone/chip-lighting-app --discriminator 1234 --KVS kvs1 --trace_decode 1` - In another terminal: - Build - `rm -rf out/python*` - `scripts/build_python.sh -m platform -i separate` - Run - `source ./out/python_env/bin/activate` - `python3 src/python_testing/TC_RR_1_1.py --commissioning-method on-network --long-discriminator 1234 --passcode 20202021` - Add `--bool-arg skip_user_label_cluster_steps:true` to the end of the command line if your DUT has broken UserLabel clusters (but if you have those, fix them :) * More work towards CAT tags * Address review comments * Fixed CAT tag testing * Update src/controller/python/chip/utils/CommissioningBuildingBlocks.py Co-authored-by: Jerry Johns --- .../chip-tool/commands/common/CHIPCommand.cpp | 6 + .../chip-tool/commands/common/CHIPCommand.h | 4 + .../common/CredentialIssuerCommands.h | 19 + .../example/ExampleCredentialIssuerCommands.h | 27 ++ .../ExampleOperationalCredentialsIssuer.cpp | 168 ++++++- .../ExampleOperationalCredentialsIssuer.h | 7 +- src/controller/python/OpCredsBinding.cpp | 16 +- .../python/chip/CertificateAuthority.py | 24 +- src/controller/python/chip/FabricAdmin.py | 2 +- .../chip/utils/CommissioningBuildingBlocks.py | 36 +- .../python/test/test_scripts/base.py | 22 +- src/python_testing/TC_RR_1_1.py | 424 ++++++++++++++++++ src/python_testing/matter_testing_support.py | 38 +- 13 files changed, 744 insertions(+), 49 deletions(-) create mode 100644 src/python_testing/TC_RR_1_1.py diff --git a/examples/chip-tool/commands/common/CHIPCommand.cpp b/examples/chip-tool/commands/common/CHIPCommand.cpp index 663ebee8257d61..89dd1d536e4550 100644 --- a/examples/chip-tool/commands/common/CHIPCommand.cpp +++ b/examples/chip-tool/commands/common/CHIPCommand.cpp @@ -358,6 +358,12 @@ CHIP_ERROR CHIPCommand::InitializeCommissioner(std::string key, chip::FabricId f // store the credentials in persistent storage, and // generate when not available in the storage. ReturnLogErrorOnFailure(mCommissionerStorage.Init(key.c_str())); + if (mUseMaxSizedCerts.HasValue()) + { + auto option = CredentialIssuerCommands::CredentialIssuerOptions::kMaximizeCertificateSizes; + mCredIssuerCmds->SetCredentialIssuerOption(option, mUseMaxSizedCerts.Value()); + } + ReturnLogErrorOnFailure(mCredIssuerCmds->InitializeCredentialsIssuer(mCommissionerStorage)); chip::MutableByteSpan nocSpan(noc.Get(), chip::Controller::kMaxCHIPDERCertLength); diff --git a/examples/chip-tool/commands/common/CHIPCommand.h b/examples/chip-tool/commands/common/CHIPCommand.h index 1714928ea1096c..7dd36a7c7d6214 100644 --- a/examples/chip-tool/commands/common/CHIPCommand.h +++ b/examples/chip-tool/commands/common/CHIPCommand.h @@ -70,6 +70,9 @@ class CHIPCommand : public Command "4. The default if not specified is \"alpha\"."); AddArgument("commissioner-nodeid", 0, UINT64_MAX, &mCommissionerNodeId, "The node id to use for chip-tool. If not provided, kTestControllerNodeId (112233, 0x1B669) will be used."); + AddArgument("use-max-sized-certs", 0, 1, &mUseMaxSizedCerts, + "Maximize the size of operational certificates. If not provided or 0 (\"false\"), normally sized operational " + "certificates are generated."); #if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED AddArgument("trace_file", &mTraceFile); AddArgument("trace_log", 0, 1, &mTraceLog); @@ -153,6 +156,7 @@ class CHIPCommand : public Command chip::Optional mCommissionerNodeId; chip::Optional mBleAdapterId; chip::Optional mPaaTrustStorePath; + chip::Optional mUseMaxSizedCerts; // Cached trust store so commands other than the original startup command // can spin up commissioners as needed. diff --git a/examples/chip-tool/commands/common/CredentialIssuerCommands.h b/examples/chip-tool/commands/common/CredentialIssuerCommands.h index 951ef86efceb40..cc04863ee2f8b6 100644 --- a/examples/chip-tool/commands/common/CredentialIssuerCommands.h +++ b/examples/chip-tool/commands/common/CredentialIssuerCommands.h @@ -74,4 +74,23 @@ class CredentialIssuerCommands virtual CHIP_ERROR GenerateControllerNOCChain(chip::NodeId nodeId, chip::FabricId fabricId, const chip::CATValues & cats, chip::Crypto::P256Keypair & keypair, chip::MutableByteSpan & rcac, chip::MutableByteSpan & icac, chip::MutableByteSpan & noc) = 0; + + // All options must start false + enum CredentialIssuerOptions : uint8_t + { + kMaximizeCertificateSizes = 0, // If set, certificate chains will be maximized for testing via padding + }; + + virtual void SetCredentialIssuerOption(CredentialIssuerOptions option, bool isEnabled) + { + // Do nothing + (void) option; + (void) isEnabled; + } + + virtual bool GetCredentialIssuerOption(CredentialIssuerOptions option) + { + // All options always start false + return false; + } }; diff --git a/examples/chip-tool/commands/example/ExampleCredentialIssuerCommands.h b/examples/chip-tool/commands/example/ExampleCredentialIssuerCommands.h index 74646c8b5f10ba..40a2871b19437b 100644 --- a/examples/chip-tool/commands/example/ExampleCredentialIssuerCommands.h +++ b/examples/chip-tool/commands/example/ExampleCredentialIssuerCommands.h @@ -49,6 +49,33 @@ class ExampleCredentialIssuerCommands : public CredentialIssuerCommands return mOpCredsIssuer.GenerateNOCChainAfterValidation(nodeId, fabricId, cats, keypair.Pubkey(), rcac, icac, noc); } + void SetCredentialIssuerOption(CredentialIssuerOptions option, bool isEnabled) override + { + switch (option) + { + case CredentialIssuerOptions::kMaximizeCertificateSizes: + mUsesMaxSizedCerts = isEnabled; + mOpCredsIssuer.SetMaximallyLargeCertsUsed(mUsesMaxSizedCerts); + break; + default: + break; + } + } + + bool GetCredentialIssuerOption(CredentialIssuerOptions option) override + { + switch (option) + { + case CredentialIssuerOptions::kMaximizeCertificateSizes: + return mUsesMaxSizedCerts; + default: + return false; + } + } + +protected: + bool mUsesMaxSizedCerts = false; + private: chip::Controller::ExampleOperationalCredentialsIssuer mOpCredsIssuer; }; diff --git a/src/controller/ExampleOperationalCredentialsIssuer.cpp b/src/controller/ExampleOperationalCredentialsIssuer.cpp index cc8cd8fbcdb404..9c0b376b13716c 100644 --- a/src/controller/ExampleOperationalCredentialsIssuer.cpp +++ b/src/controller/ExampleOperationalCredentialsIssuer.cpp @@ -39,6 +39,127 @@ using namespace Credentials; using namespace Crypto; using namespace TLV; +namespace { + +enum CertType : uint8_t +{ + kRcac = 0, + kIcac = 1, + kNoc = 2 +}; + +CHIP_ERROR IssueX509Cert(uint32_t now, uint32_t validity, ChipDN issuerDn, ChipDN desiredDn, CertType certType, bool maximizeSize, + const Crypto::P256PublicKey & subjectPublicKey, Crypto::P256Keypair & issuerKeypair, + MutableByteSpan & outX509Cert) +{ + constexpr size_t kDERCertDnEncodingOverhead = 11; + constexpr size_t kTLVCertDnEncodingOverhead = 3; + constexpr size_t kMaxCertPaddingLength = 150; + constexpr size_t kTLVDesiredSize = kMaxCHIPCertLength - 50; + + Platform::ScopedMemoryBuffer derBuf; + ReturnErrorCodeIf(!derBuf.Alloc(kMaxDERCertLength), CHIP_ERROR_NO_MEMORY); + MutableByteSpan derSpan{ derBuf.Get(), kMaxDERCertLength }; + + int64_t serialNumber = 1; + + switch (certType) + { + case CertType::kRcac: { + X509CertRequestParams rcacRequest = { serialNumber, now, now + validity, desiredDn, desiredDn }; + ReturnErrorOnFailure(NewRootX509Cert(rcacRequest, issuerKeypair, derSpan)); + break; + } + case CertType::kIcac: { + X509CertRequestParams icacRequest = { serialNumber, now, now + validity, desiredDn, issuerDn }; + ReturnErrorOnFailure(NewICAX509Cert(icacRequest, subjectPublicKey, issuerKeypair, derSpan)); + break; + } + case CertType::kNoc: { + X509CertRequestParams nocRequest = { serialNumber, now, now + validity, desiredDn, issuerDn }; + ReturnErrorOnFailure(NewNodeOperationalX509Cert(nocRequest, subjectPublicKey, issuerKeypair, derSpan)); + break; + } + default: + return CHIP_ERROR_INVALID_ARGUMENT; + } + + if (maximizeSize && (desiredDn.RDNCount() < CHIP_CONFIG_CERT_MAX_RDN_ATTRIBUTES)) + { + Platform::ScopedMemoryBuffer paddedTlvBuf; + ReturnErrorCodeIf(!paddedTlvBuf.Alloc(kMaxCHIPCertLength + kMaxCertPaddingLength), CHIP_ERROR_NO_MEMORY); + MutableByteSpan paddedTlvSpan{ paddedTlvBuf.Get(), kMaxCHIPCertLength + kMaxCertPaddingLength }; + ReturnErrorOnFailure(ConvertX509CertToChipCert(derSpan, paddedTlvSpan)); + + Platform::ScopedMemoryBuffer paddedDerBuf; + ReturnErrorCodeIf(!paddedDerBuf.Alloc(kMaxDERCertLength + kMaxCertPaddingLength), CHIP_ERROR_NO_MEMORY); + MutableByteSpan paddedDerSpan{ paddedDerBuf.Get(), kMaxDERCertLength + kMaxCertPaddingLength }; + + Platform::ScopedMemoryBuffer fillerBuf; + ReturnErrorCodeIf(!fillerBuf.Alloc(kMaxCertPaddingLength), CHIP_ERROR_NO_MEMORY); + memset(fillerBuf.Get(), 'A', kMaxCertPaddingLength); + + int derPaddingLen = static_cast(kMaxDERCertLength - kDERCertDnEncodingOverhead - derSpan.size()); + int tlvPaddingLen = static_cast(kTLVDesiredSize - kTLVCertDnEncodingOverhead - paddedTlvSpan.size()); + if (certType == CertType::kRcac) + { + // For RCAC the issuer/subject DN are the same so padding will be present in both + derPaddingLen = (derPaddingLen - static_cast(kDERCertDnEncodingOverhead)) / 2; + tlvPaddingLen = (tlvPaddingLen - static_cast(kTLVCertDnEncodingOverhead)) / 2; + } + + size_t paddingLen = 0; + if (derPaddingLen >= 1 && tlvPaddingLen >= 1) + { + paddingLen = std::min(static_cast(std::min(derPaddingLen, tlvPaddingLen)), kMaxCertPaddingLength); + } + + for (; paddingLen > 0; paddingLen--) + { + paddedDerSpan = MutableByteSpan{ paddedDerBuf.Get(), kMaxDERCertLength + kMaxCertPaddingLength }; + paddedTlvSpan = MutableByteSpan{ paddedTlvBuf.Get(), kMaxCHIPCertLength + kMaxCertPaddingLength }; + + ChipDN certDn = desiredDn; + // Fill the padding in the DomainNameQualifier DN + certDn.AddAttribute_DNQualifier(CharSpan(fillerBuf.Get(), paddingLen), false); + + switch (certType) + { + case CertType::kRcac: { + X509CertRequestParams rcacRequest = { serialNumber, now, now + validity, certDn, certDn }; + ReturnErrorOnFailure(NewRootX509Cert(rcacRequest, issuerKeypair, paddedDerSpan)); + break; + } + case CertType::kIcac: { + X509CertRequestParams icacRequest = { serialNumber, now, now + validity, certDn, issuerDn }; + ReturnErrorOnFailure(NewICAX509Cert(icacRequest, subjectPublicKey, issuerKeypair, paddedDerSpan)); + break; + } + case CertType::kNoc: { + X509CertRequestParams nocRequest = { serialNumber, now, now + validity, certDn, issuerDn }; + ReturnErrorOnFailure(NewNodeOperationalX509Cert(nocRequest, subjectPublicKey, issuerKeypair, paddedDerSpan)); + break; + } + default: + return CHIP_ERROR_INVALID_ARGUMENT; + } + + ReturnErrorOnFailure(ConvertX509CertToChipCert(paddedDerSpan, paddedTlvSpan)); + + ChipLogProgress(Controller, "Generated maximized certificate with %u DER bytes, %u TLV bytes", + static_cast(paddedDerSpan.size()), static_cast(paddedTlvSpan.size())); + if (paddedDerSpan.size() <= kMaxDERCertLength && paddedTlvSpan.size() <= kMaxCHIPCertLength) + { + return CopySpanToMutableSpan(paddedDerSpan, outX509Cert); + } + } + } + + return CopySpanToMutableSpan(derSpan, outX509Cert); +} + +} // namespace + CHIP_ERROR ExampleOperationalCredentialsIssuer::Initialize(PersistentStorageDelegate & storage) { using namespace ASN1; @@ -122,6 +243,12 @@ CHIP_ERROR ExampleOperationalCredentialsIssuer::GenerateNOCChainAfterValidation( uint16_t rcacBufLen = static_cast(std::min(rcac.size(), static_cast(UINT16_MAX))); PERSISTENT_KEY_OP(mIndex, kOperationalCredentialsRootCertificateStorage, key, err = mStorage->SyncGetKeyValue(key, rcac.data(), rcacBufLen)); + // Always regenerate RCAC on maximally sized certs. The keys remain the same, so everything is fine. + if (mUseMaximallySizedCerts) + { + err = CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND; + } + if (err == CHIP_NO_ERROR) { uint64_t rcacId; @@ -137,10 +264,14 @@ CHIP_ERROR ExampleOperationalCredentialsIssuer::GenerateNOCChainAfterValidation( ReturnErrorOnFailure(rcac_dn.AddAttribute_MatterRCACId(mIssuerId)); ChipLogProgress(Controller, "Generating RCAC"); - X509CertRequestParams rcac_request = { 0, mNow, mNow + mValidity, rcac_dn, rcac_dn }; - ReturnErrorOnFailure(NewRootX509Cert(rcac_request, mIssuer, rcac)); - + ReturnErrorOnFailure(IssueX509Cert(mNow, mValidity, rcac_dn, rcac_dn, CertType::kRcac, mUseMaximallySizedCerts, + mIssuer.Pubkey(), mIssuer, rcac)); VerifyOrReturnError(CanCastTo(rcac.size()), CHIP_ERROR_INTERNAL); + + // Re-extract DN based on final generated cert + rcac_dn = ChipDN{}; + ReturnErrorOnFailure(ExtractSubjectDNFromX509Cert(rcac, rcac_dn)); + PERSISTENT_KEY_OP(mIndex, kOperationalCredentialsRootCertificateStorage, key, ReturnErrorOnFailure(mStorage->SyncSetKeyValue(key, rcac.data(), static_cast(rcac.size())))); } @@ -149,6 +280,11 @@ CHIP_ERROR ExampleOperationalCredentialsIssuer::GenerateNOCChainAfterValidation( uint16_t icacBufLen = static_cast(std::min(icac.size(), static_cast(UINT16_MAX))); PERSISTENT_KEY_OP(mIndex, kOperationalCredentialsIntermediateCertificateStorage, key, err = mStorage->SyncGetKeyValue(key, icac.data(), icacBufLen)); + // Always regenerate ICAC on maximally sized certs. The keys remain the same, so everything is fine. + if (mUseMaximallySizedCerts) + { + err = CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND; + } if (err == CHIP_NO_ERROR) { uint64_t icacId; @@ -164,10 +300,14 @@ CHIP_ERROR ExampleOperationalCredentialsIssuer::GenerateNOCChainAfterValidation( ReturnErrorOnFailure(icac_dn.AddAttribute_MatterICACId(mIntermediateIssuerId)); ChipLogProgress(Controller, "Generating ICAC"); - X509CertRequestParams icac_request = { 0, mNow, mNow + mValidity, icac_dn, rcac_dn }; - ReturnErrorOnFailure(NewICAX509Cert(icac_request, mIntermediateIssuer.Pubkey(), mIssuer, icac)); - + ReturnErrorOnFailure(IssueX509Cert(mNow, mValidity, rcac_dn, icac_dn, CertType::kIcac, mUseMaximallySizedCerts, + mIntermediateIssuer.Pubkey(), mIssuer, icac)); VerifyOrReturnError(CanCastTo(icac.size()), CHIP_ERROR_INTERNAL); + + // Re-extract DN based on final generated cert + icac_dn = ChipDN{}; + ReturnErrorOnFailure(ExtractSubjectDNFromX509Cert(icac, icac_dn)); + PERSISTENT_KEY_OP(mIndex, kOperationalCredentialsIntermediateCertificateStorage, key, ReturnErrorOnFailure(mStorage->SyncSetKeyValue(key, icac.data(), static_cast(icac.size())))); } @@ -178,8 +318,8 @@ CHIP_ERROR ExampleOperationalCredentialsIssuer::GenerateNOCChainAfterValidation( ReturnErrorOnFailure(noc_dn.AddCATs(cats)); ChipLogProgress(Controller, "Generating NOC"); - X509CertRequestParams noc_request = { 1, mNow, mNow + mValidity, noc_dn, icac_dn }; - return NewNodeOperationalX509Cert(noc_request, pubkey, mIntermediateIssuer, noc); + return IssueX509Cert(mNow, mValidity, icac_dn, noc_dn, CertType::kNoc, mUseMaximallySizedCerts, pubkey, mIntermediateIssuer, + noc); } CHIP_ERROR ExampleOperationalCredentialsIssuer::GenerateNOCChain(const ByteSpan & csrElements, const ByteSpan & csrNonce, @@ -227,16 +367,16 @@ CHIP_ERROR ExampleOperationalCredentialsIssuer::GenerateNOCChain(const ByteSpan ReturnErrorOnFailure(VerifyCertificateSigningRequest(csr.data(), csr.size(), pubkey)); chip::Platform::ScopedMemoryBuffer noc; - ReturnErrorCodeIf(!noc.Alloc(kMaxCHIPDERCertLength), CHIP_ERROR_NO_MEMORY); - MutableByteSpan nocSpan(noc.Get(), kMaxCHIPDERCertLength); + ReturnErrorCodeIf(!noc.Alloc(kMaxDERCertLength), CHIP_ERROR_NO_MEMORY); + MutableByteSpan nocSpan(noc.Get(), kMaxDERCertLength); chip::Platform::ScopedMemoryBuffer icac; - ReturnErrorCodeIf(!icac.Alloc(kMaxCHIPDERCertLength), CHIP_ERROR_NO_MEMORY); - MutableByteSpan icacSpan(icac.Get(), kMaxCHIPDERCertLength); + ReturnErrorCodeIf(!icac.Alloc(kMaxDERCertLength), CHIP_ERROR_NO_MEMORY); + MutableByteSpan icacSpan(icac.Get(), kMaxDERCertLength); chip::Platform::ScopedMemoryBuffer rcac; - ReturnErrorCodeIf(!rcac.Alloc(kMaxCHIPDERCertLength), CHIP_ERROR_NO_MEMORY); - MutableByteSpan rcacSpan(rcac.Get(), kMaxCHIPDERCertLength); + ReturnErrorCodeIf(!rcac.Alloc(kMaxDERCertLength), CHIP_ERROR_NO_MEMORY); + MutableByteSpan rcacSpan(rcac.Get(), kMaxDERCertLength); ReturnErrorOnFailure( GenerateNOCChainAfterValidation(assignedId, mNextFabricId, chip::kUndefinedCATs, pubkey, rcacSpan, icacSpan, nocSpan)); diff --git a/src/controller/ExampleOperationalCredentialsIssuer.h b/src/controller/ExampleOperationalCredentialsIssuer.h index a85684cf6957e2..6e3b1e554d4288 100644 --- a/src/controller/ExampleOperationalCredentialsIssuer.h +++ b/src/controller/ExampleOperationalCredentialsIssuer.h @@ -65,6 +65,8 @@ class DLL_EXPORT ExampleOperationalCredentialsIssuer : public OperationalCredent mNodeIdRequested = true; } + void SetMaximallyLargeCertsUsed(bool areMaximallyLargeCertsUsed) { mUseMaximallySizedCerts = areMaximallyLargeCertsUsed; } + void SetFabricIdForNextNOCRequest(FabricId fabricId) override { mNextFabricId = fabricId; } /** @@ -108,8 +110,8 @@ class DLL_EXPORT ExampleOperationalCredentialsIssuer : public OperationalCredent Crypto::P256Keypair mIssuer; Crypto::P256Keypair mIntermediateIssuer; bool mInitialized = false; - uint32_t mIssuerId = 0; - uint32_t mIntermediateIssuerId = 1; + uint32_t mIssuerId = 1; + uint32_t mIntermediateIssuerId = 2; uint32_t mNow = 0; // By default, let's set validity to 10 years @@ -117,6 +119,7 @@ class DLL_EXPORT ExampleOperationalCredentialsIssuer : public OperationalCredent NodeId mNextAvailableNodeId = 1; PersistentStorageDelegate * mStorage = nullptr; + bool mUseMaximallySizedCerts = false; NodeId mNextRequestedNodeId = 1; FabricId mNextFabricId = 1; diff --git a/src/controller/python/OpCredsBinding.cpp b/src/controller/python/OpCredsBinding.cpp index e6ed27a6da1db3..cb714ae7385fc3 100644 --- a/src/controller/python/OpCredsBinding.cpp +++ b/src/controller/python/OpCredsBinding.cpp @@ -77,6 +77,8 @@ class OperationalCredentialsAdapter : public OperationalCredentialsDelegate return mExampleOpCredsIssuer.GenerateNOCChainAfterValidation(nodeId, fabricId, cats, pubKey, rcac, icac, noc); } + void SetMaximallyLargeCertsUsed(bool enabled) { mExampleOpCredsIssuer.SetMaximallyLargeCertsUsed(enabled); } + private: CHIP_ERROR GenerateNOCChain(const ByteSpan & csrElements, const ByteSpan & csrNonce, const ByteSpan & attestationSignature, const ByteSpan & attestationChallenge, const ByteSpan & DAC, const ByteSpan & PAI, @@ -360,9 +362,10 @@ ChipError::StorageType pychip_OpCreds_AllocateController(OpCredsContext * contex CATValues catValues; - if ((caseAuthTagLen + 1) > kMaxSubjectCATAttributeCount) + if (caseAuthTagLen > kMaxSubjectCATAttributeCount) { - ChipLogError(Controller, "# of CASE Tags exceeds kMaxSubjectCATAttributeCount"); + ChipLogError(Controller, "Too many of CASE Tags (%u) exceeds kMaxSubjectCATAttributeCount", + static_cast(caseAuthTagLen)); return CHIP_ERROR_INVALID_ARGUMENT.AsInteger(); } @@ -414,6 +417,15 @@ ChipError::StorageType pychip_OpCreds_AllocateController(OpCredsContext * contex return CHIP_NO_ERROR.AsInteger(); } +ChipError::StorageType pychip_OpCreds_SetMaximallyLargeCertsUsed(OpCredsContext * context, bool enabled) +{ + VerifyOrReturnError(context != nullptr && context->mAdapter != nullptr, CHIP_ERROR_INCORRECT_STATE.AsInteger()); + + context->mAdapter->SetMaximallyLargeCertsUsed(enabled); + + return CHIP_NO_ERROR.AsInteger(); +} + void pychip_OpCreds_FreeDelegate(OpCredsContext * context) { Platform::Delete(context); diff --git a/src/controller/python/chip/CertificateAuthority.py b/src/controller/python/chip/CertificateAuthority.py index aa9011fb60a250..7f40f0cd016100 100644 --- a/src/controller/python/chip/CertificateAuthority.py +++ b/src/controller/python/chip/CertificateAuthority.py @@ -45,7 +45,7 @@ class CertificateAuthority: Each CertificateAuthority instance is associated with a single instance of the OperationalCredentialsAdapter. This adapter instance implements the OperationalCredentialsDelegate and is meant to provide a Python adapter to the functions in that delegate. It relies on the in-built - ExampleOperationalCredentialsIssuer to then generate certificate material for the CA. This instance also uses the 'CA index' to + ExampleOperationalCredentialsIssuer to then generate certificate material for the CA. This instance also uses the 'CA index' to store/look-up the associated credential material from the provided PersistentStorage object. ''' @classmethod @@ -74,10 +74,14 @@ def __init__(self, chipStack: ChipStack.ChipStack, caIndex: int, persistentStora self._Handle().pychip_OpCreds_InitializeDelegate.restype = c_void_p self._Handle().pychip_OpCreds_InitializeDelegate.argtypes = [ctypes.py_object, ctypes.c_uint32, ctypes.c_void_p] + self._Handle().pychip_OpCreds_SetMaximallyLargeCertsUsed.restype = c_uint32 + self._Handle().pychip_OpCreds_SetMaximallyLargeCertsUsed.argtypes = [ctypes.c_void_p, ctypes.c_bool] + if (persistentStorage is None): persistentStorage = self._chipStack.GetStorageManager() self._persistentStorage = persistentStorage + self._maximizeCertChains = False self._closure = self._chipStack.Call( lambda: self._Handle().pychip_OpCreds_InitializeDelegate( @@ -181,6 +185,21 @@ def caIndex(self) -> int: def adminList(self) -> list[FabricAdmin.FabricAdmin]: return self._activeAdmins + @property + def maximizeCertChains(self) -> bool: + return self._maximizeCertChains + + @maximizeCertChains.setter + def maximizeCertChains(self, enabled: bool): + res = self._chipStack.Call( + lambda: self._Handle().pychip_OpCreds_SetMaximallyLargeCertsUsed(ctypes.c_void_p(self._closure), ctypes.c_bool(enabled)) + ) + + if res != 0: + raise self._chipStack.ErrorToException(res) + + self._maximizeCertChains = enabled + def __del__(self): self.Shutdown() @@ -243,7 +262,7 @@ def LoadAuthoritiesFromStorage(self): ca = self.NewCertificateAuthority(int(caIndex)) ca.LoadFabricAdminsFromStorage() - def NewCertificateAuthority(self, caIndex: int = None): + def NewCertificateAuthority(self, caIndex: int = None, maximizeCertChains: bool = False): ''' Creates a new CertificateAuthority instance with the provided CA Index and the PersistentStorage instance previously setup in the constructor. @@ -268,6 +287,7 @@ def NewCertificateAuthority(self, caIndex: int = None): self._persistentStorage.SetReplKey(key='caList', value=caList) ca = CertificateAuthority(chipStack=self._chipStack, caIndex=caIndex, persistentStorage=self._persistentStorage) + ca.maximizeCertChains = maximizeCertChains self._activeCaList.append(ca) return ca diff --git a/src/controller/python/chip/FabricAdmin.py b/src/controller/python/chip/FabricAdmin.py index 3cfe03ffdb7e95..97a729035f811e 100644 --- a/src/controller/python/chip/FabricAdmin.py +++ b/src/controller/python/chip/FabricAdmin.py @@ -102,7 +102,7 @@ def NewController(self, nodeId: int = None, paaTrustStorePath: str = "", useTest raise RuntimeError(f"Provided NodeId {nodeId} collides with an existing controller instance!") self.logger().warning( - f"Allocating new controller with CaIndex: {self._certificateAuthority.caIndex}, FabricId: 0x{self._fabricId:016X}, NodeId: 0x{nodeId:016X}") + f"Allocating new controller with CaIndex: {self._certificateAuthority.caIndex}, FabricId: 0x{self._fabricId:016X}, NodeId: 0x{nodeId:016X}, CatTags: {catTags}") controller = ChipDeviceCtrl.ChipDeviceController(opCredsContext=self._certificateAuthority.GetOpCredsContext(), fabricId=self._fabricId, nodeId=nodeId, adminVendorId=self._vendorId, paaTrustStorePath=paaTrustStorePath, useTestCommissioner=useTestCommissioner, fabricAdmin=self, catTags=catTags) diff --git a/src/controller/python/chip/utils/CommissioningBuildingBlocks.py b/src/controller/python/chip/utils/CommissioningBuildingBlocks.py index ae4da4a4ee1fa8..20dbcd6441a746 100644 --- a/src/controller/python/chip/utils/CommissioningBuildingBlocks.py +++ b/src/controller/python/chip/utils/CommissioningBuildingBlocks.py @@ -30,7 +30,7 @@ _UINT16_MAX = 65535 -logger = logging.getLogger() +logger = logging.getLogger('CommissioningBuildingBlocks') async def _IsNodeInFabricList(devCtrl, nodeId): @@ -43,7 +43,7 @@ async def _IsNodeInFabricList(devCtrl, nodeId): return False -async def GrantPrivilege(adminCtrl: ChipDeviceController, grantedCtrl: ChipDeviceController, privilege: Clusters.AccessControl.Enums.Privilege, targetNodeId: int): +async def GrantPrivilege(adminCtrl: ChipDeviceController, grantedCtrl: ChipDeviceController, privilege: Clusters.AccessControl.Enums.Privilege, targetNodeId: int, targetCatTags: typing.List[int] = []): ''' Given an existing controller with admin privileges over a target node, grants the specified privilege to the new ChipDeviceController instance to the entire Node. This is achieved by updating the ACL entries on the target. @@ -53,20 +53,29 @@ async def GrantPrivilege(adminCtrl: ChipDeviceController, grantedCtrl: ChipDevic Args: adminCtrl: ChipDeviceController instance with admin privileges over the target node grantedCtrl: ChipDeviceController instance that is being granted the new privilege. - privilege: Privilege to grant to the granted controller + privilege: Privilege to grant to the granted controller. If None, no privilege is granted. targetNodeId: Target node to which the controller is granted privilege. + targetCatTag: Target 32-bit CAT tag that is granted privilege. If provided, this will be used in the subject list instead of the nodeid of that of grantedCtrl. ''' - data = await adminCtrl.ReadAttribute(targetNodeId, [(Clusters.AccessControl.Attributes.Acl)]) if 0 not in data: raise ValueError("Did not get back any data (possible cause: controller has no access..") currentAcls = data[0][Clusters.AccessControl][Clusters.AccessControl.Attributes.Acl] + if len(targetCatTags) != 0: + # Convert to an ACL subject format in CAT range + targetSubjects = [tag | 0xFFFF_FFFD_0000_0000 for tag in targetCatTags] + else: + targetSubjects = [grantedCtrl.nodeId] + + if (len(targetSubjects) > 4): + raise ValueError(f"List of target subjects of len {len(targetSubjects)} exceeeded the minima of 4!") + # Step 1: Wipe the subject from all existing ACLs. for acl in currentAcls: if (acl.subjects != NullValue): - acl.subjects = [subject for subject in acl.subjects if subject != grantedCtrl.nodeId] + acl.subjects = [subject for subject in acl.subjects if subject not in targetSubjects] if (privilege): addedPrivilege = False @@ -75,9 +84,11 @@ async def GrantPrivilege(adminCtrl: ChipDeviceController, grantedCtrl: ChipDevic # the existing privilege in that entry matches our desired privilege. for acl in currentAcls: if acl.privilege == privilege: - if grantedCtrl.nodeId not in acl.subjects: - acl.subjects.append(grantedCtrl.nodeId) + subjectSet = set(acl.subjects) + subjectSet.update(targetSubjects) + acl.subjects = list(subjectSet) addedPrivilege = True + break # Step 3: If there isn't an existing entry to add to, make a new one. if (not(addedPrivilege)): @@ -86,14 +97,16 @@ async def GrantPrivilege(adminCtrl: ChipDeviceController, grantedCtrl: ChipDevic f"Cannot add another ACL entry to grant privilege to existing count of {currentAcls} ACLs -- will exceed minimas!") currentAcls.append(Clusters.AccessControl.Structs.AccessControlEntry(privilege=privilege, authMode=Clusters.AccessControl.Enums.AuthMode.kCase, - subjects=[grantedCtrl.nodeId])) + subjects=targetSubjects)) # Step 4: Prune ACLs which have empty subjects. currentAcls = [acl for acl in currentAcls if acl.subjects != NullValue and len(acl.subjects) != 0] + + logger.info(f'GrantPrivilege: Writing acls: {currentAcls}') await adminCtrl.WriteAttribute(targetNodeId, [(0, Clusters.AccessControl.Attributes.Acl(currentAcls))]) -async def CreateControllersOnFabric(fabricAdmin: FabricAdmin, adminDevCtrl: ChipDeviceController, controllerNodeIds: typing.List[int], privilege: Clusters.AccessControl.Enums.Privilege, targetNodeId: int) -> typing.List[ChipDeviceController]: +async def CreateControllersOnFabric(fabricAdmin: FabricAdmin, adminDevCtrl: ChipDeviceController, controllerNodeIds: typing.List[int], privilege: Clusters.AccessControl.Enums.Privilege, targetNodeId: int, catTags: typing.List[int] = []) -> typing.List[ChipDeviceController]: ''' Create new ChipDeviceController instances on a given fabric with a specific privilege on a target node. Args: @@ -102,13 +115,14 @@ async def CreateControllersOnFabric(fabricAdmin: FabricAdmin, adminDevCtrl: Chip controllerNodeIds: List of desired nodeIds for the controllers. privilege: The specific ACL privilege to grant to the newly minted controllers. targetNodeId: The Node ID of the target. + catTags: CAT Tags to include in the NOC of controller, as well as when setting up the ACLs on the target. ''' controllerList = [] for nodeId in controllerNodeIds: - newController = fabricAdmin.NewController(nodeId=nodeId) - await GrantPrivilege(adminDevCtrl, newController, privilege, targetNodeId) + newController = fabricAdmin.NewController(nodeId=nodeId, catTags=catTags) + await GrantPrivilege(adminDevCtrl, newController, privilege, targetNodeId, catTags) controllerList.append(newController) return controllerList diff --git a/src/controller/python/test/test_scripts/base.py b/src/controller/python/test/test_scripts/base.py index 8488b2e80b6ce6..8665c288276603 100644 --- a/src/controller/python/test/test_scripts/base.py +++ b/src/controller/python/test/test_scripts/base.py @@ -40,6 +40,7 @@ import copy import secrets import faulthandler +import ipdb logger = logging.getLogger('PythonMatterControllerTEST') logger.setLevel(logging.INFO) @@ -389,31 +390,28 @@ def TestFailsafe(self, nodeid: int): async def TestControllerCATValues(self, nodeid: int): ''' This tests controllers using CAT Values ''' - # Allocate a new controller instance with a CAT tag. - newController = self.fabricAdmin.NewController(nodeId=300, catTags=[0x00010001]) + newControllers = await CommissioningBuildingBlocks.CreateControllersOnFabric(fabricAdmin=self.fabricAdmin, adminDevCtrl=self.devCtrl, controllerNodeIds=[300], targetNodeId=nodeid, privilege=None, catTags=[0x0001_0001]) # Read out an attribute using the new controller. It has no privileges, so this should fail with an UnsupportedAccess error. - res = await newController.ReadAttribute(nodeid=nodeid, attributes=[(0, Clusters.AccessControl.Attributes.Acl)]) + res = await newControllers[0].ReadAttribute(nodeid=nodeid, attributes=[(0, Clusters.AccessControl.Attributes.Acl)]) if(res[0][Clusters.AccessControl][Clusters.AccessControl.Attributes.Acl].Reason.status != IM.Status.UnsupportedAccess): self.logger.error(f"1: Received data instead of an error:{res}") return False - # Do a read-modify-write operation on the ACL list to add the CAT tag to the ACL list. - aclList = (await self.devCtrl.ReadAttribute(nodeid, [(0, Clusters.AccessControl.Attributes.Acl)]))[0][Clusters.AccessControl][Clusters.AccessControl.Attributes.Acl] - origAclList = copy.deepcopy(aclList) - aclList[0].subjects.append(0xFFFFFFFD00010001) - await self.devCtrl.WriteAttribute(nodeid, [(0, Clusters.AccessControl.Attributes.Acl(aclList))]) + # Grant the new controller privilege by adding the CAT tag to the subject. + await CommissioningBuildingBlocks.GrantPrivilege(adminCtrl=self.devCtrl, grantedCtrl=newControllers[0], privilege=Clusters.AccessControl.Enums.Privilege.kAdminister, targetNodeId=nodeid, targetCatTags=[0x0001_0001]) # Read out the attribute again - this time, it should succeed. - res = await newController.ReadAttribute(nodeid=nodeid, attributes=[(0, Clusters.AccessControl.Attributes.Acl)]) + res = await newControllers[0].ReadAttribute(nodeid=nodeid, attributes=[(0, Clusters.AccessControl.Attributes.Acl)]) if (type(res[0][Clusters.AccessControl][Clusters.AccessControl.Attributes.Acl][0]) != Clusters.AccessControl.Structs.AccessControlEntry): self.logger.error(f"2: Received something other than data:{res}") return False - # Write back the old entry to reset ACL list back. - await self.devCtrl.WriteAttribute(nodeid, [(0, Clusters.AccessControl.Attributes.Acl(origAclList))]) - newController.Shutdown() + # Reset the privilege back to pre-test. + await CommissioningBuildingBlocks.GrantPrivilege(adminCtrl=self.devCtrl, grantedCtrl=newControllers[0], privilege=None, targetNodeId=nodeid) + + newControllers[0].Shutdown() return True diff --git a/src/python_testing/TC_RR_1_1.py b/src/python_testing/TC_RR_1_1.py new file mode 100644 index 00000000000000..00dd9a111b8308 --- /dev/null +++ b/src/python_testing/TC_RR_1_1.py @@ -0,0 +1,424 @@ +# +# Copyright (c) 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. +# + +from matter_testing_support import MatterBaseTest, default_matter_test_main, async_test_body +import chip.clusters as Clusters +import chip.FabricAdmin +import chip.CertificateAuthority +import logging +from mobly import asserts +from chip.utils import CommissioningBuildingBlocks +from chip.clusters.Attribute import TypedAttributePath, SubscriptionTransaction, AttributeStatus +from chip.interaction_model import Status as StatusEnum +import queue +import asyncio +from binascii import hexlify +from threading import Event +import time +import random + +from TC_SC_3_6 import AttributeChangeAccumulator, ResubscriptionCatcher + +# TODO: Overall, we need to add validation that session IDs have not changed throughout to be agnostic +# to some internal behavior assumptions of the SDK we are making relative to the write to +# the trigger the subscriptions not re-opening a new CASE session +# + + +class TC_RR_1_1(MatterBaseTest): + def setup_class(self): + self._pseudo_random_generator = random.Random(1234) + self._subscriptions = [] + + def teardown_class(self): + logging.info("Teardown: shutting down all subscription to avoid racy callbacks") + for subscription in self._subscriptions: + subscription.Shutdown() + + @async_test_body + async def test_TC_RR_1_1(self): + dev_ctrl = self.default_controller + + # Debug/test arguments + + # Get overrides for debugging the test + num_fabrics_to_commission = self.user_params.get("num_fabrics_to_commission", 5) + num_controllers_per_fabric = self.user_params.get("num_controllers_per_fabric", 3) + # Immediate reporting + min_report_interval_sec = self.user_params.get("min_report_interval_sec", 0) + # 10 minutes max reporting interval --> We don't care about keep-alives per-se and + # want to avoid resubscriptions + max_report_interval_sec = self.user_params.get("max_report_interval_sec", 10 * 60) + # Time to wait after changing NodeLabel for subscriptions to all hit. This is dependant + # on MRP params of subscriber and on actual min_report_interval. + # TODO: Determine the correct max value depending on target. Test plan doesn't say! + timeout_delay_sec = self.user_params.get("timeout_delay_sec", max_report_interval_sec * 2) + # Whether to skip filling the UserLabel clusters + skip_user_label_cluster_steps = self.user_params.get("skip_user_label_cluster_steps", False) + + BEFORE_LABEL = "Before Subscriptions 12345678912" + AFTER_LABEL = "After Subscriptions 123456789123" + + # Pre-conditions + + # Make sure all certificates are installed with maximal size + dev_ctrl.fabricAdmin.certificateAuthority.maximizeCertChains = True + + # TODO: Do from PICS list. The reflection approach here what a real client would do, + # and it respects what the test says: "TH writes 4 entries per endpoint where LabelList is supported" + logging.info("Pre-condition: determine whether any endpoints have UserLabel cluster (ULABEL.S.A0000(LabelList))") + endpoints_with_user_label_list = await dev_ctrl.ReadAttribute(self.dut_node_id, [Clusters.UserLabel.Attributes.LabelList]) + has_user_labels = len(endpoints_with_user_label_list) > 0 + if has_user_labels: + logging.info("--> User label cluster present on endpoints %s" % + ", ".join(["%d" % ep for ep in endpoints_with_user_label_list.keys()])) + else: + logging.info("--> User label cluster not present on any endpoitns") + + # Generate list of all clients names + all_names = [] + for fabric_idx in range(num_fabrics_to_commission): + for controller_idx in range(num_controllers_per_fabric): + all_names.append("RD%d%s" % (fabric_idx + 1, chr(ord('A') + controller_idx))) + logging.info(f"Client names that will be used: {all_names}") + client_list = [] + + # TODO: Shall we also verify SupportedFabrics attribute, and the CapabilityMinima attribute? + logging.info("Pre-conditions: validate CapabilityMinima.CaseSessionsPerFabric >= 3") + + capability_minima = await self.read_single_attribute(dev_ctrl, node_id=self.dut_node_id, endpoint=0, attribute=Clusters.Basic.Attributes.CapabilityMinima) + asserts.assert_greater_equal(capability_minima.caseSessionsPerFabric, 3) + + # Step 1: Commission 5 fabrics with maximized NOC chains + logging.info(f"Step 1: use existing fabric to configure new fabrics so that total is {num_fabrics_to_commission} fabrics") + + # Generate Node IDs for subsequent controllers start at 200, follow 200, 300, ... + node_ids = [200 + (i * 100) for i in range(num_controllers_per_fabric - 1)] + + # Prepare clients for first fabric, that includes the default controller + dev_ctrl.name = all_names.pop(0) + client_list.append(dev_ctrl) + + if num_controllers_per_fabric > 1: + new_controllers = await CommissioningBuildingBlocks.CreateControllersOnFabric(fabricAdmin=dev_ctrl.fabricAdmin, adminDevCtrl=dev_ctrl, controllerNodeIds=node_ids, privilege=Clusters.AccessControl.Enums.Privilege.kAdminister, targetNodeId=self.dut_node_id, catTags=[0x0001_0001]) + for controller in new_controllers: + controller.name = all_names.pop(0) + client_list.extend(new_controllers) + + # Prepare clients for subsequent fabrics + for i in range(num_fabrics_to_commission - 1): + admin_index = 2 + i + logging.info("Commissioning fabric %d/%d" % (admin_index, num_fabrics_to_commission)) + new_certificate_authority = self.certificate_authority_manager.NewCertificateAuthority() + new_fabric_admin = new_certificate_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=admin_index) + + new_admin_ctrl = new_fabric_admin.NewController(nodeId=dev_ctrl.nodeId, catTags=[0x0001_0001]) + new_admin_ctrl.name = all_names.pop(0) + client_list.append(new_admin_ctrl) + await CommissioningBuildingBlocks.AddNOCForNewFabricFromExisting(commissionerDevCtrl=dev_ctrl, newFabricDevCtrl=new_admin_ctrl, existingNodeId=self.dut_node_id, newNodeId=self.dut_node_id) + + if num_controllers_per_fabric > 1: + new_controllers = await CommissioningBuildingBlocks.CreateControllersOnFabric(fabricAdmin=new_fabric_admin, adminDevCtrl=new_admin_ctrl, + controllerNodeIds=node_ids, privilege=Clusters.AccessControl.Enums.Privilege.kAdminister, targetNodeId=self.dut_node_id, catTags=[0x0001_0001]) + for controller in new_controllers: + controller.name = all_names.pop(0) + + client_list.extend(new_controllers) + + asserts.assert_equal(len(client_list), num_fabrics_to_commission * + num_controllers_per_fabric, "Must have the right number of clients") + + client_by_name = {client.name: client for client in client_list} + + # Step 2: Set the Label field for each fabric and BasicInformation.NodeLabel to 32 characters + logging.info("Step 2: Setting the Label field for each fabric and BasicInformation.NodeLabel to 32 characters") + + for idx in range(num_fabrics_to_commission): + fabric_number = idx + 1 + # Client is client A for each fabric to set the Label field + client_name = "RD%dA" % fabric_number + client = client_by_name[client_name] + + # Send the UpdateLabel command + label = ("%d" % fabric_number) * 32 + logging.info("Step 2a: Setting fabric label on fabric %d to '%s' using client %s" % (fabric_number, label, client_name)) + await client.SendCommand(self.dut_node_id, 0, Clusters.OperationalCredentials.Commands.UpdateFabricLabel(label)) + + # Read back + fabric_metadata = await self.read_single_attribute(client, node_id=self.dut_node_id, endpoint=0, attribute=Clusters.OperationalCredentials.Attributes.Fabrics) + print(fabric_metadata) + asserts.assert_equal(fabric_metadata[0].label, label, "Fabrics[x].label must match what was written") + + # Before subscribing, set the NodeLabel to "Before Subscriptions" + logging.info(f"Step 2b: Set BasicInformation.NodeLabel to {BEFORE_LABEL}") + await client_list[0].WriteAttribute(self.dut_node_id, [(0, Clusters.Basic.Attributes.NodeLabel(value=BEFORE_LABEL))]) + + node_label = await self.read_single_attribute(client, node_id=self.dut_node_id, endpoint=0, attribute=Clusters.Basic.Attributes.NodeLabel) + asserts.assert_equal(node_label, BEFORE_LABEL, "NodeLabel must match what was written") + + # Step 3: Add 3 Access Control entries on DUT with a list of 4 Subjects and 3 Targets with the following parameters (...) + logging.info("Step 3: Fill ACL table so that all minimas are reached") + + for idx in range(num_fabrics_to_commission): + fabric_number = idx + 1 + # Client is client A for each fabric + client_name = "RD%dA" % fabric_number + client = client_by_name[client_name] + + acl = self.build_acl(fabric_number, client_by_name, num_controllers_per_fabric) + + logging.info(f"Step 3a: Writing ACL entry for fabric {fabric_number}") + await client.WriteAttribute(self.dut_node_id, [(0, Clusters.AccessControl.Attributes.Acl(acl))]) + + logging.info(f"Step 3b: Validating ACL entry for fabric {fabric_number}") + acl_readback = await self.read_single_attribute(client, node_id=self.dut_node_id, endpoint=0, attribute=Clusters.AccessControl.Attributes.Acl) + fabric_index = 9999 + for entry in acl_readback: + asserts.assert_equal(entry.fabricIndex, fabric_number, "Fabric Index of response entries must match") + fabric_index = entry.fabricIndex + + for entry in acl: + # Fix-up the original ACL list items (that all had fabricIndex of 0 on write, since ignored) + # so that they match incoming fabric index. Allows checking by equality of the structs + entry.fabricIndex = fabric_index + asserts.assert_equal(acl_readback, acl, "ACL must match what was written") + + # Step 4 and 5 (the operations cannot be separated): establish all CASE sessions and subscriptions + + # Subscribe with all clients to NodeLabel attribute and 2 more paths + sub_handlers = [] + resub_catchers = [] + output_queue = queue.Queue() + subscription_contents = [ + (0, Clusters.Basic.Attributes.NodeLabel), # Single attribute + (0, Clusters.OperationalCredentials), # Wildcard all of opcreds attributes on EP0 + Clusters.Descriptor # All descriptors on all endpoints + ] + + logging.info("Step 4 and 5 (first part): Establish subscription with all %d clients" % len(client_list)) + for sub_idx, client in enumerate(client_list): + logging.info("Establishing subscription %d/%d from controller node %s" % (sub_idx + 1, len(client_list), client.name)) + + sub = await client.ReadAttribute(nodeid=self.dut_node_id, attributes=subscription_contents, + reportInterval=(min_report_interval_sec, max_report_interval_sec), keepSubscriptions=False) + self._subscriptions.append(sub) + + attribute_handler = AttributeChangeAccumulator( + name=client.name, expected_attribute=Clusters.Basic.Attributes.NodeLabel, output=output_queue) + sub.SetAttributeUpdateCallback(attribute_handler) + sub_handlers.append(attribute_handler) + + # TODO: Replace resubscription catcher with API to disable re-subscription on failure + resub_catcher = ResubscriptionCatcher(name=client.name) + sub.SetResubscriptionAttemptedCallback(resub_catcher) + resub_catchers.append(resub_catcher) + + asserts.assert_equal(len(self._subscriptions), len(client_list), "Must have the right number of subscriptions") + + # Step 6: Read 9 paths and validate success + logging.info("Step 6: Read 9 paths (first 9 attributes of Basic Information cluster) and validate success") + + large_read_contents = [ + Clusters.Basic.Attributes.DataModelRevision, + Clusters.Basic.Attributes.VendorName, + Clusters.Basic.Attributes.VendorID, + Clusters.Basic.Attributes.ProductName, + Clusters.Basic.Attributes.ProductID, + Clusters.Basic.Attributes.NodeLabel, + Clusters.Basic.Attributes.Location, + Clusters.Basic.Attributes.HardwareVersion, + Clusters.Basic.Attributes.HardwareVersionString, + ] + large_read_paths = [(0, attrib) for attrib in large_read_contents] + basic_info = await dev_ctrl.ReadAttribute(self.dut_node_id, large_read_paths) + + # Make sure everything came back from the read that we expected + asserts.assert_true(0 in basic_info.keys(), "Must have read endpoint 0 data") + asserts.assert_true(Clusters.Basic in basic_info[0].keys(), "Must have read Basic Information cluster data") + for attribute in large_read_contents: + asserts.assert_true(attribute in basic_info[0][Clusters.Basic], + "Must have read back attribute %s" % (attribute.__name__)) + + # Step 7: Trigger a change on NodeLabel + logging.info( + "Step 7: Change attribute with one client, await all attributes changed successfully without loss of subscriptions") + await asyncio.sleep(1) + await client_list[0].WriteAttribute(self.dut_node_id, [(0, Clusters.Basic.Attributes.NodeLabel(value=AFTER_LABEL))]) + + all_changes = {client.name: False for client in client_list} + + # Await a stabilization delay in increments to let the event loops run + start_time = time.time() + elapsed = 0 + time_remaining = timeout_delay_sec + + while time_remaining > 0: + try: + item = output_queue.get(block=True, timeout=time_remaining) + client_name, endpoint, attribute, value = item['name'], item['endpoint'], item['attribute'], item['value'] + + # Record arrival of an expected subscription change when seen + if endpoint == 0 and attribute == Clusters.Basic.Attributes.NodeLabel and value == AFTER_LABEL: + if not all_changes[client_name]: + logging.info("Got expected attribute change for client %s" % client_name) + all_changes[client_name] = True + + # We are done waiting when we have accumulated all results + if all(all_changes.values()): + logging.info("All clients have reported, done waiting.") + break + except queue.Empty: + # No error, we update timeouts and keep going + pass + + elapsed = time.time() - start_time + time_remaining = timeout_delay_sec - elapsed + + logging.info("Step 7: Validation of results") + sub_test_failed = False + + for catcher in resub_catchers: + if catcher.caught_resubscription: + logging.error("Client %s saw a resubscription" % catcher.name) + sub_test_failed = True + else: + logging.info("Client %s correctly did not see a resubscription" % catcher.name) + + all_reports_gotten = all(all_changes.values()) + if not all_reports_gotten: + logging.error("Missing reports from the following clients: %s" % + ", ".join([name for name, value in all_changes.items() if value is False])) + sub_test_failed = True + else: + logging.info("Got successful reports from all clients, meaning all concurrent CASE sessions worked") + + # Determine result of Step 7 + if sub_test_failed: + asserts.fail("Failed step 7 !") + + # Step 8: Validate sessions have not changed by doing a read on NodeLabel from all clients + logging.info("Step 8: Read back NodeLabel directly from all clients") + for sub_idx, client in enumerate(client_list): + logging.info("Reading NodeLabel (%d/%d) from controller node %s" % (sub_idx + 1, len(client_list), client.name)) + + label_readback = await self.read_single_attribute(client, node_id=self.dut_node_id, endpoint=0, attribute=Clusters.Basic.Attributes.NodeLabel) + asserts.assert_equal(label_readback, AFTER_LABEL) + + # TODO: Compare before/after session IDs. Requires more native changes, and the + # subcription method above is actually good enough we think. + + # Step 9: Fill user label list + if has_user_labels and not skip_user_label_cluster_steps: + await self.fill_user_label_list(dev_ctrl, self.dut_node_id) + else: + logging.info("Step 9: Skipped due to no UserLabel cluster instances") + + def random_string(self, length) -> str: + rnd = self._pseudo_random_generator + return "".join([rnd.choice("abcdef0123456789") for _ in range(length)])[:length] + + async def fill_user_label_list(self, dev_ctrl, target_node_id): + logging.info("Step 9: Fill UserLabel clusters on each endpoint") + user_labels = await dev_ctrl.ReadAttribute(target_node_id, [Clusters.UserLabel]) + + # Build 4 sets of maximized labels + random_label = self.random_string(16) + random_value = self.random_string(16) + labels = [Clusters.UserLabel.Structs.LabelStruct(label=random_label, value=random_value) for _ in range(4)] + + for endpoint_id in user_labels: + clusters = user_labels[endpoint_id] + for cluster in clusters: + if cluster == Clusters.UserLabel: + logging.info("Step 9a: Filling UserLabel cluster on endpoint %d" % endpoint_id) + statuses = await dev_ctrl.WriteAttribute(target_node_id, [(endpoint_id, Clusters.UserLabel.Attributes.LabelList(labels))]) + asserts.assert_equal(statuses[0].Status, StatusEnum.Success, "Label write must succeed") + + logging.info("Step 9b: Validate UserLabel cluster contents after write on endpoint %d" % endpoint_id) + read_back_labels = await self.read_single_attribute(dev_ctrl, node_id=target_node_id, endpoint=endpoint_id, attribute=Clusters.UserLabel.Attributes.LabelList) + print(read_back_labels) + + asserts.assert_equal(read_back_labels, labels, "LabelList attribute must match what was written") + + def build_acl(self, fabric_number, client_by_name, num_controllers_per_fabric): + acl = [] + + # Test says: + # + # . struct + # - Privilege field: Administer (5) + # - AuthMode field: CASE (2) + # - Subjects field: [0xFFFF_FFFD_0001_0001, 0x2000_0000_0000_0001, 0x2000_0000_0000_0002, 0x2000_0000_0000_0003] + # - Targets field: [{Endpoint: 0}, {Cluster: 0xFFF1_FC00, DeviceType: 0xFFF1_FC30}, {Cluster: 0xFFF1_FC00, DeviceType: 0xFFF1_FC31}] + # . struct + # - Privilege field: Manage (4) + # - AuthMode field: CASE (2) + # - Subjects field: [0x1000_0000_0000_0001, 0x1000_0000_0000_0002, 0x1000_0000_0000_0003, 0x1000_0000_0000_0004] + # - Targets field: [{Cluster: 0xFFF1_FC00, DeviceType: 0xFFF1_FC20}, {Cluster: 0xFFF1_FC01, DeviceType: 0xFFF1_FC21}, {Cluster: 0xFFF1_FC02, DeviceType: 0xFFF1_FC22}] + # . struct + # - Privilege field: Operate (3) + # - AuthMode field: CASE (2) + # - Subjects field: [0x3000_0000_0000_0001, 0x3000_0000_0000_0002, 0x3000_0000_0000_0003, 0x3000_0000_0000_0004] + # - Targets field: [{Cluster: 0xFFF1_FC40, DeviceType: 0xFFF1_FC20}, {Cluster: 0xFFF1_FC41, DeviceType: 0xFFF1_FC21}, {Cluster: 0xFFF1_FC02, DeviceType: 0xFFF1_FC42}] + + # Administer ACL entry + admin_subjects = [0xFFFF_FFFD_0001_0001, 0x2000_0000_0000_0001, 0x2000_0000_0000_0002, 0x2000_0000_0000_0003] + + admin_targets = [ + Clusters.AccessControl.Structs.Target(endpoint=0), + Clusters.AccessControl.Structs.Target(cluster=0xFFF1_FC00, deviceType=0xFFF1_BC30), + Clusters.AccessControl.Structs.Target(cluster=0xFFF1_FC01, deviceType=0xFFF1_BC31) + ] + admin_acl_entry = Clusters.AccessControl.Structs.AccessControlEntry(privilege=Clusters.AccessControl.Enums.Privilege.kAdminister, + authMode=Clusters.AccessControl.Enums.AuthMode.kCase, + subjects=admin_subjects, + targets=admin_targets) + acl.append(admin_acl_entry) + + # Manage ACL entry + manage_subjects = [0x1000_0000_0000_0001, 0x1000_0000_0000_0002, 0x1000_0000_0000_0003, 0x1000_0000_0000_0004] + manage_targets = [ + Clusters.AccessControl.Structs.Target(cluster=0xFFF1_FC00, deviceType=0xFFF1_BC20), + Clusters.AccessControl.Structs.Target(cluster=0xFFF1_FC01, deviceType=0xFFF1_BC21), + Clusters.AccessControl.Structs.Target(cluster=0xFFF1_FC02, deviceType=0xFFF1_BC22) + ] + + manage_acl_entry = Clusters.AccessControl.Structs.AccessControlEntry(privilege=Clusters.AccessControl.Enums.Privilege.kManage, + authMode=Clusters.AccessControl.Enums.AuthMode.kCase, + subjects=manage_subjects, + targets=manage_targets) + acl.append(manage_acl_entry) + + # Operate ACL entry + operate_subjects = [0x3000_0000_0000_0001, 0x3000_0000_0000_0002, 0x3000_0000_0000_0003, 0x3000_0000_0000_0004] + operate_targets = [ + Clusters.AccessControl.Structs.Target(cluster=0xFFF1_FC40, deviceType=0xFFF1_BC20), + Clusters.AccessControl.Structs.Target(cluster=0xFFF1_FC41, deviceType=0xFFF1_BC21), + Clusters.AccessControl.Structs.Target(cluster=0xFFF1_FC42, deviceType=0xFFF1_BC42) + ] + + operate_acl_entry = Clusters.AccessControl.Structs.AccessControlEntry(privilege=Clusters.AccessControl.Enums.Privilege.kOperate, + authMode=Clusters.AccessControl.Enums.AuthMode.kCase, + subjects=operate_subjects, + targets=operate_targets) + acl.append(operate_acl_entry) + + return acl + + +if __name__ == "__main__": + default_matter_test_main(maximize_cert_chains=True, controller_cat_tags=[0x0001_0001]) diff --git a/src/python_testing/matter_testing_support.py b/src/python_testing/matter_testing_support.py index 607f24a057b9c3..7c21f0b6471ce8 100644 --- a/src/python_testing/matter_testing_support.py +++ b/src/python_testing/matter_testing_support.py @@ -124,6 +124,7 @@ class MatterTestConfig: ble_interface_id: int = None admin_vendor_id: int = _DEFAULT_ADMIN_VENDOR_ID + case_admin_subject: int = None global_test_params: dict = field(default_factory=dict) # List of explicit tests to run by name. If empty, all tests will run tests: List[str] = field(default_factory=list) @@ -132,6 +133,7 @@ class MatterTestConfig: discriminator: int = None setup_passcode: int = None commissionee_ip_address_just_for_testing: str = None + maximize_cert_chains: bool = False qr_code_content: str = None manual_code: str = None @@ -144,6 +146,9 @@ class MatterTestConfig: dut_node_id: int = _DEFAULT_DUT_NODE_ID # Node ID to use for controller/commissioner controller_node_id: int = _DEFAULT_CONTROLLER_NODE_ID + # CAT Tags for default controller/commissioner + controller_cat_tags: List[int] = None + # Fabric ID which to use fabric_id: int = None # "Alpha" by default @@ -185,11 +190,12 @@ def _init_stack(self, already_initialized: bool, **kwargs): if (len(self._certificate_authority_manager.activeCaList) == 0): self._logger.warn( "Didn't find any CertificateAuthorities in storage -- creating a new CertificateAuthority + FabricAdmin...") - ca = self._certificate_authority_manager.NewCertificateAuthority() - ca.NewFabricAdmin(vendorId=0xFFF1, fabricId=0xFFF1) + ca = self._certificate_authority_manager.NewCertificateAuthority(caIndex=self._config.root_of_trust_index) + ca.maximizeCertChains = self._config.maximize_cert_chains + ca.NewFabricAdmin(vendorId=0xFFF1, fabricId=self._config.fabric_id) elif (len(self._certificate_authority_manager.activeCaList[0].adminList) == 0): self._logger.warn("Didn't find any FabricAdmins in storage -- creating a new one...") - self._certificate_authority_manager.activeCaList[0].NewFabricAdmin(vendorId=0xFFF1, fabricId=0xFFF1) + self._certificate_authority_manager.activeCaList[0].NewFabricAdmin(vendorId=0xFFF1, fabricId=self._config.fabric_id) # TODO: support getting access to chip-tool credentials issuer's data @@ -477,6 +483,13 @@ def populate_commissioning_args(args: argparse.Namespace, config: MatterTestConf return False config.commissionee_ip_address_just_for_testing = args.ip_addr + if args.case_admin_subject is None: + # Use controller node ID as CASE admin subject during commissioning if nothing provided + config.case_admin_subject = config.controller_node_id + else: + # If a CASE admin subject is provided, then use that + config.case_admin_subject = args.case_admin_subject + return True @@ -569,6 +582,8 @@ def parse_matter_test_args(argv: List[str]) -> MatterTestConfig: commission_group.add_argument('--admin-vendor-id', action="store", type=int_decimal_or_hex, default=_DEFAULT_ADMIN_VENDOR_ID, metavar="VENDOR_ID", help="VendorID to use during commissioning (default 0x%04X)" % _DEFAULT_ADMIN_VENDOR_ID) + commission_group.add_argument('--case-admin-subject', action="store", type=int_decimal_or_hex, + metavar="CASE_ADMIN_SUBJECT", help="Set the CASE admin subject to an explicit value (default to commissioner Node ID)") code_group = parser.add_mutually_exclusive_group(required=False) @@ -655,7 +670,7 @@ def _commission_device(self) -> bool: raise ValueError("Invalid commissioning method %s!" % conf.commissioning_method) -def default_matter_test_main(argv=None): +def default_matter_test_main(argv=None, **kwargs): """Execute the test class in a test module. This is the default entry point for running a test script file directly. In this case, only one test class in a test script is allowed. @@ -670,6 +685,10 @@ def default_matter_test_main(argv=None): """ matter_test_config = parse_matter_test_args(argv) + # Allow override of command line from optional arguments + if matter_test_config.controller_cat_tags is None and "controller_cat_tags" in kwargs: + matter_test_config.controller_cat_tags = kwargs["controller_cat_tags"] + # Find the test class in the test script. test_class = _find_test_class() @@ -681,12 +700,21 @@ def default_matter_test_main(argv=None): if len(matter_test_config.tests) > 0: tests = matter_test_config.tests + # This is required in case we need any testing with maximized certificate chains. + # We need *all* issuers from the start, even for default controller, to use + # maximized chains, before MatterStackState init, others some stale certs + # may not chain properly. + if "maximize_cert_chains" in kwargs: + matter_test_config.maximize_cert_chains = kwargs["maximize_cert_chains"] + stack = MatterStackState(matter_test_config) test_config.user_params["matter_stack"] = stash_globally(stack) # TODO: Steer to right FabricAdmin! + # TODO: If CASE Admin Subject is a CAT tag range, then make sure to issue NOC with that CAT tag + default_controller = stack.certificate_authorities[0].adminList[0].NewController(nodeId=matter_test_config.controller_node_id, - paaTrustStorePath=str(matter_test_config.paa_trust_store_path)) + paaTrustStorePath=str(matter_test_config.paa_trust_store_path), catTags=matter_test_config.controller_cat_tags) test_config.user_params["default_controller"] = stash_globally(default_controller) test_config.user_params["matter_test_config"] = stash_globally(matter_test_config)