Skip to content

Commit 098fdff

Browse files
committed
TC-SC-5.1: Add test for unique discriminators
see CHIP-Specifications/connectedhomeip-spec#9117 Test: please see test_testing for unit tests.
1 parent b88ac27 commit 098fdff

File tree

8 files changed

+348
-54
lines changed

8 files changed

+348
-54
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_5_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"'
@@ -584,6 +585,7 @@ jobs:
584585
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestIdChecks.py'
585586
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/TestConformanceSupport.py'
586587
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/test_testing/test_IDM_10_4.py'
588+
scripts/run_in_python_env.sh out/venv 'python3 ./src/python_testing/test_testing/test_TC_SC_5_1.py'
587589
588590
- name: Uploading core files
589591
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_5_1.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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:Y.K908OC16750648G00 --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_5_1.py
32+
import logging
33+
from glob import glob
34+
from pathlib import Path
35+
from typing import List, Optional
36+
37+
import chip.clusters as Clusters
38+
from matter_testing_support import MatterBaseTest, SetupPayloadInfo, TestStep, async_test_body, default_matter_test_main
39+
from mobly import asserts
40+
41+
42+
def _trusted_root_test_step(dut_num: int) -> TestStep:
43+
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'
44+
return TestStep(dut_num, read_trusted_roots_over_pase, "List should be empty as the DUT should be in factory reset ")
45+
46+
class TC_SC_5_1(MatterBaseTest):
47+
''' TC-SC-5.1
48+
49+
This test requires two instances of the DUT with the same PID/VID to confirm that the individual
50+
devices are provisioned with different discriminators and PAKE salts in the same product line.
51+
52+
This test MUST be run on a factory reset device, over PASE, with no commissioned fabrics.
53+
'''
54+
55+
def setup_class(self):
56+
self.post_cert_test = self.user_params.get("post_cert_test", False)
57+
58+
def expected_number_of_DUTs(self) -> int:
59+
return 1 if self.post_cert_test else 2
60+
61+
def steps_TC_SC_5_1(self):
62+
if self.post_cert_test:
63+
return [_trusted_root_test_step(1),
64+
TestStep(2, "TH extracts the discriminator from the provided setup code", "Ensure the code is not the default")]
65+
66+
return [_trusted_root_test_step(1),
67+
_trusted_root_test_step(2),
68+
TestStep(3, "TH compares the discriminators from the provided setup codes", "Discriminators do not match")]
69+
70+
@async_test_body
71+
async def test_TC_SC_5_1(self):
72+
# For now, this test is WAY easier if we just ask for the setup code instead of discriminator / passcode
73+
asserts.assert_false(self.matter_test_config.discriminators, "This test needs to be run with either the QR or manual setup code. The QR code is preferred.")
74+
setup_codes = self.matter_test_config.qr_code_content if self.matter_test_config.qr_code_content is not None else []
75+
setup_codes.extend(self.matter_test_config.manual_code if self.matter_test_config.manual_code is not None else [])
76+
77+
if len(setup_codes) != self.expected_number_of_DUTs():
78+
if self.post_cert_test:
79+
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"
80+
else:
81+
msg = "This test requires two devices for use at certification. Specify two device discriminators or QR codes ex. --discriminator 1234 5678"
82+
asserts.fail(msg)
83+
84+
# 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.
85+
for i, setup_code in enumerate(setup_codes):
86+
self.step(i+1)
87+
await self.default_controller.FindOrEstablishPASESession(setupCode=setup_code, nodeid=i)
88+
root_certs = await self.read_single_attribute_check_success(node_id=i, cluster=Clusters.OperationalCredentials, attribute=Clusters.OperationalCredentials.Attributes.TrustedRootCertificates, endpoint=0)
89+
asserts.assert_equal(root_certs, [], "Root certificates found on device. Device must be factory reset before running this test.")
90+
91+
self.step(len(setup_codes)+1)
92+
setup_payload_info = self.get_setup_payload_info()
93+
if self.post_cert_test:
94+
# For post-cert, we're testing against the defaults
95+
# 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?
96+
asserts.assert_not_equal(setup_payload_info[0].filter_value, 3840, "Device is using the default discriminator")
97+
else:
98+
if setup_payload_info[0].filter_value == setup_payload_info[1].filter_value:
99+
if self.matter_test_config.manual_code is not None:
100+
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 discriminators.")
101+
asserts.assert_not_equal(setup_payload_info[0].filter_value, setup_payload_info[1].filter_value, "Devices are using the same discriminator values")
102+
103+
104+
# TODO: add test for PAKE salt. This needs to be plumbed through starting from HandlePBKDFParamResponse.
105+
# Will handle in a separate follow up as the plumbing here is aggressive and through some of the crypto layers.
106+
# TODO: Other unit-specific values?
107+
108+
109+
if __name__ == "__main__":
110+
default_matter_test_main()

