#![forbid(unsafe_code)]
use crate::clusters::network_commissioning::truncate_utf8;
use crate::noc::error::NocError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CsrResponse {
pub nocsr_elements: Vec<u8>,
pub attestation_signature: [u8; 64],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NocResponse {
pub status: u8,
pub fabric_index: Option<u8>,
pub debug_text: Option<String>,
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_attestation_request(nonce: &[u8; 32]) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bytes(Tag::Context(0), nonce)
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
pub fn decode_attestation_response(
tlv: &[u8],
) -> Result<crate::attestation::AttestationResponse, NocError> {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader.next()? {
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
_ => {
return Err(NocError::MalformedResponse(
"AttestationResponse: expected an anonymous structure",
))
}
}
let mut elements: Option<Vec<u8>> = None;
let mut sig: Option<[u8; 64]> = None;
loop {
match reader.next()? {
None => {
return Err(NocError::ClusterCodec(
matter_codec::Error::UnclosedContainer,
))
}
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Bytes(b),
}) => {
if elements.is_some() {
return Err(NocError::MalformedResponse(
"AttestationResponse: duplicate AttestationElements field",
));
}
elements = Some(b);
}
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Bytes(b),
}) => {
if sig.is_some() {
return Err(NocError::MalformedResponse(
"AttestationResponse: duplicate Signature field",
));
}
let arr: [u8; 64] = b.as_slice().try_into().map_err(|_| {
NocError::MalformedResponse(
"AttestationResponse: Signature is not exactly 64 bytes",
)
})?;
sig = Some(arr);
}
Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
Some(_) => {
return Err(NocError::MalformedResponse(
"AttestationResponse: unexpected element",
))
}
}
}
Ok(crate::attestation::AttestationResponse {
attestation_elements: elements.ok_or(NocError::MalformedResponse(
"AttestationResponse: missing AttestationElements field",
))?,
signature: sig.ok_or(NocError::MalformedResponse(
"AttestationResponse: missing Signature field",
))?,
})
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum CertChainType {
Dac = 0x01,
Pai = 0x02,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CertificateChainResponse {
pub certificate: Vec<u8>,
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_certificate_chain_request(cert_type: CertChainType) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_uint(Tag::Context(0), u64::from(cert_type as u8))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
pub fn decode_certificate_chain_response(tlv: &[u8]) -> Result<CertificateChainResponse, NocError> {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader.next()? {
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
_ => {
return Err(NocError::MalformedResponse(
"CertificateChainResponse: expected an anonymous structure",
))
}
}
let mut cert: Option<Vec<u8>> = None;
loop {
match reader.next()? {
None => {
return Err(NocError::ClusterCodec(
matter_codec::Error::UnclosedContainer,
))
}
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Bytes(b),
}) => {
if cert.is_some() {
return Err(NocError::MalformedResponse(
"CertificateChainResponse: duplicate Certificate field",
));
}
cert = Some(b);
}
Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
Some(_) => {
return Err(NocError::MalformedResponse(
"CertificateChainResponse: unexpected element",
))
}
}
}
Ok(CertificateChainResponse {
certificate: cert.ok_or(NocError::MalformedResponse(
"CertificateChainResponse: missing Certificate field",
))?,
})
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_csr_request(nonce: &[u8; 32], is_for_update_noc: bool) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bytes(Tag::Context(0), nonce)
.expect("infallible: vec writer");
if is_for_update_noc {
w.put_bool(Tag::Context(1), true)
.expect("infallible: vec writer");
}
w.end_container().expect("infallible: vec writer");
buf
}
pub fn decode_csr_response(tlv: &[u8]) -> Result<CsrResponse, NocError> {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader.next()? {
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
_ => {
return Err(NocError::MalformedResponse(
"CSRResponse: expected an anonymous structure",
))
}
}
let mut nocsr: Option<Vec<u8>> = None;
let mut sig: Option<[u8; 64]> = None;
loop {
match reader.next()? {
None => {
return Err(NocError::ClusterCodec(
matter_codec::Error::UnclosedContainer,
))
}
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Bytes(b),
}) => {
if nocsr.is_some() {
return Err(NocError::MalformedResponse(
"CSRResponse: duplicate NOCSRElements field",
));
}
nocsr = Some(b);
}
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Bytes(b),
}) => {
if sig.is_some() {
return Err(NocError::MalformedResponse(
"CSRResponse: duplicate AttestationSignature field",
));
}
let arr: [u8; 64] = b.as_slice().try_into().map_err(|_| {
NocError::MalformedResponse(
"CSRResponse: AttestationSignature is not exactly 64 bytes",
)
})?;
sig = Some(arr);
}
Some(Element::Scalar { .. } | Element::ContainerStart { .. }) => {}
Some(_) => {
return Err(NocError::MalformedResponse(
"CSRResponse: unexpected element",
))
}
}
}
Ok(CsrResponse {
nocsr_elements: nocsr.ok_or(NocError::MalformedResponse(
"CSRResponse: missing NOCSRElements field",
))?,
attestation_signature: sig.ok_or(NocError::MalformedResponse(
"CSRResponse: missing AttestationSignature field",
))?,
})
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_add_trusted_root(rcac_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)
.expect("infallible: vec writer");
w.put_bytes(Tag::Context(0), rcac_tlv)
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_add_noc(
noc_tlv: &[u8],
icac_tlv: Option<&[u8]>,
ipk: &[u8; 16],
case_admin_subject: u64,
admin_vendor_id: u16,
) -> Vec<u8> {
use matter_codec::{Tag, TlvWriter};
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous)
.expect("infallible: vec writer");
w.put_bytes(Tag::Context(0), noc_tlv)
.expect("infallible: vec writer");
if let Some(icac) = icac_tlv {
w.put_bytes(Tag::Context(1), icac)
.expect("infallible: vec writer");
}
w.put_bytes(Tag::Context(2), ipk)
.expect("infallible: vec writer");
w.put_uint(Tag::Context(3), case_admin_subject)
.expect("infallible: vec writer");
w.put_uint(Tag::Context(4), u64::from(admin_vendor_id))
.expect("infallible: vec writer");
w.end_container().expect("infallible: vec writer");
buf
}
#[must_use]
#[allow(clippy::expect_used, clippy::missing_panics_doc)] pub fn encode_update_noc(noc_tlv: &[u8], icac_tlv: 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)
.expect("infallible: vec writer");
w.put_bytes(Tag::Context(0), noc_tlv)
.expect("infallible: vec writer");
if let Some(icac) = icac_tlv {
w.put_bytes(Tag::Context(1), icac)
.expect("infallible: vec writer");
}
w.end_container().expect("infallible: vec writer");
buf
}
pub fn decode_noc_response(tlv: &[u8]) -> Result<NocResponse, NocError> {
use matter_codec::{ContainerKind, Element, Tag, TlvReader, Value};
let mut reader = TlvReader::new(tlv);
match reader.next()? {
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
}) => {}
_ => {
return Err(NocError::MalformedResponse(
"NOCResponse: expected an anonymous structure",
))
}
}
let mut status: Option<u8> = None;
let mut fabric_index: Option<u8> = None;
let mut debug_text: Option<String> = None;
loop {
match reader.next()? {
None => {
return Err(NocError::ClusterCodec(
matter_codec::Error::UnclosedContainer,
))
}
Some(Element::ContainerEnd) => break,
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Uint(v),
}) => {
let n = u8::try_from(v).map_err(|_| {
NocError::MalformedResponse("NOCResponse: StatusCode exceeds u8")
})?;
status = Some(n);
}
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Uint(v),
}) => {
let n = u8::try_from(v).map_err(|_| {
NocError::MalformedResponse("NOCResponse: FabricIndex exceeds u8")
})?;
fabric_index = Some(n);
}
Some(Element::Scalar {
tag: Tag::Context(2),
value: Value::Utf8(s),
}) => {
debug_text = Some(truncate_utf8(s, 512));
}
Some(_) => {}
}
}
Ok(NocResponse {
status: status.ok_or(NocError::MalformedResponse(
"NOCResponse: missing StatusCode field",
))?,
fabric_index,
debug_text,
})
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::items_after_statements
)] mod tests {
use super::*;
use matter_codec::{Tag, TlvWriter};
fn write_csr_response(nocsr: &[u8], att_sig: &[u8; 64]) -> 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), nocsr).unwrap();
w.put_bytes(Tag::Context(1), att_sig).unwrap();
w.end_container().unwrap();
buf
}
#[test]
fn decode_csr_response_roundtrips() {
let nocsr = b"opaque nocsr bytes".to_vec();
let sig = [0xAB; 64];
let tlv = write_csr_response(&nocsr, &sig);
let decoded = decode_csr_response(&tlv).unwrap();
assert_eq!(decoded.nocsr_elements, nocsr);
assert_eq!(decoded.attestation_signature, sig);
}
#[test]
fn decode_csr_response_rejects_missing_signature() {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(0), b"some nocsr").unwrap();
w.end_container().unwrap();
assert!(matches!(
decode_csr_response(&buf),
Err(NocError::MalformedResponse(_))
));
}
#[test]
fn decode_csr_response_rejects_wrong_sig_length() {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(0), b"some nocsr").unwrap();
w.put_bytes(Tag::Context(1), &[0u8; 32]).unwrap();
w.end_container().unwrap();
assert!(matches!(
decode_csr_response(&buf),
Err(NocError::MalformedResponse(_))
));
}
#[test]
fn decode_csr_response_rejects_non_struct_outer() {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.put_uint(Tag::Anonymous, 7).unwrap();
match decode_csr_response(&buf) {
Err(NocError::MalformedResponse(_)) => {}
other => panic!("expected MalformedResponse, got {other:?}"),
}
}
#[test]
fn decode_noc_response_rejects_non_struct_outer() {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.put_uint(Tag::Anonymous, 7).unwrap();
match decode_noc_response(&buf) {
Err(NocError::MalformedResponse(_)) => {}
other => panic!("expected MalformedResponse, got {other:?}"),
}
}
fn write_noc_response(
status: u8,
fabric_index: Option<u8>,
debug_text: Option<&str>,
) -> 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(status)).unwrap();
if let Some(fi) = fabric_index {
w.put_uint(Tag::Context(1), u64::from(fi)).unwrap();
}
if let Some(text) = debug_text {
w.put_utf8(Tag::Context(2), text).unwrap();
}
w.end_container().unwrap();
buf
}
#[test]
fn decode_noc_response_ok_status() {
let tlv = write_noc_response(0, Some(3), None);
let r = decode_noc_response(&tlv).unwrap();
assert_eq!(r.status, 0);
assert_eq!(r.fabric_index, Some(3));
assert_eq!(r.debug_text, None);
}
#[test]
fn decode_noc_response_with_debug_text() {
let tlv = write_noc_response(0, Some(1), Some("welcome to the fabric"));
let r = decode_noc_response(&tlv).unwrap();
assert_eq!(r.debug_text.as_deref(), Some("welcome to the fabric"));
}
#[test]
fn decode_noc_response_failure_status_no_index() {
let tlv = write_noc_response(9, None, Some("invalid NOC"));
let r = decode_noc_response(&tlv).unwrap();
assert_eq!(r.status, 9);
assert_eq!(r.fabric_index, None);
assert_eq!(r.debug_text.as_deref(), Some("invalid NOC"));
}
#[test]
fn decode_noc_response_caps_debug_text_at_512_bytes() {
let long_text = "x".repeat(600);
let tlv = write_noc_response(0, Some(1), Some(&long_text));
let r = decode_noc_response(&tlv).unwrap();
assert_eq!(r.debug_text.unwrap().len(), 512, "capped at spec bound");
}
#[test]
fn decode_noc_response_rejects_missing_status() {
let mut buf = Vec::new();
let mut w = TlvWriter::new(&mut buf);
w.start_structure(Tag::Anonymous).unwrap();
w.end_container().unwrap();
assert!(matches!(
decode_noc_response(&buf),
Err(NocError::MalformedResponse(_))
));
}
#[test]
fn encode_csr_request_then_parse() {
let nonce = [0x11u8; 32];
let bytes = encode_csr_request(&nonce, false);
use matter_codec::{ContainerKind, Element, TlvReader, Value};
let mut r = TlvReader::new(&bytes);
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
})
));
match r.next().unwrap() {
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Bytes(b),
}) => assert_eq!(b, nonce.to_vec()),
other => panic!("unexpected: {other:?}"),
}
assert!(matches!(r.next().unwrap(), Some(Element::ContainerEnd)));
}
#[test]
fn encode_csr_request_with_update_flag_emits_field_1() {
let nonce = [0x11u8; 32];
let bytes = encode_csr_request(&nonce, true);
use matter_codec::{Element, TlvReader, Value};
let mut r = TlvReader::new(&bytes);
let _ = r.next();
let _ = r.next();
match r.next().unwrap() {
Some(Element::Scalar {
tag: Tag::Context(1),
value: Value::Bool(true),
}) => {}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn attestation_request_emits_anonymous_struct_with_32_byte_nonce() {
let nonce = [0xAB_u8; 32];
let bytes = encode_attestation_request(&nonce);
assert_eq!(bytes[0], 0x15);
assert_eq!(*bytes.last().expect("non-empty"), 0x18);
assert_eq!(bytes[1], 0x30);
assert_eq!(bytes[2], 0x00);
assert_eq!(bytes[3], 0x20);
assert_eq!(&bytes[4..4 + 32], &nonce);
}
#[test]
fn attestation_response_round_trips() {
let elements = vec![0xDE, 0xAD, 0xBE, 0xEF];
let sig = [0x42_u8; 64];
let mut tlv = vec![0x15];
tlv.extend_from_slice(&[0x30, 0x00, 0x04]); tlv.extend_from_slice(&elements);
tlv.extend_from_slice(&[0x30, 0x01, 0x40]); tlv.extend_from_slice(&sig);
tlv.push(0x18);
let resp = decode_attestation_response(&tlv).expect("decode happy path");
assert_eq!(resp.attestation_elements, elements);
assert_eq!(resp.signature, sig);
}
#[test]
fn cert_chain_request_dac_matches_spec_bytes() {
let bytes = encode_certificate_chain_request(CertChainType::Dac);
assert_eq!(bytes, vec![0x15, 0x24, 0x00, 0x01, 0x18]);
}
#[test]
fn cert_chain_request_pai_matches_spec_bytes() {
let bytes = encode_certificate_chain_request(CertChainType::Pai);
assert_eq!(bytes, vec![0x15, 0x24, 0x00, 0x02, 0x18]);
}
#[test]
fn cert_chain_response_round_trips_der_payload() {
let tlv = vec![0x15, 0x30, 0x00, 0x03, 0xAA, 0xBB, 0xCC, 0x18];
let resp = decode_certificate_chain_response(&tlv).expect("decode happy path");
assert_eq!(resp.certificate, vec![0xAA, 0xBB, 0xCC]);
}
#[test]
fn encode_add_noc_emits_required_fields() {
let noc = b"noc-bytes".to_vec();
let ipk = [0x77u8; 16];
let bytes = encode_add_noc(&noc, None, &ipk, 0xDEAD_BEEF, 0xFFF1);
use matter_codec::{ContainerKind, Element, TlvReader, Value};
let mut r = TlvReader::new(&bytes);
assert!(matches!(
r.next().unwrap(),
Some(Element::ContainerStart {
tag: Tag::Anonymous,
kind: ContainerKind::Structure,
})
));
match r.next().unwrap() {
Some(Element::Scalar {
tag: Tag::Context(0),
value: Value::Bytes(b),
}) => assert_eq!(b, noc),
other => panic!("unexpected: {other:?}"),
}
match r.next().unwrap() {
Some(Element::Scalar {
tag: Tag::Context(2),
value: Value::Bytes(b),
}) => assert_eq!(b, ipk.to_vec()),
other => panic!("unexpected: {other:?}"),
}
}
}