blob: 01db13eb319301c95baf9285b8e3715757e7d4a0 [file]
#
# Copyright (c) 2025 Project CHIP Authors
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import datetime
import random
import string
from collections.abc import Callable
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, utils
from cryptography.x509 import CertificateBuilder, CertificateSigningRequest, random_serial_number
from cryptography.x509.oid import NameOID
from ecdsa.curves import curve_by_name
from mobly import asserts
from pyasn1.codec.der.decoder import decode as der_decoder
from pyasn1.error import PyAsn1Error
from pyasn1_modules import rfc2986, rfc5480
import matter.clusters as Clusters
import matter.testing.matchers as matchers
from matter import ChipDeviceCtrl
from matter.clusters.enum import MatterIntEnum
from matter.clusters.Types import Nullable, NullValue
from matter.interaction_model import InteractionModelError, Status
from matter.testing import matter_asserts
from matter.testing.conversions import hex_from_bytes
from matter.testing.matter_testing import MatterBaseTest
from matter.tlv import uint
class TLSUtils:
"""Helper utility with local certificate generation & verification,
and node/endpoint-scoped TLS commands.
"""
def __init__(self, matter_test: MatterBaseTest, endpoint: int | None = None,
dev_ctrl: ChipDeviceCtrl.ChipDeviceController | None = None,
node_id: int | None = None, fabric_index: int | None = None):
self.test = matter_test
self.endpoint = endpoint
self.dev_ctrl = dev_ctrl
self.node_id = node_id
self.fabric_index = fabric_index
class CertWithKey:
def __init__(self, cert: bytes, key: ec.EllipticCurvePrivateKey):
self.cert = cert
self.key = key
def get_key(self) -> ec.EllipticCurvePrivateKey:
return ec.generate_private_key(
ec.SECP256R1()
)
def gen_cert(self, public_key: bytes | None = None) -> bytes:
return self.gen_cert_with_key(self.get_key(), public_key)
def gen_cert_chain(self, root: ec.EllipticCurvePrivateKey, length: int):
signer = root
result = []
for i in range(length):
key = self.get_key()
cert = self.gen_cert_with_key(signer, key.public_key())
result.append(self.CertWithKey(cert, key))
signer = key
return result
def gen_cert_with_key(self, signer: ec.EllipticCurvePrivateKey, public_key: bytes | None = None,
subject: x509.Name | None = None, builder_lambda: Callable[
[], CertificateBuilder] = lambda: CertificateBuilder()) -> bytes:
if not public_key:
public_key = signer.public_key()
rand_suffix = "".join(
random.choices(string.ascii_letters + string.digits, k=16)
)
root_cert_subject = x509.Name(
[
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "CSA"),
x509.NameAttribute(
NameOID.COMMON_NAME, "TC_PAVS root " + rand_suffix
),
]
)
if not subject:
subject = root_cert_subject
cert = (
builder_lambda()
.subject_name(subject)
.issuer_name(root_cert_subject)
.public_key(public_key)
.serial_number(random_serial_number())
.not_valid_before(datetime.datetime.now(datetime.UTC))
.not_valid_after(
datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=365.25*20)
)
.add_extension(
# We make it so that our root can only issue leaf certificates, no intermediate here.
x509.BasicConstraints(ca=True, path_length=0), critical=True
)
.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=True,
crl_sign=True,
encipher_only=False,
decipher_only=False,
),
critical=True,
)
.add_extension(
x509.SubjectKeyIdentifier.from_public_key(
signer.public_key()
),
critical=False,
)
.sign(signer, hashes.SHA256())
)
return cert.public_bytes(serialization.Encoding.DER)
def assert_valid_caid(self, caid):
asserts.assert_greater_equal(caid, 0, "Invalid CAID returned")
asserts.assert_less_equal(caid, 65534, "Invalid CAID returned")
def assert_valid_ccdid(self, caid):
asserts.assert_greater_equal(caid, 0, "Invalid CCDID returned")
asserts.assert_less_equal(caid, 65534, "Invalid CCDID returned")
def assert_valid_csr(self, response: Clusters.TlsCertificateManagement.Commands.ClientCSRResponse, nonce: bytes) -> CertificateSigningRequest:
# Verify der encoded and PKCS #10 (rfc2986 is PKCS #10) - next two requirements
try:
temp, _ = der_decoder(response.csr, asn1Spec=rfc2986.CertificationRequest())
except PyAsn1Error:
asserts.fail("Unable to decode CSR - improperly formatted DER")
layer1 = dict(temp)
# Verify public key is 256 bytes
csr = x509.load_der_x509_csr(response.csr)
csr_pubkey = csr.public_key()
asserts.assert_equal(csr_pubkey.key_size, 256, "Incorrect key size")
# Verify signature algorithm is ecdsa-with-SHA256
signature_algorithm = dict(layer1['signatureAlgorithm'])['algorithm']
asserts.assert_equal(signature_algorithm, rfc5480.ecdsa_with_SHA256, "CSR specifies incorrect signature key algorithm")
# Verify signature is valid
asserts.assert_true(csr.is_signature_valid, "Signature is invalid")
# Verify response.nonceSignature is octet string of length 32
try:
# response.nonceSignature is an octet string if it can be converted to an int
int(hex_from_bytes(response.nonceSignature), 16)
except ValueError:
asserts.fail("Returned CSR nonceSignature is not an octet string")
# Verify response.nonceSignature is valid signature
baselen = curve_by_name("NIST256p").baselen
signature_raw_r = int(hex_from_bytes(response.nonceSignature[:baselen]), 16)
signature_raw_s = int(hex_from_bytes(response.nonceSignature[baselen:]), 16)
nonce_signature = utils.encode_dss_signature(signature_raw_r, signature_raw_s)
csr_pubkey.verify(signature=nonce_signature, data=nonce, signature_algorithm=ec.ECDSA(hashes.SHA256()))
return csr
def get_fingerprint(self, cert: bytes) -> bytes:
return x509.load_der_x509_certificate(cert).fingerprint(hashes.SHA256())
async def send_provision_root_command(
self, certificate: bytes, caid: Nullable | int = NullValue,
expected_status: Status = Status.Success) -> Clusters.TlsCertificateManagement.Commands.ProvisionRootCertificateResponse | InteractionModelError:
try:
result = await self.test.send_single_cmd(cmd=Clusters.TlsCertificateManagement.Commands.ProvisionRootCertificate(certificate=certificate, caid=caid),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
asserts.assert_true(matchers.is_type(result, Clusters.TlsCertificateManagement.Commands.ProvisionRootCertificateResponse),
"Unexpected return type for ProvisionRootCertificate")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e
async def send_find_root_command(
self, caid: Nullable | int = NullValue,
expected_status: Status = Status.Success) -> Clusters.TlsCertificateManagement.Commands.FindRootCertificateResponse | InteractionModelError:
try:
result = await self.test.send_single_cmd(cmd=Clusters.TlsCertificateManagement.Commands.FindRootCertificate(caid=caid),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
asserts.assert_true(matchers.is_type(result, Clusters.TlsCertificateManagement.Commands.FindRootCertificateResponse),
"Unexpected return type for FindRootCertificate")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e
async def send_lookup_root_command(
self, fingerprint: bytes,
expected_status: Status = Status.Success) -> Clusters.TlsCertificateManagement.Commands.LookupRootCertificateResponse | InteractionModelError:
try:
result = await self.test.send_single_cmd(cmd=Clusters.TlsCertificateManagement.Commands.LookupRootCertificate(fingerprint=fingerprint),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
asserts.assert_true(matchers.is_type(result, Clusters.TlsCertificateManagement.Commands.LookupRootCertificateResponse),
"Unexpected return type for LookupRootCertificate")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e
async def send_remove_root_command(
self, caid: int,
expected_status: Status = Status.Success) -> InteractionModelError:
try:
result = await self.test.send_single_cmd(cmd=Clusters.TlsCertificateManagement.Commands.RemoveRootCertificate(caid=caid),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e
async def send_csr_command(
self, nonce: bytes, ccdid: Nullable | int = NullValue,
expected_status: Status = Status.Success) -> Clusters.TlsCertificateManagement.Commands.ClientCSRResponse | InteractionModelError:
try:
result = await self.test.send_single_cmd(cmd=Clusters.TlsCertificateManagement.Commands.ClientCSR(ccdid=ccdid, nonce=nonce),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
asserts.assert_true(matchers.is_type(result, Clusters.TlsCertificateManagement.Commands.ClientCSRResponse),
"Unexpected return type for ClientCSR")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e
async def send_provision_client_command(
self, certificate: bytes, ccdid: int, intermediates: list[bytes] = [],
expected_status: Status = Status.Success) -> InteractionModelError:
try:
result = await self.test.send_single_cmd(cmd=Clusters.TlsCertificateManagement.Commands.ProvisionClientCertificate(ccdid=ccdid, clientCertificate=certificate, intermediateCertificates=intermediates),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e
async def send_find_client_command(
self, ccdid: Nullable | int = NullValue,
expected_status: Status = Status.Success) -> Clusters.TlsCertificateManagement.Commands.FindClientCertificateResponse | InteractionModelError:
try:
result = await self.test.send_single_cmd(cmd=Clusters.TlsCertificateManagement.Commands.FindClientCertificate(ccdid=ccdid),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
asserts.assert_true(matchers.is_type(result, Clusters.TlsCertificateManagement.Commands.FindClientCertificateResponse),
"Unexpected return type for FindClientCertificate")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e
async def send_lookup_client_command(
self, fingerprint: bytes,
expected_status: Status = Status.Success) -> Clusters.TlsCertificateManagement.Commands.LookupClientCertificateResponse | InteractionModelError:
try:
result = await self.test.send_single_cmd(cmd=Clusters.TlsCertificateManagement.Commands.LookupClientCertificate(fingerprint=fingerprint),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
asserts.assert_true(matchers.is_type(result, Clusters.TlsCertificateManagement.Commands.LookupClientCertificateResponse),
"Unexpected return type for LookupClientCertificate")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e
async def send_remove_client_command(
self, ccdid: int,
expected_status: Status = Status.Success) -> InteractionModelError:
try:
result = await self.test.send_single_cmd(cmd=Clusters.TlsCertificateManagement.Commands.RemoveClientCertificate(ccdid=ccdid),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e
async def read_tls_cert_attribute(self, attribute, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.MRP_PAYLOAD):
cluster = Clusters.TlsCertificateManagement
return await self.test.read_single_attribute_check_success(endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, cluster=cluster, attribute=attribute, payloadCapability=payloadCapability)
async def read_tls_client_attribute(self, attribute, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.MRP_PAYLOAD):
cluster = Clusters.TlsClientManagement
return await self.test.read_single_attribute_check_success(endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id, cluster=cluster, attribute=attribute, payloadCapability=payloadCapability)
async def read_root_certs_attribute_as_map(self, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.MRP_PAYLOAD):
attribute_certs = await self.read_tls_cert_attribute(Clusters.TlsCertificateManagement.Attributes.ProvisionedRootCertificates, payloadCapability)
matter_asserts.assert_list(attribute_certs, "Expected list")
found_certs = {}
for cert in attribute_certs:
found_certs[cert.caid] = cert
return found_certs
async def read_client_certs_attribute_as_map(self, payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.MRP_PAYLOAD):
attribute_certs = await self.read_tls_cert_attribute(Clusters.TlsCertificateManagement.Attributes.ProvisionedClientCertificates, payloadCapability)
matter_asserts.assert_list(attribute_certs, "Expected list")
found_certs = {}
for cert in attribute_certs:
found_certs[cert.ccdid] = cert
return found_certs
async def send_find_client_command_as_map(
self,
expected_status: Status = Status.Success):
find_response = await self.send_find_client_command(expected_status=expected_status)
found_certs = {}
for cert in find_response.certificateDetails:
found_certs[cert.ccdid] = cert
return found_certs
async def send_remove_fabric_command(self, fabric_index: int):
resp = await self.test.send_single_cmd(cmd=Clusters.OperationalCredentials.Commands.RemoveFabric(fabric_index), endpoint=0, dev_ctrl=self.dev_ctrl)
asserts.assert_equal(
resp.statusCode, Clusters.OperationalCredentials.Enums.NodeOperationalCertStatusEnum.kOk)
async def send_provision_tls_endpoint_command(
self,
hostname: bytes,
port: uint,
caid: uint,
endpoint_id: Nullable | int = NullValue,
ccdid: Nullable | uint = NullValue,
expected_status: Status | MatterIntEnum = Status.Success,
) -> Clusters.TlsClientManagement.Commands.ProvisionEndpointResponse | InteractionModelError:
try:
result = await self.test.send_single_cmd(
cmd=Clusters.TlsClientManagement.Commands.ProvisionEndpoint(
hostname=hostname, port=port, caid=caid, ccdid=ccdid, endpointID=endpoint_id
),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id,
payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD,
)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
asserts.assert_true(
matchers.is_type(
result,
Clusters.TlsClientManagement.Commands.ProvisionEndpointResponse,
),
"Unexpected return type for ProvisionEndpoint",
)
return result
except InteractionModelError as e:
if matchers.is_type(expected_status, Status):
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
else:
asserts.assert_equal(e.status, Status.Failure, "Unexpected error returned")
asserts.assert_equal(e.clusterStatus, expected_status, "Unexpected error returned")
return e
async def send_remove_tls_endpoint_command(
self,
endpoint_id: uint,
expected_status: Status = Status.Success,
) -> InteractionModelError:
try:
result = await self.test.send_single_cmd(
cmd=Clusters.TlsClientManagement.Commands.RemoveEndpoint(
endpointID=endpoint_id
),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id,
payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD,
)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e
async def send_find_tls_endpoint_command(
self,
endpoint_id: uint,
expected_status: Status = Status.Success,
) -> InteractionModelError:
try:
result = await self.test.send_single_cmd(
cmd=Clusters.TlsClientManagement.Commands.FindEndpoint(
endpointID=endpoint_id
),
endpoint=self.endpoint, dev_ctrl=self.dev_ctrl, node_id=self.node_id,
payloadCapability=ChipDeviceCtrl.TransportPayloadCapability.LARGE_PAYLOAD,
)
asserts.assert_equal(Status.Success, expected_status, "Unexpected success")
asserts.assert_true(matchers.is_type(result, Clusters.TlsClientManagement.Commands.FindEndpointResponse),
"Unexpected return type for FindEndpoint")
return result
except InteractionModelError as e:
asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
return e