Skip to content

Commit

Permalink
Implement TC-SC-3.6 as a Python test (#21719)
Browse files Browse the repository at this point in the history
* Implement TC-SC-3.6 as a Python test

- Test TC-SC-3.6 was not feasible with chip-tool and manual
  intervention due to the complexity of validating that the
  correct sessions were established, establishing all of them,
  and ensuring all subscriptions fire as intended.

This PR:
- Adds a version of TC-SC-3.6 in Python
- Updates the Python code with minor improvements needed during
  the development of the test
- **Touches no C++ SDK code**

Testing done:

- Ran the test as it could be done by an end-user, passed on Linux
  and on ESP32 (after some changes to resources in the examples, not
  included in this PR)

To run:

- Build all-clusters app Linux:
  - `scripts/examples/gn_build_example.sh examples/all-clusters-app/linux out/debug/standalone chip_config_network_layer_ble=false`
  - In a shell, run: `clear && rm -f kvs1 && out/debug/standalone/chip-all-clusters-app --discriminator 2118 --KVS kvs1`
- Build the Python environment, activate it, then run the test
  - `./scripts/build_python.sh -m platform -i separate`
  - `. ./out/python_env/bin/activate`
  - Run the test: `rm -f admin_storage.json && python src/python_testing/TC_SC_3_6.py -m on-network -d 2118 -p 20202021`

* Apply review comments
  • Loading branch information
tcarmelveilleux authored and pull[bot] committed Aug 16, 2023
1 parent 647bc71 commit 1410044
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 45 deletions.
6 changes: 3 additions & 3 deletions src/controller/python/chip-device-ctrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ def do_setuppayload(self, line):
setup-payload generate [options]
Options:
-vr Version
-vr Version
-vi Vendor ID
-pi Product ID
-cf Custom Flow [Standard = 0, UserActionRequired = 1, Custom = 2]
Expand Down Expand Up @@ -971,7 +971,7 @@ def do_opencommissioningwindow(self, line):
open-commissioning-window <nodeid> [options]
Options:
-t Timeout (in seconds)
-t Timeout (in seconds)
-o Option [TokenWithRandomPIN = 1, TokenWithProvidedPIN = 2]
-d Discriminator Value
-i Iteration
Expand Down Expand Up @@ -1024,7 +1024,7 @@ def do_getfabricid(self, line):
return

compressed_fabricid = self.devCtrl.GetCompressedFabricId()
raw_fabricid = self.devCtrl.GetFabricId()
raw_fabricid = self.devCtrl.fabricId
except exceptions.ChipStackException as ex:
print("An exception occurred during reading FabricID:")
print(str(ex))
Expand Down
67 changes: 53 additions & 14 deletions src/controller/python/chip/ChipDeviceCtrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@
from .clusters import Objects as GeneratedObjects
from .clusters.CHIPClusters import *
from . import clusters as Clusters
from .FabricAdmin import FabricAdmin
import enum
import threading
import typing
import builtins
import ipdb
import ctypes
import copy

Expand Down Expand Up @@ -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):
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):
self.state = DCState.NOT_INITIALIZED
self.devCtrl = None
self._ChipStack = builtins.chipStack
Expand All @@ -174,19 +174,23 @@ def __init__(self, opCredsContext: ctypes.c_void_p, fabricId: int, nodeId: int,
opCredsContext), pointer(devCtrl), fabricId, nodeId, adminVendorId, ctypes.c_char_p(None if len(paaTrustStorePath) == 0 else str.encode(paaTrustStorePath)), useTestCommissioner)
)

self.nodeId = nodeId

if res != 0:
raise self._ChipStack.ErrorToException(res)

self.devCtrl = devCtrl
self._fabricAdmin = fabricAdmin
self._fabricId = fabricId
self._nodeId = nodeId
self._adminIndex = fabricAdmin.adminIndex

if name is None:
self._name = "adminIndex(%x)/fabricId(0x%016X)/nodeId(0x%016X)" % (fabricAdmin.adminIndex, fabricId, nodeId)
else:
self._name = name

self._Cluster = ChipClusters(builtins.chipStack)
self._Cluster.InitLib(self._dmLib)

def GetNodeId(self):
return self.nodeId

def HandleCommissioningComplete(nodeid, err):
if err != 0:
print("Failed to commission: {}".format(err))
Expand All @@ -198,7 +202,7 @@ def HandleCommissioningComplete(nodeid, err):
self._ChipStack.commissioningCompleteEvent.set()
self._ChipStack.completeEvent.set()

