#![allow(clippy::unwrap_used, clippy::expect_used)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::similar_names)]
#![allow(dead_code, unreachable_pub)]
#![allow(clippy::cast_possible_truncation, clippy::manual_assert)]
use std::path::PathBuf;
use base64::Engine;
use matter_cert::test_support::{build_x509_der, TestCertFields};
use matter_cert::{
BasicConstraints, DistinguishedName, DnAttribute, Extensions, KeyUsage, MatterTime, Signature,
TrustedRoots,
};
use matter_commissioning::attestation::{AttestationResponse, Paa, PaaTrustStore};
use matter_commissioning::driver::{
decode_unsecured, encode_unsecured, AsyncDatagram, DriverError, InMemoryDatagram,
};
use matter_commissioning::CsrResponse;
use matter_crypto::pase::{PasePbkdfParams, PaseVerifier};
use matter_crypto::{CaseCredentials, CaseResponder, CaseSigner as _, RingSigner, Sigma1Outcome};
use matter_transport::{
DecodeInboundOutput, MrpFlags, PeerHint, ProtocolId, SessionId, SessionManager, SessionRole,
};
use serde::Deserialize;
pub const VID: u16 = 0xFFF1;
pub const PID: u16 = 0x8001;
const EKU_CLIENT_AUTH: u32 = 2;
pub struct MockDevicePki {
pub dac_signer: RingSigner,
pub dac_der: Vec<u8>,
pub pai_der: Vec<u8>,
pub paa_der: Vec<u8>,
pub paa_trust_store: PaaTrustStore,
}
pub fn build_mock_device_pki(now: MatterTime) -> MockDevicePki {
let now_unix = now.to_unix_secs();
let (paa_signer, paa_pkcs8) = RingSigner::generate().expect("PAA key generation");
let paa_pk = paa_signer.public_key().clone();
let paa_dn = DistinguishedName::new(vec![
DnAttribute::CommonName("Matter Test PAA (mock-device)".into()),
DnAttribute::VendorId(VID),
]);
let paa_der = build_x509_der(
TestCertFields {
serial: vec![0x01],
issuer: paa_dn.clone(), not_before: MatterTime::from_unix_secs(now_unix.saturating_sub(365 * 86_400)),
not_after: MatterTime::from_unix_secs(now_unix.saturating_add(3650 * 86_400)),
subject: paa_dn.clone(),
public_key: paa_pk,
extensions: Extensions::builder()
.basic_constraints(Some(BasicConstraints::new(true, Some(1))))
.key_usage(Some(KeyUsage::KEY_CERT_SIGN | KeyUsage::CRL_SIGN))
.build(),
signature: Signature::new([0u8; 64]),
},
&paa_pkcs8, )
.expect("PAA DER build");
let (pai_signer, pai_pkcs8) = RingSigner::generate().expect("PAI key generation");
let pai_pk = pai_signer.public_key().clone();
let pai_dn = DistinguishedName::new(vec![
DnAttribute::CommonName("Matter Test PAI (mock-device)".into()),
DnAttribute::VendorId(VID),
]);
let pai_der = build_x509_der(
TestCertFields {
serial: vec![0x02],
issuer: paa_dn, not_before: MatterTime::from_unix_secs(now_unix.saturating_sub(180 * 86_400)),
not_after: MatterTime::from_unix_secs(now_unix.saturating_add(1825 * 86_400)),
subject: pai_dn.clone(),
public_key: pai_pk,
extensions: Extensions::builder()
.basic_constraints(Some(BasicConstraints::new(true, Some(0))))
.key_usage(Some(KeyUsage::KEY_CERT_SIGN | KeyUsage::CRL_SIGN))
.build(),
signature: Signature::new([0u8; 64]),
},
&paa_pkcs8, )
.expect("PAI DER build");
let _ = pai_signer;
let (dac_signer, _dac_pkcs8) = RingSigner::generate().expect("DAC key generation");
let dac_pk = dac_signer.public_key().clone();
let dac_dn = DistinguishedName::new(vec![
DnAttribute::CommonName("Matter Test DAC (mock-device)".into()),
DnAttribute::VendorId(VID),
DnAttribute::ProductId(PID),
]);
let dac_der = build_x509_der(
TestCertFields {
serial: vec![0x03],
issuer: pai_dn, not_before: MatterTime::from_unix_secs(now_unix.saturating_sub(30 * 86_400)),
not_after: MatterTime::from_unix_secs(now_unix.saturating_add(365 * 86_400)),
subject: dac_dn,
public_key: dac_pk,
extensions: Extensions::builder()
.basic_constraints(Some(BasicConstraints::new(false, None)))
.key_usage(Some(KeyUsage::DIGITAL_SIGNATURE))
.extended_key_usage(Some(vec![EKU_CLIENT_AUTH]))
.build(),
signature: Signature::new([0u8; 64]),
},
&pai_pkcs8, )
.expect("DAC DER build");
let mut paa_trust_store = PaaTrustStore::empty();
paa_trust_store.add(Paa::from_der(&paa_der).expect("PAA parses back"));
MockDevicePki {
dac_signer,
dac_der,
pai_der,
paa_der,
paa_trust_store,
}
}
fn mint_pkcs10_csr(seed: u8) -> (Vec<u8>, [u8; 65]) {
use p256::ecdsa::{signature::Signer, Signature, SigningKey};
let mut scalar = [0u8; 32];
scalar[0] = seed;
let signing_key = SigningKey::from_slice(&scalar).unwrap();
let verifying_key = signing_key.verifying_key();
let encoded = verifying_key.to_encoded_point(false);
let mut public_key = [0u8; 65];
public_key.copy_from_slice(encoded.as_bytes());
let tbs_csr_info = build_pkcs10_csr_info(&public_key);
let signature: Signature = signing_key.sign(&tbs_csr_info);
let sig_der = signature.to_der().as_bytes().to_vec();
let csr_der = wrap_pkcs10_csr(&tbs_csr_info, &sig_der);
(csr_der, public_key)
}
fn build_pkcs10_csr_info(public_key_sec1: &[u8; 65]) -> Vec<u8> {
let alg_id = encode_der_sequence(&[
&encode_der_oid(&[1, 2, 840, 10045, 2, 1]),
&encode_der_oid(&[1, 2, 840, 10045, 3, 1, 7]),
]);
let pk_bit_string = encode_der_bit_string(public_key_sec1);
let subject_pk_info = encode_der_sequence(&[&alg_id, &pk_bit_string]);
let subject = encode_der_sequence(&[]);
let version = encode_der_integer_zero();
let attributes = encode_der_context_implicit_set(0, &[]);
encode_der_sequence(&[&version, &subject, &subject_pk_info, &attributes])
}
fn wrap_pkcs10_csr(tbs: &[u8], signature_der: &[u8]) -> Vec<u8> {
let alg_id = encode_der_sequence(&[&encode_der_oid(&[1, 2, 840, 10045, 4, 3, 2])]);
let sig_bit_string = encode_der_bit_string(signature_der);
encode_der_sequence(&[tbs, &alg_id, &sig_bit_string])
}
fn write_nocsr(
csr_der: &[u8],
nonce: &[u8; 32],
vr1: Option<&[u8]>,
vr2: Option<&[u8]>,
vr3: Option<&[u8]>,
) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(1), csr_der).unwrap();
w.put_bytes(Tag::Context(2), nonce).unwrap();
if let Some(b) = vr1 {
w.put_bytes(Tag::Context(3), b).unwrap();
}
if let Some(b) = vr2 {
w.put_bytes(Tag::Context(4), b).unwrap();
}
if let Some(b) = vr3 {
w.put_bytes(Tag::Context(5), b).unwrap();
}
w.end_container().unwrap();
buf
}
fn encode_der_length(body_len: usize) -> Vec<u8> {
if body_len < 0x80 {
vec![body_len as u8]
} else if body_len <= 0xff {
vec![0x81, body_len as u8]
} else if body_len <= 0xffff {
vec![0x82, (body_len >> 8) as u8, body_len as u8]
} else {
panic!("test helper: length too big");
}
}
fn encode_der_tlv(tag: u8, body: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
out.extend_from_slice(&encode_der_length(body.len()));
out.extend_from_slice(body);
out
}
fn encode_der_sequence(parts: &[&[u8]]) -> Vec<u8> {
let mut body = Vec::new();
for p in parts {
body.extend_from_slice(p);
}
encode_der_tlv(0x30, &body)
}
fn encode_der_integer_zero() -> Vec<u8> {
encode_der_tlv(0x02, &[0x00])
}
fn encode_der_bit_string(bytes: &[u8]) -> Vec<u8> {
let mut body = Vec::with_capacity(bytes.len() + 1);
body.push(0x00);
body.extend_from_slice(bytes);
encode_der_tlv(0x03, &body)
}
fn encode_der_context_implicit_set(tag: u8, parts: &[&[u8]]) -> Vec<u8> {
let mut body = Vec::new();
for p in parts {
body.extend_from_slice(p);
}
encode_der_tlv(0xA0 | tag, &body)
}
fn encode_der_oid(arcs: &[u32]) -> Vec<u8> {
let mut body = Vec::new();
if arcs.len() < 2 {
panic!("OID needs at least two arcs");
}
body.push((arcs[0] * 40 + arcs[1]) as u8);
for arc in &arcs[2..] {
let mut a = *arc;
let mut digits = Vec::new();
digits.push((a & 0x7f) as u8);
a >>= 7;
while a > 0 {
digits.push(((a & 0x7f) | 0x80) as u8);
a >>= 7;
}
digits.reverse();
body.extend_from_slice(&digits);
}
encode_der_tlv(0x06, &body)
}
#[derive(Deserialize)]
struct CdFixture {
cd_b64: String,
}
pub fn load_cd_fixture() -> Vec<u8> {
let mut path: PathBuf = env!("CARGO_MANIFEST_DIR").into();
path.push("..");
path.push("..");
path.push("test-vectors");
path.push("commissioning");
path.push("cd");
path.push("happy-path.json");
let raw = std::fs::read_to_string(&path).expect("CD fixture file present");
let f: CdFixture = serde_json::from_str(&raw).expect("CD fixture parses as JSON");
base64::engine::general_purpose::STANDARD
.decode(f.cd_b64.as_bytes())
.expect("cd_b64 is valid base64")
}
const AT_MATTER_EPOCH_SECS: u64 = 1_800_000_000u64.saturating_sub(946_684_800);
pub fn build_attestation_response(
cd_bytes: &[u8],
nonce: [u8; 32],
challenge: [u8; 16],
dac_signer: &RingSigner,
) -> AttestationResponse {
use matter_codec::{Tag, TlvWriter};
let mut elements = Vec::new();
{
let mut w = TlvWriter::new(&mut elements);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(1), cd_bytes).unwrap();
w.put_bytes(Tag::Context(2), &nonce).unwrap();
w.put_uint(Tag::Context(3), AT_MATTER_EPOCH_SECS).unwrap();
w.end_container().unwrap();
}
let mut tbs = Vec::with_capacity(elements.len() + challenge.len());
tbs.extend_from_slice(&elements);
tbs.extend_from_slice(&challenge);
let signature = dac_signer
.sign_p256_sha256(&tbs)
.expect("DAC signing must not fail");
AttestationResponse {
attestation_elements: elements,
signature,
}
}
pub fn build_csr_response(
csr_nonce: [u8; 32],
challenge: [u8; 16],
dac_signer: &RingSigner,
) -> CsrResponse {
let (csr_der, _csr_pubkey) = mint_pkcs10_csr(0x42);
let nocsr_elements = write_nocsr(&csr_der, &csr_nonce, None, None, None);
let mut tbs = Vec::with_capacity(nocsr_elements.len() + challenge.len());
tbs.extend_from_slice(&nocsr_elements);
tbs.extend_from_slice(&challenge);
let attestation_signature = dac_signer
.sign_p256_sha256(&tbs)
.expect("DAC signing must not fail");
CsrResponse {
nocsr_elements,
attestation_signature,
}
}
fn tlv_skip_container(r: &mut matter_codec::TlvReader<'_>) {
use matter_codec::Element;
let mut depth = 1usize;
loop {
match r.next().expect("tlv_skip_container: unexpected EOF") {
Some(Element::ContainerEnd) => {
depth -= 1;
if depth == 0 {
return;
}
}
Some(Element::ContainerStart { .. }) => depth += 1,
None => panic!("tlv_skip_container: stream ended before container closed"),
Some(_) => {}
}
}
}
fn tlv_read_members(
r: &mut matter_codec::TlvReader<'_>,
) -> Vec<(matter_codec::Tag, matter_codec::Value)> {
use matter_codec::Element;
let mut out = Vec::new();
loop {
match r.next().expect("tlv_read_members: unexpected EOF") {
Some(Element::ContainerEnd) => return out,
None => panic!("tlv_read_members: stream ended before container closed"),
Some(Element::Scalar { tag, value }) => out.push((tag, value)),
Some(Element::ContainerStart { tag, kind }) => {
let v = tlv_read_container_value(r, kind);
out.push((tag, v));
}
Some(_) => {}
}
}
}
fn tlv_read_container_value(
r: &mut matter_codec::TlvReader<'_>,
kind: matter_codec::ContainerKind,
) -> matter_codec::Value {
let members = tlv_read_members(r);
match kind {
matter_codec::ContainerKind::Structure => matter_codec::Value::Structure(members),
matter_codec::ContainerKind::Array => {
matter_codec::Value::Array(members.into_iter().map(|(_, v)| v).collect())
}
_ => matter_codec::Value::List(members),
}
}
pub struct InvokeRequestDecoded {
pub path: matter_commissioning::im::CommandPath,
pub fields_tlv: Vec<u8>,
}
pub fn parse_invoke_request(bytes: &[u8]) -> InvokeRequestDecoded {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
use matter_commissioning::im::CommandPath;
let mut r = TlvReader::new(bytes);
match r.next().expect("InvokeRequest: first element") {
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
other => panic!("InvokeRequest: expected anon struct, got {other:?}"),
}
loop {
match r.next().expect("InvokeRequest: scan for InvokeRequests") {
Some(Element::ContainerStart {
tag: Tag::Context(2),
kind: ContainerKind::Array,
}) => break,
Some(Element::ContainerEnd) | None => {
panic!("InvokeRequest: missing InvokeRequests array")
}
Some(Element::ContainerStart { .. }) => {
tlv_skip_container(&mut r);
}
Some(_) => {}
}
}
match r.next().expect("InvokeRequest: first CommandDataIB") {
Some(Element::ContainerStart {
kind: ContainerKind::Structure,
..
}) => {}
_ => panic!("InvokeRequest: missing CommandDataIB struct"),
}
let mut path: Option<CommandPath> = None;
let mut fields_tlv: Vec<u8> = Vec::new();
loop {
match r.next().expect("InvokeRequest: scan CommandDataIB members") {
None => panic!("InvokeRequest: CommandDataIB body ended without end-of-container"),
Some(Element::ContainerEnd) => break,
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::List,
}) => {
let members = tlv_read_members(&mut r);
let mut endpoint = None;
let mut cluster = None;
let mut command = None;
for (tag, v) in members {
match (tag, v) {
(Tag::Context(0), Value::Uint(n)) => {
endpoint = Some(u16::try_from(n).expect("endpoint fits u16"));
}
(Tag::Context(1), Value::Uint(n)) => {
cluster = Some(u32::try_from(n).expect("cluster fits u32"));
}
(Tag::Context(2), Value::Uint(n)) => {
command = Some(u32::try_from(n).expect("command fits u32"));
}
_ => {}
}
}
path = Some(CommandPath {
endpoint: endpoint.expect("CommandPath.endpoint present"),
cluster: cluster.expect("CommandPath.cluster present"),
command: command.expect("CommandPath.command present"),
});
}
Some(Element::ContainerStart {
tag: Tag::Context(1),
kind,
}) => {
let v = tlv_read_container_value(&mut r, kind);
let mut buf = Vec::new();
let mut w = matter_codec::TlvWriter::new(&mut buf);
w.write_value(Tag::Anonymous, &v)
.expect("InvokeRequest: re-encode CommandFields");
fields_tlv = buf;
}
Some(Element::ContainerStart { .. }) => tlv_skip_container(&mut r),
Some(_) => {}
}
}
if fields_tlv.is_empty() {
let mut buf = Vec::new();
let mut w = matter_codec::TlvWriter::new(&mut buf);
w.write_value(Tag::Anonymous, &Value::Structure(Vec::new()))
.expect("encode empty struct");
fields_tlv = buf;
}
InvokeRequestDecoded {
path: path.expect("CommandDataIB.CommandPath present"),
fields_tlv,
}
}
pub fn build_invoke_response(
path: matter_commissioning::im::CommandPath,
fields_tlv: &[u8],
) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap(); w.put_bool(Tag::Context(0), false).unwrap(); w.start_array(Tag::Context(1)).unwrap(); {
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(0)).unwrap(); w.start_list(Tag::Context(0)).unwrap();
w.put_uint(Tag::Context(0), u64::from(path.endpoint))
.unwrap();
w.put_uint(Tag::Context(1), u64::from(path.cluster))
.unwrap();
w.put_uint(Tag::Context(2), u64::from(path.command))
.unwrap();
w.end_container().unwrap(); w.put_preencoded(Tag::Context(1), fields_tlv).unwrap();
w.end_container().unwrap(); w.end_container().unwrap(); }
w.end_container().unwrap(); w.put_uint(
Tag::Context(0xFF),
u64::from(matter_commissioning::im::IM_REVISION),
)
.unwrap();
w.end_container().unwrap(); buf
}
pub fn build_invoke_status_response(
path: matter_commissioning::im::CommandPath,
status_code: u8,
) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap(); w.put_bool(Tag::Context(0), false).unwrap(); w.start_array(Tag::Context(1)).unwrap(); {
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(0)).unwrap();
w.put_uint(Tag::Context(0), u64::from(path.endpoint))
.unwrap();
w.put_uint(Tag::Context(1), u64::from(path.cluster))
.unwrap();
w.put_uint(Tag::Context(2), u64::from(path.command))
.unwrap();
w.end_container().unwrap(); w.start_structure(Tag::Context(1)).unwrap(); w.put_uint(Tag::Context(0), u64::from(status_code)).unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); w.end_container().unwrap(); }
w.end_container().unwrap(); w.put_uint(
Tag::Context(0xFF),
u64::from(matter_commissioning::im::IM_REVISION),
)
.unwrap();
w.end_container().unwrap(); buf
}
pub fn parse_read_request(bytes: &[u8]) -> Vec<matter_commissioning::im::AttributePath> {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
use matter_commissioning::im::AttributePath;
let mut r = TlvReader::new(bytes);
match r.next().expect("ReadRequest: first element") {
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
other => panic!("ReadRequest: expected anon struct, got {other:?}"),
}
loop {
match r.next().expect("ReadRequest: scan for AttributeRequests") {
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::Array,
}) => break,
Some(Element::ContainerEnd) | None => {
panic!("ReadRequest: missing AttributeRequests array")
}
Some(Element::ContainerStart { .. }) => tlv_skip_container(&mut r),
Some(_) => {}
}
}
let mut paths = Vec::new();
loop {
match r.next().expect("ReadRequest: iterate AttributeRequests") {
Some(Element::ContainerEnd) => break, None => panic!("ReadRequest: AttributeRequests array not closed"),
Some(Element::ContainerStart {
kind: ContainerKind::List,
..
}) => {
let members = tlv_read_members(&mut r);
let mut endpoint = None;
let mut cluster = None;
let mut attribute = None;
for (tag, v) in members {
match (tag, v) {
(Tag::Context(2), Value::Uint(n)) => {
endpoint = Some(u16::try_from(n).expect("endpoint fits u16"));
}
(Tag::Context(3), Value::Uint(n)) => {
cluster = Some(u32::try_from(n).expect("cluster fits u32"));
}
(Tag::Context(4), Value::Uint(n)) => {
attribute = Some(u32::try_from(n).expect("attribute fits u32"));
}
_ => {}
}
}
paths.push(AttributePath {
endpoint: endpoint.expect("AttributePath.endpoint present"),
cluster: cluster.expect("AttributePath.cluster present"),
attribute: attribute.expect("AttributePath.attribute present"),
});
}
Some(Element::ContainerStart { .. }) => tlv_skip_container(&mut r),
Some(_) => {}
}
}
paths
}
pub fn build_report_data(reports: &[(matter_commissioning::im::AttributePath, &[u8])]) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap(); w.start_array(Tag::Context(1)).unwrap(); for (path, value_tlv) in reports {
w.start_structure(Tag::Anonymous).unwrap(); w.start_structure(Tag::Context(1)).unwrap(); w.start_list(Tag::Context(1)).unwrap();
w.put_uint(Tag::Context(2), u64::from(path.endpoint))
.unwrap();
w.put_uint(Tag::Context(3), u64::from(path.cluster))
.unwrap();
w.put_uint(Tag::Context(4), u64::from(path.attribute))
.unwrap();
w.end_container().unwrap(); w.put_preencoded(Tag::Context(2), value_tlv).unwrap();
w.end_container().unwrap(); w.end_container().unwrap(); }
w.end_container().unwrap(); w.put_uint(
Tag::Context(0xFF),
u64::from(matter_commissioning::im::IM_REVISION),
)
.unwrap();
w.end_container().unwrap(); buf
}
pub enum DeviceReply {
Command(Vec<u8>),
Status(u8),
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::match_same_arms)]
pub fn respond(
path: matter_commissioning::im::CommandPath,
fields_tlv: &[u8],
challenge: [u8; 16],
pki: &MockDevicePki,
) -> DeviceReply {
use matter_codec::{Tag, TlvWriter};
let ok_commissioning_response = || -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap(); w.put_uint(Tag::Context(0), 0_u64).unwrap(); w.put_utf8(Tag::Context(1), "").unwrap(); w.end_container().unwrap();
buf
};
match (path.cluster, path.command) {
(0x0030, 0x00) => DeviceReply::Command(ok_commissioning_response()),
(0x0030, 0x02) => DeviceReply::Command(ok_commissioning_response()),
(0x0030, 0x04) => DeviceReply::Command(ok_commissioning_response()),
(0x003E, 0x00) => {
use matter_codec::{Element, Tag as CTag, TlvReader, Value};
let mut reader = TlvReader::new(fields_tlv);
let _ = reader.next().expect("AttestationRequest: struct start");
let mut att_nonce: Option<[u8; 32]> = None;
loop {
match reader.next().expect("AttestationRequest: field scan") {
Some(Element::ContainerEnd) | None => break,
Some(Element::Scalar {
tag: CTag::Context(0),
value: Value::Bytes(b),
}) => {
let arr: [u8; 32] = b.as_slice().try_into().expect("32-byte nonce");
att_nonce = Some(arr);
}
Some(_) => {}
}
}
let nonce = att_nonce.expect("AttestationRequest: nonce at ctx(0)");
let cd = load_cd_fixture();
let att_resp = build_attestation_response(&cd, nonce, challenge, &pki.dac_signer);
let mut buf = Vec::new();
{
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(0), &att_resp.attestation_elements)
.unwrap(); w.put_bytes(Tag::Context(1), &att_resp.signature).unwrap(); w.end_container().unwrap();
}
DeviceReply::Command(buf)
}
(0x003E, 0x02) => {
use matter_codec::{Element, Tag as CTag, TlvReader, Value};
let mut reader = TlvReader::new(fields_tlv);
let _ = reader.next().expect("CertChainRequest: struct start");
let mut cert_type: Option<u8> = None;
loop {
match reader.next().expect("CertChainRequest: field scan") {
Some(Element::ContainerEnd) | None => break,
Some(Element::Scalar {
tag: CTag::Context(0),
value: Value::Uint(n),
}) => {
cert_type = Some(u8::try_from(n).expect("CertChainType fits u8"));
}
Some(_) => {}
}
}
let cert_der = match cert_type.expect("CertificateChainRequest: type at ctx(0)") {
0x01 => pki.dac_der.clone(),
0x02 => pki.pai_der.clone(),
other => panic!(
"CertificateChainRequest: unknown CertChainType 0x{other:02X} (expected 0x01=DAC or 0x02=PAI)"
),
};
let mut buf = Vec::new();
{
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(0), &cert_der).unwrap(); w.end_container().unwrap();
}
DeviceReply::Command(buf)
}
(0x003E, 0x04) => {
use matter_codec::{Element, Tag as CTag, TlvReader, Value};
let mut reader = TlvReader::new(fields_tlv);
let _ = reader.next().expect("CSRRequest: struct start");
let mut csr_nonce: Option<[u8; 32]> = None;
loop {
match reader.next().expect("CSRRequest: field scan") {
Some(Element::ContainerEnd) | None => break,
Some(Element::Scalar {
tag: CTag::Context(0),
value: Value::Bytes(b),
}) => {
let arr: [u8; 32] = b.as_slice().try_into().expect("32-byte CSRNonce");
csr_nonce = Some(arr);
}
Some(_) => {}
}
}
let nonce = csr_nonce.expect("CSRRequest: nonce at ctx(0)");
let csr_resp = build_csr_response(nonce, challenge, &pki.dac_signer);
let mut buf = Vec::new();
{
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(0), &csr_resp.nocsr_elements)
.unwrap(); w.put_bytes(Tag::Context(1), &csr_resp.attestation_signature)
.unwrap(); w.end_container().unwrap();
}
DeviceReply::Command(buf)
}
(0x003E, 0x06) => {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), 0_u64).unwrap(); w.put_uint(Tag::Context(1), 1_u64).unwrap(); w.end_container().unwrap();
DeviceReply::Command(buf)
}
(0x003E, 0x0B) => DeviceReply::Status(0),
(0x0031, 0x02) => {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), 0_u64).unwrap(); w.end_container().unwrap();
DeviceReply::Command(buf)
}
(0x0031, 0x03) => {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), 0_u64).unwrap(); w.end_container().unwrap();
DeviceReply::Command(buf)
}
(0x0031, 0x06) => {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), 0_u64).unwrap(); w.end_container().unwrap();
DeviceReply::Command(buf)
}
(c, cmd) => panic!(
"respond: unrecognised (cluster=0x{c:04X}, command=0x{cmd:02X}) — not in the commissioning happy path"
),
}
}
pub fn respond_read_attribute_wifi(attr_path: matter_commissioning::im::AttributePath) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
if attr_path.cluster == 0x0031 && attr_path.attribute == 0xFFFC {
let wifi_feature: u32 =
matter_commissioning::clusters::network_commissioning::NetworkCommissioningFeature::WIFI
.bits();
let mut buf = Vec::new();
TlvWriter::new(&mut buf)
.put_uint(Tag::Anonymous, u64::from(wifi_feature))
.unwrap();
buf
} else {
respond_read_attribute(attr_path)
}
}
pub fn respond_read_attribute_thread(
attr_path: matter_commissioning::im::AttributePath,
) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
if attr_path.cluster == 0x0031 && attr_path.attribute == 0xFFFC {
let thread_feature: u32 =
matter_commissioning::clusters::network_commissioning::NetworkCommissioningFeature::THREAD
.bits();
let mut buf = Vec::new();
TlvWriter::new(&mut buf)
.put_uint(Tag::Anonymous, u64::from(thread_feature))
.unwrap();
buf
} else {
respond_read_attribute(attr_path)
}
}
pub fn respond_read_attribute(attr_path: matter_commissioning::im::AttributePath) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
match (attr_path.cluster, attr_path.attribute) {
(0x0030, 0x0000) => {
let mut buf = Vec::new();
TlvWriter::new(&mut buf)
.put_uint(Tag::Anonymous, 0)
.unwrap();
buf
}
(0x0030, 0x0001) => {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), 60_u64).unwrap(); w.put_uint(Tag::Context(1), 900_u64).unwrap(); w.end_container().unwrap();
buf
}
(0x0030, 0x0002) => {
let mut buf = Vec::new();
TlvWriter::new(&mut buf)
.put_uint(Tag::Anonymous, 2)
.unwrap(); buf
}
(0x0030, 0x0004) => {
let mut buf = Vec::new();
TlvWriter::new(&mut buf)
.put_bool(Tag::Anonymous, true)
.unwrap();
buf
}
(0x0031, 0xFFFC) => {
let ethernet_feature: u32 =
matter_commissioning::clusters::network_commissioning::NetworkCommissioningFeature::ETHERNET
.bits();
let mut buf = Vec::new();
TlvWriter::new(&mut buf)
.put_uint(Tag::Anonymous, u64::from(ethernet_feature))
.unwrap();
buf
}
(0x0031, 0x0003) => {
let mut buf = Vec::new();
TlvWriter::new(&mut buf)
.put_uint(Tag::Anonymous, 30)
.unwrap();
buf
}
(c, a) => {
panic!("respond_read_attribute: unrecognised (cluster=0x{c:04X}, attribute=0x{a:04X})")
}
}
}
#[cfg(test)]
mod mock_device_response_table {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::items_after_statements
)]
use matter_cert::time::MatterTime;
use matter_codec::{Tag, TlvWriter};
use matter_commissioning::clusters::network_commissioning::NetworkCommissioningFeature;
use matter_commissioning::im::{AttributePath, CommandPath};
use matter_commissioning::noc::{
decode_attestation_response, decode_certificate_chain_response,
};
use matter_commissioning::state_machine::Expectation;
use super::{build_mock_device_pki, respond, respond_read_attribute, DeviceReply};
fn now() -> MatterTime {
MatterTime::from_unix_secs(1_800_000_000)
}
fn arm_fail_safe_fields() -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), 60_u64).unwrap(); w.put_uint(Tag::Context(1), 1_u64).unwrap(); w.end_container().unwrap();
buf
}
fn cert_chain_request_fields(cert_type: u8) -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), u64::from(cert_type)).unwrap();
w.end_container().unwrap();
buf
}
fn attestation_request_fields(nonce: [u8; 32]) -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(0), &nonce).unwrap();
w.end_container().unwrap();
buf
}
fn csr_request_fields(nonce: [u8; 32]) -> Vec<u8> {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(0), &nonce).unwrap();
w.end_container().unwrap();
buf
}
#[test]
fn arm_fail_safe_returns_ok_response_accepted_by_commissioner_decoder() {
let pki = build_mock_device_pki(now());
let path = CommandPath {
endpoint: 0,
cluster: 0x0030,
command: 0x00,
};
let fields = arm_fail_safe_fields();
let challenge = [0u8; 16];
let reply = respond(path, &fields, challenge, &pki);
let fields_tlv = match reply {
DeviceReply::Command(v) => v,
DeviceReply::Status(s) => panic!("expected Command, got Status({s})"),
};
use matter_commissioning::clusters::general_commissioning::decode_arm_fail_safe_response;
let decoded =
decode_arm_fail_safe_response(&fields_tlv).expect("ArmFailSafeResponse decodes");
assert_eq!(decoded.error_code, 0, "error_code must be 0 (OK)");
let text = decoded.debug_text.as_deref().unwrap_or("");
assert_eq!(text, "", "debug_text must be empty on success");
}
#[test]
fn arm_fail_safe_fields_accepted_by_on_response() {
use std::sync::Arc;
use matter_cert::time::MatterTime;
use matter_commissioning::attestation::CdSigningRoots;
use matter_commissioning::noc::{FabricRecord, NocRng, SystemNocRng};
use matter_commissioning::setup::{
CommissioningFlow, DiscoveryCapabilities, Discriminator, Passcode, SetupPayload,
};
use matter_commissioning::state_machine::CommissionerConfig;
use matter_crypto::{RingSigner, Signer};
let pki = build_mock_device_pki(now());
let (signer, _) = RingSigner::generate().unwrap();
let signer: Arc<dyn Signer> = Arc::new(signer);
let fabric = FabricRecord::new_root_only(
0x0000_0000_0000_0001,
signer,
MatterTime::from_unix_secs(1_704_067_200),
MatterTime::from_unix_secs(1_735_689_600),
42,
&SystemNocRng,
)
.unwrap();
let setup = SetupPayload {
version: 0,
vendor_id: Some(super::VID),
product_id: Some(super::PID),
commissioning_flow: CommissioningFlow::Standard,
discovery_capabilities: DiscoveryCapabilities::ON_NETWORK,
discriminator: Discriminator::new(0x0F00).unwrap(),
passcode: Passcode::new(20_202_021).unwrap(),
};
let paa = pki.paa_trust_store.clone();
let cd = CdSigningRoots::with_csa_test_roots();
let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
let mut sm = matter_commissioning::state_machine::Commissioner::new(CommissionerConfig {
pase_attestation_challenge: [0u8; 16],
fabric: &fabric,
setup_payload: &setup,
paa_trust_store: &paa,
cd_signing_roots: &cd,
commissioner_node_id: 0x1,
assigned_node_id: 0x2,
ipk_epoch_key: [0x42_u8; 16],
case_admin_subject: 0x1,
admin_vendor_id: 0xFFF1,
now: MatterTime::from_unix_secs(1_800_000_000),
rng,
network: matter_commissioning::NetworkCredentials::AlreadyOnNetwork,
})
.unwrap();
let _ = sm.poll().unwrap();
let commissioning_info_tlv = respond_read_attribute(AttributePath {
endpoint: 0,
cluster: 0x0030,
attribute: 0x0001, });
sm.on_response(Expectation::CommissioningInfo, &commissioning_info_tlv)
.expect("CommissioningInfo accepted");
let _ = sm.poll().unwrap();
let path = CommandPath {
endpoint: 0,
cluster: 0x0030,
command: 0x00,
};
let fields = arm_fail_safe_fields();
let reply = respond(path, &fields, [0u8; 16], &pki);
let fields_tlv = match reply {
DeviceReply::Command(v) => v,
DeviceReply::Status(s) => panic!("expected Command, got Status({s})"),
};
sm.on_response(Expectation::ArmFailsafeResponse, &fields_tlv)
.expect("Commissioner accepts ArmFailSafeResponse from mock device");
assert_eq!(
sm.stage(),
matter_commissioning::state_machine::Stage::ConfigRegulatory,
"state machine advanced past ArmFailsafe"
);
}
#[test]
fn attestation_request_returns_fields_accepted_by_noc_decoder() {
let pki = build_mock_device_pki(now());
let path = CommandPath {
endpoint: 0,
cluster: 0x003E,
command: 0x00,
};
let nonce = [0xAB_u8; 32];
let challenge = [0x01_u8; 16];
let fields = attestation_request_fields(nonce);
let reply = respond(path, &fields, challenge, &pki);
let fields_tlv = match reply {
DeviceReply::Command(v) => v,
DeviceReply::Status(s) => panic!("expected Command, got Status({s})"),
};
let decoded =
decode_attestation_response(&fields_tlv).expect("AttestationResponse decodes");
assert_eq!(
decoded.signature.len(),
64,
"ECDSA signature must be 64 bytes (IEEE P1363)"
);
assert!(
!decoded.attestation_elements.is_empty(),
"attestation_elements must be non-empty"
);
}
#[test]
fn attestation_response_elements_echo_the_nonce() {
use matter_commissioning::attestation::extract_attestation_elements_fields;
let pki = build_mock_device_pki(now());
let path = CommandPath {
endpoint: 0,
cluster: 0x003E,
command: 0x00,
};
let nonce = [0xCC_u8; 32];
let challenge = [0x02_u8; 16];
let fields = attestation_request_fields(nonce);
let reply = respond(path, &fields, challenge, &pki);
let DeviceReply::Command(fields_tlv) = reply else {
panic!("expected Command");
};
let att_resp = decode_attestation_response(&fields_tlv).unwrap();
let att_fields = extract_attestation_elements_fields(&att_resp.attestation_elements)
.expect("elements parse");
assert_eq!(
att_fields.attestation_nonce, nonce,
"nonce must be echoed back"
);
}
#[test]
fn cert_chain_request_pai_returns_pai_der() {
let pki = build_mock_device_pki(now());
let path = CommandPath {
endpoint: 0,
cluster: 0x003E,
command: 0x02,
};
let fields = cert_chain_request_fields(0x02); let reply = respond(path, &fields, [0u8; 16], &pki);
let fields_tlv = match reply {
DeviceReply::Command(v) => v,
DeviceReply::Status(s) => panic!("expected Command, got Status({s})"),
};
let decoded =
decode_certificate_chain_response(&fields_tlv).expect("CertChainResponse decodes");
assert_eq!(decoded.certificate, pki.pai_der, "PAI DER mismatch");
}
#[test]
fn cert_chain_request_dac_returns_dac_der() {
let pki = build_mock_device_pki(now());
let path = CommandPath {
endpoint: 0,
cluster: 0x003E,
command: 0x02,
};
let fields = cert_chain_request_fields(0x01); let reply = respond(path, &fields, [0u8; 16], &pki);
let fields_tlv = match reply {
DeviceReply::Command(v) => v,
DeviceReply::Status(s) => panic!("expected Command, got Status({s})"),
};
let decoded =
decode_certificate_chain_response(&fields_tlv).expect("CertChainResponse decodes");
assert_eq!(decoded.certificate, pki.dac_der, "DAC DER mismatch");
}
#[test]
fn add_trusted_root_emits_status_zero() {
let pki = build_mock_device_pki(now());
let path = CommandPath {
endpoint: 0,
cluster: 0x003E,
command: 0x0B,
};
let fields = vec![0x15u8, 0x18]; let reply = respond(path, &fields, [0u8; 16], &pki);
match reply {
DeviceReply::Status(0) => {}
DeviceReply::Status(s) => panic!("expected Status(0), got Status({s})"),
DeviceReply::Command(_) => panic!("expected Status, got Command"),
}
}
#[test]
fn feature_map_is_ethernet_only() {
use matter_commissioning::clusters::network_commissioning::decode_feature_map;
let value_tlv = respond_read_attribute(AttributePath {
endpoint: 0,
cluster: 0x0031,
attribute: 0xFFFC,
});
let features = decode_feature_map(&value_tlv).expect("FeatureMap decodes");
assert!(
features.contains(NetworkCommissioningFeature::ETHERNET),
"ETHERNET bit must be set"
);
assert!(
!features.contains(NetworkCommissioningFeature::WIFI),
"WIFI bit must NOT be set (Ethernet path)"
);
assert!(
!features.contains(NetworkCommissioningFeature::THREAD),
"THREAD bit must NOT be set (Ethernet path)"
);
}
#[test]
fn basic_commissioning_info_decodes_correctly() {
use matter_commissioning::clusters::general_commissioning::decode_basic_commissioning_info;
let value_tlv = respond_read_attribute(AttributePath {
endpoint: 0,
cluster: 0x0030,
attribute: 0x0001, });
let info =
decode_basic_commissioning_info(&value_tlv).expect("BasicCommissioningInfo decodes");
assert_eq!(
info.failsafe_expiry_length_seconds, 60,
"failsafe_expiry_length_seconds must be 60"
);
assert_eq!(
info.max_cumulative_failsafe_seconds, 900,
"max_cumulative_failsafe_seconds must be 900"
);
}
#[test]
fn csr_request_returns_fields_accepted_by_noc_decoder() {
use matter_commissioning::noc::decode_csr_response;
let pki = build_mock_device_pki(now());
let path = CommandPath {
endpoint: 0,
cluster: 0x003E,
command: 0x04,
};
let nonce = [0xDD_u8; 32];
let fields = csr_request_fields(nonce);
let reply = respond(path, &fields, [0u8; 16], &pki);
let fields_tlv = match reply {
DeviceReply::Command(v) => v,
DeviceReply::Status(s) => panic!("expected Command, got Status({s})"),
};
let decoded = decode_csr_response(&fields_tlv).expect("CSRResponse decodes");
assert_eq!(decoded.attestation_signature.len(), 64);
assert!(!decoded.nocsr_elements.is_empty());
}
#[test]
fn add_noc_returns_success_noc_response() {
use matter_commissioning::noc::decode_noc_response;
let pki = build_mock_device_pki(now());
let path = CommandPath {
endpoint: 0,
cluster: 0x003E,
command: 0x06,
};
let fields = vec![0x15u8, 0x18]; let reply = respond(path, &fields, [0u8; 16], &pki);
let fields_tlv = match reply {
DeviceReply::Command(v) => v,
DeviceReply::Status(s) => panic!("expected Command, got Status({s})"),
};
let decoded = decode_noc_response(&fields_tlv).expect("NOCResponse decodes");
assert_eq!(decoded.status, 0, "status must be 0 (OK)");
assert_eq!(decoded.fabric_index, Some(1), "fabric_index must be 1");
}
}
#[cfg(test)]
mod device_im_roundtrip {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use matter_codec::{Tag, TlvWriter, Value};
use matter_commissioning::im::{
build_invoke_request, build_read_request, parse_invoke_response, parse_report_data,
AttributePath, CommandPath, InvokeResponse,
};
use super::{
build_invoke_response, build_invoke_status_response, build_report_data,
parse_invoke_request, parse_read_request,
};
#[test]
fn parse_invoke_request_is_inverse_of_build_invoke_request() {
let path = CommandPath {
endpoint: 0,
cluster: 0x0030,
command: 0x00,
};
let mut fields_buf = Vec::new();
{
let mut w = TlvWriter::new(&mut fields_buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), 60).unwrap(); w.put_uint(Tag::Context(1), 1).unwrap(); w.end_container().unwrap();
}
let request_bytes = build_invoke_request(path, &fields_buf);
let decoded = parse_invoke_request(&request_bytes);
assert_eq!(decoded.path, path);
assert_eq!(
decoded.fields_tlv, fields_buf,
"fields_tlv mismatch: got {:02X?}, expected {fields_buf:02X?}",
decoded.fields_tlv
);
}
#[test]
fn parse_invoke_response_accepts_build_invoke_response() {
let path = CommandPath {
endpoint: 0,
cluster: 0x0030,
command: 0x01, };
let mut fields_buf = Vec::new();
{
let mut w = TlvWriter::new(&mut fields_buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_uint(Tag::Context(0), 0).unwrap(); w.end_container().unwrap();
}
let response_bytes = build_invoke_response(path, &fields_buf);
let parsed = parse_invoke_response(&response_bytes).unwrap();
match parsed {
InvokeResponse::Command {
path: p,
fields_tlv,
} => {
assert_eq!(p, path);
assert_eq!(
fields_tlv, fields_buf,
"fields_tlv mismatch: got {fields_tlv:02X?}, expected {fields_buf:02X?}",
);
}
InvokeResponse::Status(s) => panic!("expected Command, got Status({s:?})"),
}
}
#[test]
fn parse_invoke_response_accepts_build_invoke_status_response() {
let path = CommandPath {
endpoint: 0,
cluster: 0x003E, command: 0x0B, };
let response_bytes = build_invoke_status_response(path, 0x00); let parsed = parse_invoke_response(&response_bytes).unwrap();
assert!(
matches!(parsed, InvokeResponse::Status(_)),
"expected Status, got {parsed:?}"
);
if let InvokeResponse::Status(status) = parsed {
let status_name = format!("{status:?}");
assert!(
status_name.contains("Success") || status_name.contains('0'),
"expected Success status, got {status_name}"
);
}
}
#[test]
fn parse_read_request_is_inverse_of_build_read_request() {
let paths = vec![
AttributePath {
endpoint: 0,
cluster: 0x0030,
attribute: 0x0001, },
AttributePath {
endpoint: 0,
cluster: 0x0031,
attribute: 0xFFFC, },
];
let request_bytes = build_read_request(&paths);
let decoded = parse_read_request(&request_bytes);
assert_eq!(decoded.len(), paths.len());
assert_eq!(decoded, paths);
}
#[test]
fn parse_report_data_accepts_build_report_data() {
let path1 = AttributePath {
endpoint: 0,
cluster: 0x0030,
attribute: 0x0004,
};
let mut val1_buf = Vec::new();
{
let mut w = TlvWriter::new(&mut val1_buf);
w.put_uint(Tag::Anonymous, 0x01).unwrap();
}
let path2 = AttributePath {
endpoint: 0,
cluster: 0x0031,
attribute: 0xFFFC,
};
let mut val2_buf = Vec::new();
{
let mut w = TlvWriter::new(&mut val2_buf);
w.put_uint(Tag::Anonymous, 3).unwrap();
}
let report_bytes =
build_report_data(&[(path1, val1_buf.as_slice()), (path2, val2_buf.as_slice())]);
let parsed = parse_report_data(&report_bytes).unwrap();
let attrs: Vec<_> = parsed.attributes().collect();
assert_eq!(attrs.len(), 2);
let (p0, v0) = attrs[0];
assert_eq!(*p0, path1);
assert_eq!(*v0, Value::Uint(0x01));
let (p1, v1) = attrs[1];
assert_eq!(*p1, path2);
assert_eq!(*v1, Value::Uint(3));
}
#[test]
fn roundtrip_empty_command_fields() {
let path = CommandPath {
endpoint: 0,
cluster: 0x003E,
command: 0x0B, };
let empty_fields = vec![0x15u8, 0x18];
let req = build_invoke_request(path, &empty_fields);
let decoded = parse_invoke_request(&req);
assert_eq!(decoded.path, path);
assert_eq!(decoded.fields_tlv, empty_fields);
let resp = build_invoke_response(path, &empty_fields);
match parse_invoke_response(&resp).unwrap() {
InvokeResponse::Command {
path: p,
fields_tlv,
} => {
assert_eq!(p, path);
assert_eq!(fields_tlv, empty_fields);
}
InvokeResponse::Status(s) => panic!("expected Command, got {s:?}"),
}
}
}
mod op {
pub const PBKDF_PARAM_REQUEST: u8 = 0x20;
pub const PBKDF_PARAM_RESPONSE: u8 = 0x21;
pub const PASE_PAKE1: u8 = 0x22;
pub const PASE_PAKE2: u8 = 0x23;
pub const PASE_PAKE3: u8 = 0x24;
pub const CASE_SIGMA1: u8 = 0x30;
pub const CASE_SIGMA2: u8 = 0x31;
pub const CASE_SIGMA3: u8 = 0x32;
pub const MRP_STANDALONE_ACK: u8 = 0x10;
pub const STATUS_REPORT: u8 = 0x40;
pub const IM_INVOKE_REQUEST: u8 = 0x08;
pub const IM_INVOKE_RESPONSE: u8 = 0x09;
pub const IM_READ_REQUEST: u8 = 0x02;
pub const IM_REPORT_DATA: u8 = 0x05;
}
const CMD_COMMISSIONING_COMPLETE: u32 = 0x04;
async fn close_handshake_with_status_report<T: AsyncDatagram>(
dev_io: &T,
peer: std::net::SocketAddr,
counter: u32,
exchange_id: u16,
ack_counter: u32,
) -> Result<(), DriverError> {
let mut body = Vec::with_capacity(8);
body.extend_from_slice(&0u16.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u16.to_le_bytes()); let report = encode_unsecured(
counter,
exchange_id,
op::STATUS_REPORT,
ProtocolId::SECURE_CHANNEL,
false,
true,
Some(ack_counter),
None,
&body,
);
dev_io.send_to(&report, peer).await?;
let (p, _) = dev_io.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::MRP_STANDALONE_ACK);
debug_assert_eq!(m.ack_counter, Some(counter));
Ok(())
}
pub struct MockDeviceCaseSetup {
pub credentials: CaseCredentials,
pub trusted_roots: TrustedRoots,
pub responder_session_id: u16,
pub now: MatterTime,
}
#[allow(clippy::too_many_lines, clippy::items_after_statements)]
pub async fn run_mock_device(
dev_io: &InMemoryDatagram,
peer: std::net::SocketAddr,
pki: &MockDevicePki,
pase_pin: u32,
pase_params: PasePbkdfParams,
pase_responder_session_id: u16,
case_setup: MockDeviceCaseSetup,
) -> Result<(), DriverError> {
use std::time::Instant;
const CTRL_PASE_SESSION_ID: u16 = 1;
let mut sessions = SessionManager::new();
let mut verifier = PaseVerifier::new_from_pin(pase_pin, pase_params, pase_responder_session_id)
.map_err(DriverError::Crypto)?;
let mut unsecured_ctr: u32 = 100;
let (p, _) = dev_io.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::PBKDF_PARAM_REQUEST);
verifier
.handle_pbkdf_request(&m.payload)
.map_err(DriverError::Crypto)?;
let resp = verifier.next_message().map_err(DriverError::Crypto)?;
let wire = encode_unsecured(
unsecured_ctr,
m.exchange_id,
op::PBKDF_PARAM_RESPONSE,
ProtocolId::SECURE_CHANNEL,
false,
true,
Some(m.message_counter),
None,
&resp,
);
unsecured_ctr += 1;
dev_io.send_to(&wire, peer).await?;
let (p, _) = dev_io.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::PASE_PAKE1);
verifier
.handle_pake1(&m.payload)
.map_err(DriverError::Crypto)?;
let pake2 = verifier.next_message().map_err(DriverError::Crypto)?;
let wire = encode_unsecured(
unsecured_ctr,
m.exchange_id,
op::PASE_PAKE2,
ProtocolId::SECURE_CHANNEL,
false,
true,
Some(m.message_counter),
None,
&pake2,
);
dev_io.send_to(&wire, peer).await?;
let (p, _) = dev_io.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::PASE_PAKE3);
verifier
.handle_pake3(&m.payload)
.map_err(DriverError::Crypto)?;
unsecured_ctr += 1;
close_handshake_with_status_report(
dev_io,
peer,
unsecured_ctr,
m.exchange_id,
m.message_counter,
)
.await?;
let pase_keys = verifier.finish().map_err(DriverError::Crypto)?;
let attestation_challenge: [u8; 16] = pase_keys.attestation_key;
let pase_sid = SessionId(pase_responder_session_id);
sessions.register_pase_with_local_id(
pase_sid,
pase_keys,
SessionRole::Responder,
CTRL_PASE_SESSION_ID,
PeerHint::default(),
);
async fn service_secured_im(
dev_io: &InMemoryDatagram,
peer: std::net::SocketAddr,
sessions: &mut SessionManager,
session_id: SessionId,
packet: &[u8],
challenge: [u8; 16],
pki: &MockDevicePki,
) -> Result<Option<(u32, u32)>, DriverError> {
let decoded = sessions.decode_inbound(packet, Instant::now())?;
let (exchange_id, payload) = match decoded {
DecodeInboundOutput::AppMessage {
exchange_id,
payload,
..
} => (exchange_id, payload),
DecodeInboundOutput::DuplicateReliableAckResent { ack_packet, .. } => {
dev_io.send_to(&ack_packet, peer).await?;
return Ok(None);
}
_ => return Ok(None),
};
if is_read_request(&payload) {
let paths = parse_read_request(&payload);
let values: Vec<Vec<u8>> = paths.iter().map(|p| respond_read_attribute(*p)).collect();
let pairs: Vec<(matter_commissioning::im::AttributePath, &[u8])> = paths
.iter()
.zip(values.iter())
.map(|(p, v)| (*p, v.as_slice()))
.collect();
let reply_bytes = build_report_data(&pairs);
let out = sessions.encode_outbound(
session_id,
Some(exchange_id),
op::IM_REPORT_DATA,
ProtocolId::INTERACTION_MODEL,
&reply_bytes,
MrpFlags { reliable: true },
Instant::now(),
)?;
dev_io.send_to(&out.wire_bytes, peer).await?;
Ok(None)
} else {
let decoded_req = parse_invoke_request(&payload);
let path = decoded_req.path;
let serviced = (path.cluster, path.command);
let reply_bytes = match respond(path, &decoded_req.fields_tlv, challenge, pki) {
DeviceReply::Command(fields_tlv) => build_invoke_response(path, &fields_tlv),
DeviceReply::Status(code) => build_invoke_status_response(path, code),
};
let out = sessions.encode_outbound(
session_id,
Some(exchange_id),
op::IM_INVOKE_RESPONSE,
ProtocolId::INTERACTION_MODEL,
&reply_bytes,
MrpFlags { reliable: true },
Instant::now(),
)?;
dev_io.send_to(&out.wire_bytes, peer).await?;
Ok(Some(serviced))
}
}
let sigma1_packet: Vec<u8> = loop {
let (packet, _from) = dev_io.recv_from().await?;
let session_id_field = u16::from_le_bytes([packet[1], packet[2]]);
if session_id_field == 0 {
break packet;
}
service_secured_im(
dev_io,
peer,
&mut sessions,
pase_sid,
&packet,
attestation_challenge,
pki,
)
.await?;
};
let MockDeviceCaseSetup {
credentials,
trusted_roots,
responder_session_id,
now: case_now,
} = case_setup;
let mut responder =
CaseResponder::new(credentials, trusted_roots, responder_session_id, case_now)
.map_err(DriverError::Crypto)?;
let m = decode_unsecured(&sigma1_packet)?;
debug_assert_eq!(m.opcode, op::CASE_SIGMA1);
match responder
.handle_sigma1(&m.payload)
.map_err(DriverError::Crypto)?
{
Sigma1Outcome::NewSession => {}
Sigma1Outcome::ResumptionRequested { .. } => {
return Err(DriverError::Handshake(
"mock device CASE Sigma1 was not a fresh session",
));
}
}
let sigma2 = responder.next_message().map_err(DriverError::Crypto)?;
let wire = encode_unsecured(
unsecured_ctr,
m.exchange_id,
op::CASE_SIGMA2,
ProtocolId::SECURE_CHANNEL,
false,
true,
Some(m.message_counter),
None,
&sigma2,
);
dev_io.send_to(&wire, peer).await?;
let (p, _) = dev_io.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::CASE_SIGMA3);
responder
.handle_sigma3(&m.payload)
.map_err(DriverError::Crypto)?;
unsecured_ctr += 1;
close_handshake_with_status_report(
dev_io,
peer,
unsecured_ctr,
m.exchange_id,
m.message_counter,
)
.await?;
let case_output = responder.finish().map_err(DriverError::Crypto)?;
let case_sid = sessions.register_case(&case_output, SessionRole::Responder);
loop {
let (packet, _from) = dev_io.recv_from().await?;
let serviced = service_secured_im(
dev_io,
peer,
&mut sessions,
case_sid,
&packet,
attestation_challenge,
pki,
)
.await?;
if let Some((cluster, command)) = serviced {
if cluster == matter_commissioning::clusters::general_commissioning::CLUSTER_ID
&& command == CMD_COMMISSIONING_COMPLETE
{
return Ok(());
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MockNetworkKind {
WiFi,
Thread,
}
#[derive(Clone, Debug)]
pub struct CapturedCommand {
pub cluster: u32,
pub command: u32,
pub fields_tlv: Vec<u8>,
}
pub type CommandLog = std::sync::Arc<std::sync::Mutex<Vec<CapturedCommand>>>;
#[allow(clippy::too_many_arguments)]
async fn service_secured_im_dual(
dev_io: &InMemoryDatagram,
peer: std::net::SocketAddr,
sessions: &mut SessionManager,
session_id: SessionId,
packet: &[u8],
challenge: [u8; 16],
pki: &MockDevicePki,
network: MockNetworkKind,
command_log: &CommandLog,
) -> Result<Option<(u32, u32)>, DriverError> {
use std::time::Instant;
let decoded = sessions.decode_inbound(packet, Instant::now())?;
let (exchange_id, payload) = match decoded {
DecodeInboundOutput::AppMessage {
exchange_id,
payload,
..
} => (exchange_id, payload),
DecodeInboundOutput::DuplicateReliableAckResent { ack_packet, .. } => {
dev_io.send_to(&ack_packet, peer).await?;
return Ok(None);
}
_ => return Ok(None),
};
if is_read_request(&payload) {
let paths = parse_read_request(&payload);
let values: Vec<Vec<u8>> = paths
.iter()
.map(|p| match network {
MockNetworkKind::WiFi => respond_read_attribute_wifi(*p),
MockNetworkKind::Thread => respond_read_attribute_thread(*p),
})
.collect();
let pairs: Vec<(matter_commissioning::im::AttributePath, &[u8])> = paths
.iter()
.zip(values.iter())
.map(|(p, v)| (*p, v.as_slice()))
.collect();
let reply_bytes = build_report_data(&pairs);
let out = sessions.encode_outbound(
session_id,
Some(exchange_id),
op::IM_REPORT_DATA,
ProtocolId::INTERACTION_MODEL,
&reply_bytes,
MrpFlags { reliable: true },
Instant::now(),
)?;
dev_io.send_to(&out.wire_bytes, peer).await?;
Ok(None)
} else {
let decoded_req = parse_invoke_request(&payload);
let path = decoded_req.path;
command_log.lock().unwrap().push(CapturedCommand {
cluster: path.cluster,
command: path.command,
fields_tlv: decoded_req.fields_tlv.clone(),
});
let serviced = (path.cluster, path.command);
let reply_bytes = match respond(path, &decoded_req.fields_tlv, challenge, pki) {
DeviceReply::Command(fields_tlv) => build_invoke_response(path, &fields_tlv),
DeviceReply::Status(code) => build_invoke_status_response(path, code),
};
let out = sessions.encode_outbound(
session_id,
Some(exchange_id),
op::IM_INVOKE_RESPONSE,
ProtocolId::INTERACTION_MODEL,
&reply_bytes,
MrpFlags { reliable: true },
Instant::now(),
)?;
dev_io.send_to(&out.wire_bytes, peer).await?;
Ok(Some(serviced))
}
}
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
pub async fn run_mock_device_dual(
btp_dev: &InMemoryDatagram,
udp_dev: &InMemoryDatagram,
btp_peer: std::net::SocketAddr,
udp_peer: std::net::SocketAddr,
pki: &MockDevicePki,
pase_pin: u32,
pase_params: PasePbkdfParams,
pase_responder_session_id: u16,
case_setup: MockDeviceCaseSetup,
network: MockNetworkKind,
command_log: &CommandLog,
) -> Result<(), DriverError> {
const CTRL_PASE_SESSION_ID: u16 = 1;
let mut sessions = SessionManager::new();
let mut verifier = PaseVerifier::new_from_pin(pase_pin, pase_params, pase_responder_session_id)
.map_err(DriverError::Crypto)?;
let mut unsecured_ctr: u32 = 100;
let (p, _) = btp_dev.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::PBKDF_PARAM_REQUEST);
verifier
.handle_pbkdf_request(&m.payload)
.map_err(DriverError::Crypto)?;
let resp = verifier.next_message().map_err(DriverError::Crypto)?;
btp_dev
.send_to(
&encode_unsecured(
unsecured_ctr,
m.exchange_id,
op::PBKDF_PARAM_RESPONSE,
ProtocolId::SECURE_CHANNEL,
false,
false, Some(m.message_counter),
None,
&resp,
),
btp_peer,
)
.await?;
unsecured_ctr += 1;
let (p, _) = btp_dev.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::PASE_PAKE1);
verifier
.handle_pake1(&m.payload)
.map_err(DriverError::Crypto)?;
let pake2 = verifier.next_message().map_err(DriverError::Crypto)?;
btp_dev
.send_to(
&encode_unsecured(
unsecured_ctr,
m.exchange_id,
op::PASE_PAKE2,
ProtocolId::SECURE_CHANNEL,
false,
false,
Some(m.message_counter),
None,
&pake2,
),
btp_peer,
)
.await?;
unsecured_ctr += 1;
let (p, _) = btp_dev.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::PASE_PAKE3);
verifier
.handle_pake3(&m.payload)
.map_err(DriverError::Crypto)?;
let mut body = Vec::with_capacity(8);
body.extend_from_slice(&0u16.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u16.to_le_bytes()); btp_dev
.send_to(
&encode_unsecured(
unsecured_ctr,
m.exchange_id,
op::STATUS_REPORT,
ProtocolId::SECURE_CHANNEL,
false,
false, Some(m.message_counter),
None,
&body,
),
btp_peer,
)
.await?;
unsecured_ctr += 1;
let pase_keys = verifier.finish().map_err(DriverError::Crypto)?;
let attestation_challenge: [u8; 16] = pase_keys.attestation_key;
let pase_sid = SessionId(pase_responder_session_id);
sessions.register_pase_with_local_id(
pase_sid,
pase_keys,
SessionRole::Responder,
CTRL_PASE_SESSION_ID,
PeerHint::default(),
);
let sigma1_packet: Vec<u8> = loop {
tokio::select! {
r = btp_dev.recv_from() => {
let (packet, _from) = r?;
service_secured_im_dual(
btp_dev,
btp_peer,
&mut sessions,
pase_sid,
&packet,
attestation_challenge,
pki,
network,
command_log,
)
.await?;
}
r = udp_dev.recv_from() => {
let (packet, _from) = r?;
break packet;
}
}
};
let MockDeviceCaseSetup {
credentials,
trusted_roots,
responder_session_id,
now: case_now,
} = case_setup;
let mut responder =
CaseResponder::new(credentials, trusted_roots, responder_session_id, case_now)
.map_err(DriverError::Crypto)?;
let m = decode_unsecured(&sigma1_packet)?;
debug_assert_eq!(m.opcode, op::CASE_SIGMA1);
match responder
.handle_sigma1(&m.payload)
.map_err(DriverError::Crypto)?
{
Sigma1Outcome::NewSession => {}
Sigma1Outcome::ResumptionRequested { .. } => {
return Err(DriverError::Handshake(
"mock dual device CASE Sigma1 was not a fresh session",
));
}
}
let sigma2 = responder.next_message().map_err(DriverError::Crypto)?;
udp_dev
.send_to(
&encode_unsecured(
unsecured_ctr,
m.exchange_id,
op::CASE_SIGMA2,
ProtocolId::SECURE_CHANNEL,
false,
true, Some(m.message_counter),
None,
&sigma2,
),
udp_peer,
)
.await?;
let (p, _) = udp_dev.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::CASE_SIGMA3);
responder
.handle_sigma3(&m.payload)
.map_err(DriverError::Crypto)?;
unsecured_ctr += 1;
close_handshake_with_status_report(
udp_dev,
udp_peer,
unsecured_ctr,
m.exchange_id,
m.message_counter,
)
.await?;
let case_output = responder.finish().map_err(DriverError::Crypto)?;
let case_sid = sessions.register_case(&case_output, SessionRole::Responder);
loop {
let (packet, _from) = udp_dev.recv_from().await?;
let serviced = service_secured_im_dual(
udp_dev,
udp_peer,
&mut sessions,
case_sid,
&packet,
attestation_challenge,
pki,
network,
command_log,
)
.await?;
if let Some((cluster, command)) = serviced {
if cluster == matter_commissioning::clusters::general_commissioning::CLUSTER_ID
&& command == CMD_COMMISSIONING_COMPLETE
{
return Ok(());
}
}
}
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
pub async fn run_mock_device_dual_silent_after(
btp_dev: &InMemoryDatagram,
_udp_dev: &InMemoryDatagram,
btp_peer: std::net::SocketAddr,
_udp_peer: std::net::SocketAddr,
pki: &MockDevicePki,
pase_pin: u32,
pase_params: PasePbkdfParams,
pase_responder_session_id: u16,
service_pre_op_stages: usize,
) -> Result<(), DriverError> {
const CTRL_PASE_SESSION_ID: u16 = 1;
let mut sessions = SessionManager::new();
let mut verifier = PaseVerifier::new_from_pin(pase_pin, pase_params, pase_responder_session_id)
.map_err(DriverError::Crypto)?;
let mut unsecured_ctr: u32 = 100;
let (p, _) = btp_dev.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::PBKDF_PARAM_REQUEST);
verifier
.handle_pbkdf_request(&m.payload)
.map_err(DriverError::Crypto)?;
let resp = verifier.next_message().map_err(DriverError::Crypto)?;
btp_dev
.send_to(
&encode_unsecured(
unsecured_ctr,
m.exchange_id,
op::PBKDF_PARAM_RESPONSE,
ProtocolId::SECURE_CHANNEL,
false,
false, Some(m.message_counter),
None,
&resp,
),
btp_peer,
)
.await?;
unsecured_ctr += 1;
let (p, _) = btp_dev.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::PASE_PAKE1);
verifier
.handle_pake1(&m.payload)
.map_err(DriverError::Crypto)?;
let pake2 = verifier.next_message().map_err(DriverError::Crypto)?;
btp_dev
.send_to(
&encode_unsecured(
unsecured_ctr,
m.exchange_id,
op::PASE_PAKE2,
ProtocolId::SECURE_CHANNEL,
false,
false,
Some(m.message_counter),
None,
&pake2,
),
btp_peer,
)
.await?;
unsecured_ctr += 1;
let (p, _) = btp_dev.recv_from().await?;
let m = decode_unsecured(&p)?;
debug_assert_eq!(m.opcode, op::PASE_PAKE3);
verifier
.handle_pake3(&m.payload)
.map_err(DriverError::Crypto)?;
let mut body = Vec::with_capacity(8);
body.extend_from_slice(&0u16.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u16.to_le_bytes()); btp_dev
.send_to(
&encode_unsecured(
unsecured_ctr,
m.exchange_id,
op::STATUS_REPORT,
ProtocolId::SECURE_CHANNEL,
false,
false, Some(m.message_counter),
None,
&body,
),
btp_peer,
)
.await?;
let pase_keys = verifier.finish().map_err(DriverError::Crypto)?;
let attestation_challenge: [u8; 16] = pase_keys.attestation_key;
let pase_sid = SessionId(pase_responder_session_id);
sessions.register_pase_with_local_id(
pase_sid,
pase_keys,
SessionRole::Responder,
CTRL_PASE_SESSION_ID,
PeerHint::default(),
);
let discard_log: CommandLog = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
for _ in 0..service_pre_op_stages {
let (packet, _from) = btp_dev.recv_from().await?;
service_secured_im_dual(
btp_dev,
btp_peer,
&mut sessions,
pase_sid,
&packet,
attestation_challenge,
pki,
MockNetworkKind::WiFi,
&discard_log,
)
.await?;
}
std::future::pending::<()>().await;
unreachable!("run_mock_device_dual_silent_after parks forever until dropped")
}
fn is_read_request(payload: &[u8]) -> bool {
use matter_codec::{ContainerKind, Element, Tag, TlvReader};
let mut r = TlvReader::new(payload);
match r.next().expect("IM payload: first element") {
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
other => panic!("IM payload: expected top-level anon struct, got {other:?}"),
}
match r.next().expect("IM payload: first member") {
Some(Element::ContainerStart {
tag: Tag::Context(0),
kind: ContainerKind::Array,
}) => true,
_ => false,
}
}