src/python_testing/basic_composition_support.py

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

100100
class BasicCompositionTests:
101101
async def connect_over_pase(self, dev_ctrl):
102+
asserts.assert_true(self.matter_test_config.qr_code_content is None or self.matter_test_config.manual_code is None, "Cannot have both QR and manual code specified")
102103
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)
104+
asserts.assert_equal(len(setupCode), 1, "Require one of either --qr-code or --manual-code.")
105+
await dev_ctrl.FindOrEstablishPASESession(setupCode[0], self.dut_node_id)
105106

106107
def dump_wildcard(self, dump_device_composition_path: typing.Optional[str]):
107108
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

+46-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: Optional[List[str]] = None
370+
manual_code: Optional[List[str]] = None
371371

372372
wifi_ssid: Optional[str] = None
373373
wifi_passphrase: Optional[str] = None
@@ -1069,34 +1069,47 @@ def step(self, step: typing.Union[int, str]):
10691069
self.current_step_index = self.current_step_index + 1
10701070
self.step_skipped = False
10711071

1072-
def get_setup_payload_info(self) -> SetupPayloadInfo:
1072+
def get_setup_payload_info(self) -> List[SetupPayloadInfo]:
1073+
setup_payloads = []
10731074
if self.matter_test_config.qr_code_content is not None:
1074-
qr_code = self.matter_test_config.qr_code_content
1075-
try:
1076-
setup_payload = SetupPayload().ParseQrCode(qr_code)
1077-
except ChipStackError:
1078-
asserts.fail(f"QR code '{qr_code} failed to parse properly as a Matter setup code.")
1079-
1080-
elif self.matter_test_config.manual_code is not None:
1081-
manual_code = self.matter_test_config.manual_code
1082-
try:
1083-
setup_payload = SetupPayload().ParseManualPairingCode(manual_code)
1084-
except ChipStackError:
1085-
asserts.fail(
1086-
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.")
1087-
else:
1088-
asserts.fail("Require either --qr-code or --manual-code.")
1089-
1090-
info = SetupPayloadInfo()
1091-
info.passcode = setup_payload.setup_passcode
1092-
if setup_payload.short_discriminator is not None:
1093-
info.filter_type = discovery.FilterType.SHORT_DISCRIMINATOR
1094-
info.filter_value = setup_payload.short_discriminator
1095-
else:
1096-
info.filter_type = discovery.FilterType.LONG_DISCRIMINATOR
1097-
info.filter_value = setup_payload.long_discriminator
1098-
1099-
return info
1075+
for qr_code in self.matter_test_config.qr_code_content:
1076+
try:
1077+
setup_payloads.append(SetupPayload().ParseQrCode(qr_code))
1078+
except ChipStackError:
1079+
asserts.fail(f"QR code '{qr_code} failed to parse properly as a Matter setup code.")
1080+
1081+
if self.matter_test_config.manual_code is not None:
1082+
for manual_code in self.matter_test_config.manual_code:
1083+
try:
1084+
setup_payloads.append(SetupPayload().ParseManualPairingCode(manual_code))
1085+
except ChipStackError:
1086+
asserts.fail(
1087+
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.")
1088+
1089+
infos = []
1090+
for setup_payload in setup_payloads:
1091+
info = SetupPayloadInfo()
1092+
info.passcode = setup_payload.setup_passcode
1093+
if setup_payload.short_discriminator is not None:
1094+
info.filter_type = discovery.FilterType.SHORT_DISCRIMINATOR
1095+
info.filter_value = setup_payload.short_discriminator
1096+
else:
1097+
info.filter_type = discovery.FilterType.LONG_DISCRIMINATOR
1098+
info.filter_value = setup_payload.long_discriminator
1099+
infos.append(info)
1100+
1101+
num_passcodes = 0 if self.matter_test_config.setup_passcodes is None else len(self.matter_test_config.setup_passcodes)
1102+
num_discriminators = 0 if self.matter_test_config.discriminators is None else len(self.matter_test_config.discriminators)
1103+
asserts.assert_equal(num_passcodes, num_discriminators, "Must have same number of discriminators as passcodes")
1104+
if self.matter_test_config.discriminators:
1105+
for idx, discriminator in enumerate(self.matter_test_config.discriminators):
1106+
info = SetupPayloadInfo()
1107+
info.passcode = self.matter_test_config.setup_passcodes[idx]
1108+
info.filter_type = DiscoveryFilterType.LONG_DISCRIMINATOR
1109+
info.filter_value = discriminator
1110+
infos.append(info)
1111+
1112+
return infos
11001113

11011114
def wait_for_user_input(self,
11021115
prompt_msg: str,
@@ -1295,7 +1308,6 @@ def populate_commissioning_args(args: argparse.Namespace, config: MatterTestConf
12951308
config.commissioning_method = args.commissioning_method
12961309
config.commission_only = args.commission_only
12971310

1298-
# TODO: this should also allow multiple once QR and manual codes are supported.
12991311
config.qr_code_content = args.qr_code
13001312
if args.manual_code:
13011313
config.manual_code = args.manual_code
@@ -1323,8 +1335,7 @@ def populate_commissioning_args(args: argparse.Namespace, config: MatterTestConf
13231335
print("error: supplied number of discriminators does not match number of passcodes")
13241336
return False
13251337

1326-
device_descriptors = [config.qr_code_content] if config.qr_code_content is not None else [
1327-
config.manual_code] if config.manual_code is not None else config.discriminators
1338+
device_descriptors = config.qr_code_content if config.qr_code_content is not None else config.manual_code if config.manual_code is not None else config.discriminators
13281339

13291340
if len(config.dut_node_ids) > len(device_descriptors):
13301341
print("error: More node IDs provided than discriminators")
@@ -1493,9 +1504,9 @@ def parse_matter_test_args(argv: Optional[List[str]] = None) -> MatterTestConfig
14931504
code_group = parser.add_mutually_exclusive_group(required=False)
14941505

14951506
code_group.add_argument('-q', '--qr-code', type=str,
1496-
metavar="QR_CODE", help="QR setup code content (overrides passcode and discriminator)")
1507+
metavar="QR_CODE", default=[], help="QR setup code content (overrides passcode and discriminator)", nargs="+")
14971508
code_group.add_argument('--manual-code', type=str_from_manual_code,
1498-
metavar="MANUAL_CODE", help="Manual setup code content (overrides passcode and discriminator)")
1509+
metavar="MANUAL_CODE", default=[], help="Manual setup code content (overrides passcode and discriminator)", nargs="+")
14991510

15001511
fabric_group = parser.add_argument_group(
15011512
title="Fabric selection", description="Fabric selection for single-fabric basic usage, and commissioning")
@@ -1569,15 +1580,7 @@ async def _commission_device(self, i) -> bool:
15691580
dev_ctrl = self.default_controller
15701581
conf = self.matter_test_config
15711582

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

15821585
if conf.commissioning_method == "on-network":
15831586
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)