def HandleKeyExchangeComplete(err):
def HandlePASEEstablishmentComplete(err):
if err != 0:
print("Failed to establish secure session to device: {}".format(err))
self._ChipStack.callbackRes = self._ChipStack.ErrorToException(
Expand All @@ -207,7 +211,7 @@ def HandleKeyExchangeComplete(err):
print("Established secure session with Device")

if self.state != DCState.COMMISSIONING:
# During Commissioning, HandleKeyExchangeComplete will also be called,
# During Commissioning, HandlePASEEstablishmentComplete will also be called,
# in this case the async operation should be marked as finished by
# HandleCommissioningComplete instead this function.
self.state = DCState.IDLE
Expand All @@ -218,10 +222,10 @@ def HandleKeyExchangeComplete(err):
if err != 0:
HandleCommissioningComplete(0, err)

self.cbHandleKeyExchangeCompleteFunct = _DevicePairingDelegate_OnPairingCompleteFunct(
HandleKeyExchangeComplete)
self.cbHandlePASEEstablishmentCompleteFunct = _DevicePairingDelegate_OnPairingCompleteFunct(
HandlePASEEstablishmentComplete)
self._dmLib.pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback(
self.devCtrl, self.cbHandleKeyExchangeCompleteFunct)
self.devCtrl, self.cbHandlePASEEstablishmentCompleteFunct)

self.cbHandleCommissioningCompleteFunct = _DevicePairingDelegate_OnCommissioningCompleteFunct(
HandleCommissioningComplete)
Expand All @@ -231,8 +235,40 @@ def HandleKeyExchangeComplete(err):
self.state = DCState.IDLE
self.isActive = True

# Validate FabricID/NodeID followed from NOC Chain
self._fabricId = self.GetFabricIdInternal()
assert self._fabricId == fabricId
self._nodeId = self.GetNodeIdInternal()
assert self._nodeId == nodeId

ChipDeviceController.activeList.add(self)

@property
def fabricAdmin(self) -> FabricAdmin:
return self._fabricAdmin

@property
def nodeId(self) -> int:
self.CheckIsActive()
return self._nodeId

@property
def fabricId(self) -> int:
self.CheckIsActive()
return self._fabricId

@property
def adminIndex(self) -> int:
return self._adminIndex

@property
def name(self) -> str:
return self._name

@name.setter
def name(self, new_name: str):
self._name = new_name

