Skip to content

Commit 27c640c

Browse files
TC-SC-7.1: Add test for unique discriminators (project-chip#34407)
* TC-SC-5.1: Add test for unique discriminators see CHIP-Specifications/connectedhomeip-spec#9117 Test: please see test_testing for unit tests. * Restyled by autopep8 * Restyled by isort * rename test to TC-SC-7.1 TC-SC-5.1 exists, it's just in a different section. * linter * make setup codes empty lists by default * fix basic comp * fix qr code in ci * Fixup manual codes * Restyled by autopep8 * linter --------- Co-authored-by: Restyled.io <commits@restyled.io>
1 parent 27a653c commit 27c640c

File tree

8 files changed

+357
-55
lines changed

8 files changed

+357
-55
lines changed

.github/workflows/tests.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ jobs:
576576
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_RVCOPSTATE_2_1.py'
577577
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_RVCOPSTATE_2_3.py'
578578
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_RVCOPSTATE_2_4.py'
579+
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_SC_7_1.py'
579580
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestConformanceSupport.py" --script-args "--trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
580581
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestMatterTestingSupport.py" --script-args "--trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
581582
scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestSpecParsingSupport.py" --script-args "--trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
@@ -585,6 +586,7 @@ jobs:
585586
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestSpecParsingDeviceType.py'
586587
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestConformanceSupport.py'
587588
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/test_testing/test_IDM_10_4.py'
589+
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/test_testing/test_TC_SC_7_1.py'
588590
589591
- name: Uploading core files
590592
uses: actions/upload-artifact@v4

src/python_testing/TC_DA_1_7.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,13 @@ def steps_TC_DA_1_7(self):
158158

159159
@async_test_body
160160
async def test_TC_DA_1_7(self):
161-
# post_cert_tests (or sdk) can use the qr or manual code
162-
# We don't currently support this in cert because the base doesn't support multiple QR/manual
163161
num = 0
164162
if self.matter_test_config.discriminators:
165163
num += len(self.matter_test_config.discriminators)
166164
if self.matter_test_config.qr_code_content:
167-
num += 1
165+
num += len(self.matter_test_config.qr_code_content)
168166
if self.matter_test_config.manual_code:
169-
num += 1
167+
num += len(self.matter_test_config.manual_code)
170168

171169
if num != self.expected_number_of_DUTs():
172170
if self.allow_sdk_dac:

src/python_testing/TC_SC_7_1.py

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#
2+
# Copyright (c) 2022 Project CHIP Authors
3+
# All rights reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments
19+
# for details about the block below.
20+
#
21+
# === BEGIN CI TEST ARGUMENTS ===
22+
# test-runner-runs: run1
23+
# test-runner-run/run1/app: ${ALL_CLUSTERS_APP}
24+
# test-runner-run/run1/factoryreset: True
25+
# test-runner-run/run1/quiet: True
26+
# test-runner-run/run1/app-args: --discriminator 2222 --KVS kvs1 --trace-to json:${TRACE_APP}.json
27+
# test-runner-run/run1/script-args: --storage-path admin_storage.json --bool-arg post_cert_test:true --qr-code MT:-24J0KCZ16750648G00 --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
28+
# === END CI TEST ARGUMENTS ===
29+
30+
# Note that in the CI we are using the post-cert test as we can only start one app from the current script.
31+
# This should still be fine as this test has unit tests for other conditions. See test_TC_SC_7_1.py
32+
import logging
33+
34+
import chip.clusters as Clusters
35+
from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
36+
from mobly import asserts
37+
38+
39+
def _trusted_root_test_step(dut_num: int) -> TestStep:
40+
read_trusted_roots_over_pase = f'TH establishes a PASE session to DUT{dut_num} using the provided setup code and reads the TrustedRootCertificates attribute from the operational credentials cluster over PASE'
41+
return TestStep(dut_num, read_trusted_roots_over_pase, "List should be empty as the DUT should be in factory reset ")
42+
43+
44+
class TC_SC_7_1(MatterBaseTest):
45+
''' TC-SC-7.1
46+
47+
This test requires two instances of the DUT with the same PID/VID to confirm that the individual
48+
devices are provisioned with different discriminators and PAKE salts in the same product line.
49+
50+
This test MUST be run on a factory reset device, over PASE, with no commissioned fabrics.
51+
'''
52+
53+
def __init__(self, *args):
54+
super().__init__(*args)
55+
self.post_cert_test = False
56+
57+
def setup_class(self):
58+
super().setup_class()
59+
self.post_cert_test = self.user_params.get("post_cert_test", False)
60+
61+
def expected_number_of_DUTs(self) -> int:
62+
return 1 if self.post_cert_test else 2
63+
64+
def steps_TC_SC_7_1(self):
65+
if self.post_cert_test:
66+
return [_trusted_root_test_step(1),
67+
TestStep(2, "TH extracts the discriminator from the provided setup code", "Ensure the code is not the default")]
68+
69+
return [_trusted_root_test_step(1),
70+
_trusted_root_test_step(2),
71+
TestStep(3, "TH compares the discriminators from the provided setup codes", "Discriminators do not match")]
72+
73+
# TODO: Need a pics or something to limit this to devices that have a factory-provided matter setup code (as opposed to a field upgradable device / device with a custom commissioning where this test won't apply)
74+
75+
@async_test_body
76+
async def test_TC_SC_7_1(self):
77+
# For now, this test is WAY easier if we just ask for the setup code instead of discriminator / passcode
78+
asserts.assert_false(self.matter_test_config.discriminators,
79+
"This test needs to be run with either the QR or manual setup code. The QR code is preferred.")
80+
81+
if len(self.matter_test_config.qr_code_content + self.matter_test_config.manual_code) != self.expected_number_of_DUTs():
82+
if self.post_cert_test:
83+
msg = "The post_cert_test flag is only for use post-certification. When using this flag, specify a single discriminator, manual-code or qr-code-content"
84+
else:
85+
msg = "This test requires two devices for use at certification. Specify two device discriminators or QR codes ex. --discriminator 1234 5678"
86+
asserts.fail(msg)
87+
88+
# Make sure these are no fabrics on the device so we know we're looking at the factory discriminator. This also ensures that the provided codes are correct.
89+
for i, setup_code in enumerate(self.matter_test_config.qr_code_content + self.matter_test_config.manual_code):
90+
self.step(i+1)
91+
await self.default_controller.FindOrEstablishPASESession(setupCode=setup_code, nodeid=i+1)
92+
root_certs = await self.read_single_attribute_check_success(node_id=i+1, cluster=Clusters.OperationalCredentials, attribute=Clusters.OperationalCredentials.Attributes.TrustedRootCertificates, endpoint=0)
93+
asserts.assert_equal(
94+
root_certs, [], "Root certificates found on device. Device must be factory reset before running this test.")
95+
96+
self.step(i+2)
97+
setup_payload_info = self.get_setup_payload_info()
98+
if self.post_cert_test:
99+
# For post-cert, we're testing against the defaults
100+
# TODO: Does it even make sense to test against a manual code in post-cert? It's such a small space, collisions are likely. Should we restrict post-cert to QR? What if one isn't provided?
101+
asserts.assert_not_equal(setup_payload_info[0].filter_value, 3840, "Device is using the default discriminator")
102+
else:
103+
if setup_payload_info[0].filter_value == setup_payload_info[1].filter_value and self.matter_test_config.manual_code is not None:
104+
logging.warn("The two provided discriminators are the same. Note that this CAN occur by chance, especially when using manual codes with the short discriminator. Consider using a QR code, or a different device if you believe the DUTs have individually provisioned")
105+
asserts.assert_not_equal(
106+
setup_payload_info[0].filter_value, setup_payload_info[1].filter_value, "Devices are using the same discriminator values")
107+
108+
# TODO: add test for PAKE salt. This needs to be plumbed through starting from HandlePBKDFParamResponse.
109+
# Will handle in a separate follow up as the plumbing here is aggressive and through some of the crypto layers.
110+
# TODO: Other unit-specific values?
111+
112+
113+
if __name__ == "__main__":
114+
default_matter_test_main()

src/python_testing/basic_composition_support.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,11 @@ def ConvertValue(value) -> Any:
9999

100100
class BasicCompositionTests:
101101
async def connect_over_pase(self, dev_ctrl):
102-
setupCode = self.matter_test_config.qr_code_content if self.matter_test_config.qr_code_content is not None else self.matter_test_config.manual_code
103-
asserts.assert_true(setupCode, "Require either --qr-code or --manual-code.")
104-
await dev_ctrl.FindOrEstablishPASESession(setupCode, self.dut_node_id)
102+
asserts.assert_true(self.matter_test_config.qr_code_content == [] or self.matter_test_config.manual_code == [],
103+
"Cannot have both QR and manual code specified")
104+
setupCode = self.matter_test_config.qr_code_content + self.matter_test_config.manual_code
105+
asserts.assert_equal(len(setupCode), 1, "Require one of either --qr-code or --manual-code.")
106+
await dev_ctrl.FindOrEstablishPASESession(setupCode[0], self.dut_node_id)
105107

106108
def dump_wildcard(self, dump_device_composition_path: typing.Optional[str]):
107109
node_dump_dict = {endpoint_id: MatterTlvToJson(self.endpoints_tlv[endpoint_id]) for endpoint_id in self.endpoints_tlv}

src/python_testing/matter_testing_support.py

+41-43
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,8 @@ class MatterTestConfig:
366366
# This allows cert tests to be run without re-commissioning for RR-1.1.
367367
maximize_cert_chains: bool = True
368368

369-
qr_code_content: Optional[str] = None
370-
manual_code: Optional[str] = None
369+
qr_code_content: List[str] = field(default_factory=list)
370+
manual_code: List[str] = field(default_factory=list)
371371

372372
wifi_ssid: Optional[str] = None
373373
wifi_passphrase: Optional[str] = None
@@ -1067,34 +1067,45 @@ def step(self, step: typing.Union[int, str]):
10671067
self.current_step_index = self.current_step_index + 1
10681068
self.step_skipped = False
10691069

1070-
def get_setup_payload_info(self) -> SetupPayloadInfo:
1071-
if self.matter_test_config.qr_code_content is not None:
1072-
qr_code = self.matter_test_config.qr_code_content
1070+
def get_setup_payload_info(self) -> List[SetupPayloadInfo]:
1071+
setup_payloads = []
1072+
for qr_code in self.matter_test_config.qr_code_content:
10731073
try:
1074-
setup_payload = SetupPayload().ParseQrCode(qr_code)
1074+
setup_payloads.append(SetupPayload().ParseQrCode(qr_code))
10751075
except ChipStackError:
10761076
asserts.fail(f"QR code '{qr_code} failed to parse properly as a Matter setup code.")
10771077

1078-
elif self.matter_test_config.manual_code is not None:
1079-
manual_code = self.matter_test_config.manual_code
1078+
for manual_code in self.matter_test_config.manual_code:
10801079
try:
1081-
setup_payload = SetupPayload().ParseManualPairingCode(manual_code)
1080+
setup_payloads.append(SetupPayload().ParseManualPairingCode(manual_code))
10821081
except ChipStackError:
10831082
asserts.fail(
10841083
f"Manual code code '{manual_code}' failed to parse properly as a Matter setup code. Check that all digits are correct and length is 11 or 21 characters.")
1085-
else:
1086-
asserts.fail("Require either --qr-code or --manual-code.")
1087-
1088-
info = SetupPayloadInfo()
1089-
info.passcode = setup_payload.setup_passcode
1090-
if setup_payload.short_discriminator is not None:
1091-
info.filter_type = discovery.FilterType.SHORT_DISCRIMINATOR
1092-
info.filter_value = setup_payload.short_discriminator
1093-
else:
1094-
info.filter_type = discovery.FilterType.LONG_DISCRIMINATOR
1095-
info.filter_value = setup_payload.long_discriminator
10961084

1097-
return info
1085+
infos = []
1086+
for setup_payload in setup_payloads:
1087+
info = SetupPayloadInfo()
1088+
info.passcode = setup_payload.setup_passcode
1089+
if setup_payload.short_discriminator is not None:
1090+
info.filter_type = discovery.FilterType.SHORT_DISCRIMINATOR
1091+
info.filter_value = setup_payload.short_discriminator
1092+
else:
1093+
info.filter_type = discovery.FilterType.LONG_DISCRIMINATOR
1094+
info.filter_value = setup_payload.long_discriminator
1095+
infos.append(info)
1096+
1097+
num_passcodes = 0 if self.matter_test_config.setup_passcodes is None else len(self.matter_test_config.setup_passcodes)
1098+
num_discriminators = 0 if self.matter_test_config.discriminators is None else len(self.matter_test_config.discriminators)
1099+
asserts.assert_equal(num_passcodes, num_discriminators, "Must have same number of discriminators as passcodes")
1100+
if self.matter_test_config.discriminators:
1101+
for idx, discriminator in enumerate(self.matter_test_config.discriminators):
1102+
info = SetupPayloadInfo()
1103+
info.passcode = self.matter_test_config.setup_passcodes[idx]
1104+
info.filter_type = DiscoveryFilterType.LONG_DISCRIMINATOR
1105+
info.filter_value = discriminator
1106+
infos.append(info)
1107+
1108+
return infos
10981109

10991110
def wait_for_user_input(self,
11001111
prompt_msg: str,
@@ -1293,36 +1304,31 @@ def populate_commissioning_args(args: argparse.Namespace, config: MatterTestConf
12931304
config.commissioning_method = args.commissioning_method
12941305
config.commission_only = args.commission_only
12951306

1296-
# TODO: this should also allow multiple once QR and manual codes are supported.
1297-
config.qr_code_content = args.qr_code
1298-
if args.manual_code:
1299-
config.manual_code = args.manual_code
1300-
else:
1301-
config.manual_code = None
1307+
config.qr_code_content.extend(args.qr_code)
1308+
config.manual_code.extend(args.manual_code)
13021309

13031310
if args.commissioning_method is None:
13041311
return True
13051312

1306-
if args.discriminators is None and (args.qr_code is None and args.manual_code is None):
1313+
if args.discriminators == [] and (args.qr_code == [] and args.manual_code == []):
13071314
print("error: Missing --discriminator when no --qr-code/--manual-code present!")
13081315
return False
13091316
config.discriminators = args.discriminators
13101317

1311-
if args.passcodes is None and (args.qr_code is None and args.manual_code is None):
1318+
if args.passcodes == [] and (args.qr_code == [] and args.manual_code == []):
13121319
print("error: Missing --passcode when no --qr-code/--manual-code present!")
13131320
return False
13141321
config.setup_passcodes = args.passcodes
13151322

1316-
if args.qr_code is not None and args.manual_code is not None:
1323+
if args.qr_code != [] and args.manual_code != []:
13171324
print("error: Cannot have both --qr-code and --manual-code present!")
13181325
return False
13191326

13201327
if len(config.discriminators) != len(config.setup_passcodes):
13211328
print("error: supplied number of discriminators does not match number of passcodes")
13221329
return False
13231330

1324-
device_descriptors = [config.qr_code_content] if config.qr_code_content is not None else [
1325-
config.manual_code] if config.manual_code is not None else config.discriminators
1331+
device_descriptors = config.qr_code_content + config.manual_code + config.discriminators
13261332

13271333
if len(config.dut_node_ids) > len(device_descriptors):
13281334
print("error: More node IDs provided than discriminators")
@@ -1491,9 +1497,9 @@ def parse_matter_test_args(argv: Optional[List[str]] = None) -> MatterTestConfig
14911497
code_group = parser.add_mutually_exclusive_group(required=False)
14921498

14931499
code_group.add_argument('-q', '--qr-code', type=str,
1494-
metavar="QR_CODE", help="QR setup code content (overrides passcode and discriminator)")
1500+
metavar="QR_CODE", default=[], help="QR setup code content (overrides passcode and discriminator)", nargs="+")
14951501
code_group.add_argument('--manual-code', type=str_from_manual_code,
1496-
metavar="MANUAL_CODE", help="Manual setup code content (overrides passcode and discriminator)")
1502+
metavar="MANUAL_CODE", default=[], help="Manual setup code content (overrides passcode and discriminator)", nargs="+")
14971503

14981504
fabric_group = parser.add_argument_group(
14991505
title="Fabric selection", description="Fabric selection for single-fabric basic usage, and commissioning")
@@ -1567,15 +1573,7 @@ async def _commission_device(self, i) -> bool:
15671573
dev_ctrl = self.default_controller
15681574
conf = self.matter_test_config
15691575

1570-
# TODO: qr code and manual code aren't lists
1571-
1572-
if conf.qr_code_content or conf.manual_code:
1573-
info = self.get_setup_payload_info()
1574-
else:
1575-
info = SetupPayloadInfo()
1576-
info.passcode = conf.setup_passcodes[i]
1577-
info.filter_type = DiscoveryFilterType.LONG_DISCRIMINATOR
1578-
info.filter_value = conf.discriminators[i]
1576+
info = self.get_setup_payload_info()[i]
15791577

15801578
if conf.commissioning_method == "on-network":
15811579
try:

src/python_testing/post_certification_tests/production_device_checks.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -352,9 +352,9 @@ def __init__(self, code: str, code_type: SetupCodeType):
352352
self.config = MatterTestConfig(endpoint=0, dut_node_ids=[
353353
1], global_test_params=global_test_params, storage_path=self.admin_storage)
354354
if code_type == SetupCodeType.QR:
355-
self.config.qr_code_content = code
355+
self.config.qr_code_content = [code]
356356
else:
357-
self.config.manual_code = code
357+
self.config.manual_code = [code]
358358
self.config.paa_trust_store_path = Path(self.paa_path)
359359
# Set for DA-1.2, which uses the CD signing certs for verification. This test is now set to use the production CD signing certs from the DCL.
360360
self.config.global_test_params['cd_cert_dir'] = tmpdir_cd

0 commit comments

Comments
 (0)