def Shutdown(self):
''' Shuts down this controller and reclaims any used resources, including the bound
C++ constructor instance in the SDK.
Expand Down Expand Up @@ -447,6 +483,7 @@ def CommissionWithCode(self, setupPayload: str, nodeid: int):
return self._ChipStack.commissioningEventRes == 0

def CommissionIP(self, ipaddr: str, setupPinCode: int, nodeid: int):
""" DEPRECATED, DO NOT USE! Use `CommissionOnNetwork` or `CommissionWithCode` """
self.CheckIsActive()

# IP connection will run through full commissioning, so we need to wait
Expand Down Expand Up @@ -614,7 +651,8 @@ def GetCompressedFabricId(self):
else:
raise self._ChipStack.ErrorToException(res)

def GetFabricId(self):
def GetFabricIdInternal(self):
"""Get the fabric ID from the object. Only used to validate cached value from property."""
self.CheckIsActive()

fabricid = c_uint64(0)
Expand All @@ -629,7 +667,8 @@ def GetFabricId(self):
else:
raise self._ChipStack.ErrorToException(res)

def GetNodeId(self):
def GetNodeIdInternal(self) -> int:
"""Get the node ID from the object. Only used to validate cached value from property."""
self.CheckIsActive()

nodeid = c_uint64(0)
Expand Down
17 changes: 14 additions & 3 deletions src/controller/python/chip/FabricAdmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from typing import *
from ctypes import *
from rich.pretty import pprint
import ipdb
import json
import logging
import builtins
Expand Down Expand Up @@ -102,7 +101,7 @@ def __init__(self, vendorId: int, adminIndex: int = None, fabricId: int = 1):
raise ValueError(
f"Invalid VendorID ({vendorId}) provided!")

self.vendorId = vendorId
self._vendorId = vendorId
self._fabricId = fabricId

if (adminIndex is None):
Expand Down Expand Up @@ -160,7 +159,7 @@ def NewController(self, nodeId: int = None, paaTrustStorePath: str = "", useTest
f"Allocating new controller with FabricId: 0x{self._fabricId:016X}, NodeId: 0x{nodeId:016X}")

controller = ChipDeviceCtrl.ChipDeviceController(
self.closure, self._fabricId, nodeId, self.vendorId, paaTrustStorePath, useTestCommissioner)
self.closure, self._fabricId, nodeId, self.vendorId, paaTrustStorePath, useTestCommissioner, fabricAdmin=self)
return controller

def ShutdownAll():
Expand Down Expand Up @@ -200,3 +199,15 @@ def Shutdown(self, deleteFromStorage: bool = True):

def __del__(self):
self.Shutdown(False)

@property
def vendorId(self) -> int:
return self._vendorId

@property
def fabricId(self) -> int:
return self._fabricId

@property
def adminIndex(self) -> int:
return self._adminIndex
6 changes: 3 additions & 3 deletions src/controller/python/chip/clusters/Attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ def UpdateCachedData(self):


class SubscriptionTransaction:
def __init__(self, transaction: 'AsyncReadTransaction', subscriptionId, devCtrl):
def __init__(self, transaction: AsyncReadTransaction, subscriptionId, devCtrl):
self._onResubscriptionAttemptedCb = DefaultResubscriptionAttemptedCallback
self._onAttributeChangeCb = DefaultAttributeChangeCallback
self._onEventChangeCb = DefaultEventChangeCallback
Expand Down Expand Up @@ -760,9 +760,9 @@ def _handleDone(self):
if not self._future.done():
if self._resultError:
if self._subscription_handler:
self._subscription_handler.OnErrorCb(chipError, self._subscription_handler)
self._subscription_handler.OnErrorCb(self._resultError, self._subscription_handler)
else:
self._future.set_exception(chip.exceptions.ChipStackError(chipError))
self._future.set_exception(chip.exceptions.ChipStackError(self._resultError))
else:
self._future.set_result(AsyncReadTransaction.ReadResponse(
attributes=self._cache.attributeCache, events=self._events))
Expand Down
1 change: 0 additions & 1 deletion src/controller/python/chip/storage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from typing import *
from ctypes import *
from rich.pretty import pprint
import ipdb
import json
import logging
import base64
Expand Down
35 changes: 16 additions & 19 deletions src/controller/python/chip/utils/CommissioningBuildingBlocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async def GrantPrivilege(adminCtrl: ChipDeviceController, grantedCtrl: ChipDevic
# 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.GetNodeId()]
acl.subjects = [subject for subject in acl.subjects if subject != grantedCtrl.nodeId]

if (privilege):
addedPrivilege = False
Expand All @@ -75,8 +75,8 @@ 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.GetNodeId() not in acl.subjects:
acl.subjects.append(grantedCtrl.GetNodeId())
if grantedCtrl.nodeId not in acl.subjects:
acl.subjects.append(grantedCtrl.nodeId)
addedPrivilege = True

# Step 3: If there isn't an existing entry to add to, make a new one.
Expand All @@ -86,7 +86,7 @@ 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.GetNodeId()]))
subjects=[grantedCtrl.nodeId]))

# Step 4: Prune ACLs which have empty subjects.
currentAcls = [acl for acl in currentAcls if acl.subjects != NullValue and len(acl.subjects) != 0]
Expand Down Expand Up @@ -115,24 +115,21 @@ async def CreateControllersOnFabric(fabricAdmin: FabricAdmin, adminDevCtrl: Chip


async def AddNOCForNewFabricFromExisting(commissionerDevCtrl, newFabricDevCtrl, existingNodeId, newNodeId):
''' Perform sequence to commission new frabric using existing commissioned fabric.
''' Perform sequence to commission new fabric using existing commissioned fabric.
Args:
commissionerDevCtrl (ChipDeviceController): Already commissioned device controller used
to commission a new fabric on `newFabricDevCtrl`.
newFabricDevCtrl (ChipDeviceController): New device controller which is used for the new
fabric we are establishing.
existingNodeId (int): Node ID of the server we are establishing a CASE session on the
existing fabric that we will used to perform AddNOC.
newNodeId (int): Node ID that we would like to server to used on the new fabric being
added.
existingNodeId (int): Node ID of the target where an AddNOC needs to be done for a new fabric.
newNodeId (int): Node ID to use for the target node on the new fabric.
Return:
bool: True if successful, False otherwise.
'''

resp = await commissionerDevCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(60), timedRequestTimeoutMs=1000)
resp = await commissionerDevCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(60))
if resp.errorCode is not generalCommissioning.Enums.CommissioningError.kOk:
return False

Expand All @@ -141,20 +138,20 @@ async def AddNOCForNewFabricFromExisting(commissionerDevCtrl, newFabricDevCtrl,
chainForAddNOC = newFabricDevCtrl.IssueNOCChain(csrForAddNOC, newNodeId)
if chainForAddNOC.rcacBytes is None or chainForAddNOC.icacBytes is None or chainForAddNOC.nocBytes is None or chainForAddNOC.ipkBytes is None:
# Expiring the failsafe timer in an attempt to clean up.
await commissionerDevCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0), timedRequestTimeoutMs=1000)
await commissionerDevCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0))
return False

await commissionerDevCtrl.SendCommand(existingNodeId, 0, opCreds.Commands.AddTrustedRootCertificate(chainForAddNOC.rcacBytes))
resp = await commissionerDevCtrl.SendCommand(existingNodeId, 0, opCreds.Commands.AddNOC(chainForAddNOC.nocBytes, chainForAddNOC.icacBytes, chainForAddNOC.ipkBytes, newFabricDevCtrl.GetNodeId(), 0xFFF1))
resp = await commissionerDevCtrl.SendCommand(existingNodeId, 0, opCreds.Commands.AddNOC(chainForAddNOC.nocBytes, chainForAddNOC.icacBytes, chainForAddNOC.ipkBytes, newFabricDevCtrl.nodeId, 0xFFF1))
if resp.statusCode is not opCreds.Enums.OperationalCertStatus.kSuccess:
# Expiring the failsafe timer in an attempt to clean up.
await commissionerDevCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0), timedRequestTimeoutMs=1000)
await commissionerDevCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0))
return False

resp = await newFabricDevCtrl.SendCommand(newNodeId, 0, generalCommissioning.Commands.CommissioningComplete())
if resp.errorCode is not generalCommissioning.Enums.CommissioningError.kOk:
# Expiring the failsafe timer in an attempt to clean up.
await commissionerDevCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0), timedRequestTimeoutMs=1000)
await commissionerDevCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0))
return False

if not await _IsNodeInFabricList(newFabricDevCtrl, newNodeId):
Expand All @@ -179,20 +176,20 @@ async def UpdateNOC(devCtrl, existingNodeId, newNodeId):
bool: True if successful, False otherwise.
"""
resp = await devCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(600), timedRequestTimeoutMs=1000)
resp = await devCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(600))
if resp.errorCode is not generalCommissioning.Enums.CommissioningError.kOk:
return False
csrForUpdateNOC = await devCtrl.SendCommand(
existingNodeId, 0, opCreds.Commands.CSRRequest(CSRNonce=os.urandom(32), isForUpdateNOC=True))
chainForUpdateNOC = devCtrl.IssueNOCChain(csrForUpdateNOC, newNodeId)
if chainForUpdateNOC.rcacBytes is None or chainForUpdateNOC.icacBytes is None or chainForUpdateNOC.nocBytes is None or chainForUpdateNOC.ipkBytes is None:
await devCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0), timedRequestTimeoutMs=1000)
await devCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0))
return False

resp = await devCtrl.SendCommand(existingNodeId, 0, opCreds.Commands.UpdateNOC(chainForUpdateNOC.nocBytes, chainForUpdateNOC.icacBytes))
if resp.statusCode is not opCreds.Enums.OperationalCertStatus.kSuccess:
# Expiring the failsafe timer in an attempt to clean up.
await devCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0), timedRequestTimeoutMs=1000)
await devCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0))
return False

# Forget our session since the peer deleted it
Expand All @@ -201,7 +198,7 @@ async def UpdateNOC(devCtrl, existingNodeId, newNodeId):
resp = await devCtrl.SendCommand(newNodeId, 0, generalCommissioning.Commands.CommissioningComplete())
if resp.errorCode is not generalCommissioning.Enums.CommissioningError.kOk:
# Expiring the failsafe timer in an attempt to clean up.
await devCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0), timedRequestTimeoutMs=1000)
await devCtrl.SendCommand(existingNodeId, 0, generalCommissioning.Commands.ArmFailSafe(0))
return False

if not await _IsNodeInFabricList(devCtrl, newNodeId):
Expand Down
Loading

0 comments on commit 1410044

Please sign in to comment.