#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::doc_markdown)] #![allow(clippy::struct_field_names)]
use std::fs;
use std::path::PathBuf;
use matter_cert::{MatterCertificate, MatterTime, TrustAnchor, TrustedRoots};
use matter_crypto::{
test_support::{
case_initiator_with_eph_key, case_initiator_with_resumption_eph_key,
case_responder_with_eph_key_and_resumption_id,
},
CaseCredentials, PeerInfo, ResumptionId, ResumptionRecord, RingSigner, Sigma1Outcome,
};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Fixture {
inputs: FixtureInputs,
messages: FixtureMessages,
}
#[derive(Debug, Deserialize)]
struct FixtureInputs {
fabric_id: u64,
initiator_node_id: u64,
responder_node_id: u64,
ipk: String,
rcac_noc: String,
rcac_public_key: String,
initiator_noc: String,
initiator_pkcs8: String,
responder_noc: String,
responder_pkcs8: String,
#[serde(default)]
icac_noc: Option<String>,
initiator_eph_priv: String,
initiator_random: String,
responder_eph_priv: String,
responder_random: String,
#[serde(default)]
resumption_id: Option<String>,
#[serde(default)]
resumption_shared_secret: Option<String>,
#[serde(default)]
new_resumption_id: Option<String>,
}
#[derive(Debug, Deserialize)]
struct FixtureMessages {
sigma1: String,
#[serde(default)]
sigma2: Option<String>,
#[serde(default)]
sigma2_resume: Option<String>,
#[serde(default)]
sigma3: Option<String>,
}
fn load_fixture(scenario: &str) -> Fixture {
let path = PathBuf::from("../../test-vectors/case").join(format!("{scenario}.json"));
let bytes = fs::read_to_string(&path).unwrap_or_else(|_| {
panic!(
"fixture {} not found — run `cargo xtask capture-case` to regenerate",
path.display()
)
});
serde_json::from_str(&bytes)
.unwrap_or_else(|e| panic!("malformed fixture {}: {}", path.display(), e))
}
fn hex_to_array<const N: usize>(s: &str) -> [u8; N] {
let bytes = hex::decode(s).expect("valid hex");
assert_eq!(bytes.len(), N, "expected {N} bytes, got {}", bytes.len());
bytes.try_into().unwrap()
}
fn build_credentials_from_fixture(
noc_hex: &str,
pkcs8_hex: &str,
icac_hex: Option<&str>,
fabric_id: u64,
node_id: u64,
ipk: [u8; 16],
rcac_public_key: [u8; 65],
) -> CaseCredentials {
let noc = MatterCertificate::from_tlv(&hex::decode(noc_hex).unwrap()).expect("parse NOC");
let signer = RingSigner::from_pkcs8(&hex::decode(pkcs8_hex).unwrap()).expect("load signer");
let icac = icac_hex
.map(|h| MatterCertificate::from_tlv(&hex::decode(h).unwrap()).expect("parse ICAC"));
CaseCredentials {
noc,
icac,
signer: Box::new(signer),
fabric_id,
node_id,
ipk,
rcac_public_key,
}
}
fn build_trusted_roots(rcac_noc_hex: &str) -> TrustedRoots {
let rcac =
MatterCertificate::from_tlv(&hex::decode(rcac_noc_hex).unwrap()).expect("parse RCAC");
let mut roots = TrustedRoots::new();
roots.add(TrustAnchor::from_root_cert(&rcac));
roots
}
#[test]
fn debug_tbs_data2_and_signature() {
use matter_codec::{Tag, TlvWriter};
use matter_crypto::CaseSigner;
use p256::elliptic_curve::sec1::ToEncodedPoint;
use p256::{NonZeroScalar, SecretKey};
let fx = load_fixture("handshake-new-session");
let noc_bytes = hex::decode(&fx.inputs.responder_noc).unwrap();
let cert = MatterCertificate::from_tlv(&noc_bytes).expect("parse responder NOC");
let re_encoded_noc = cert.to_tlv().expect("re-encode NOC");
let resp_eph_priv_bytes = hex_to_array::<32>(&fx.inputs.responder_eph_priv);
let init_eph_priv_bytes = hex_to_array::<32>(&fx.inputs.initiator_eph_priv);
let resp_eph_scalar = NonZeroScalar::from_repr(resp_eph_priv_bytes.into()).unwrap();
let resp_eph_sk = SecretKey::new(resp_eph_scalar.into());
let resp_eph_pub_encoded = resp_eph_sk.public_key().to_encoded_point(false);
let mut resp_eph_pub = [0u8; 65];
resp_eph_pub.copy_from_slice(resp_eph_pub_encoded.as_bytes());
let init_eph_scalar = NonZeroScalar::from_repr(init_eph_priv_bytes.into()).unwrap();
let init_eph_sk = SecretKey::new(init_eph_scalar.into());
let init_eph_pub_encoded = init_eph_sk.public_key().to_encoded_point(false);
let mut init_eph_pub = [0u8; 65];
init_eph_pub.copy_from_slice(init_eph_pub_encoded.as_bytes());
let icac_re_encoded: Option<Vec<u8>> = fx.inputs.icac_noc.as_deref().map(|h| {
let icac_bytes = hex::decode(h).unwrap();
let icac_cert = MatterCertificate::from_tlv(&icac_bytes).expect("parse ICAC");
icac_cert.to_tlv().expect("re-encode ICAC")
});
let mut tbs_data = Vec::new();
{
let mut w = TlvWriter::new(&mut tbs_data);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(1), &re_encoded_noc).unwrap();
if let Some(icac) = &icac_re_encoded {
w.put_bytes(Tag::Context(2), icac).unwrap();
}
w.put_bytes(Tag::Context(3), &resp_eph_pub).unwrap();
w.put_bytes(Tag::Context(4), &init_eph_pub).unwrap();
w.end_container().unwrap();
}
eprintln!("TBSData2 (Rust): {}", hex::encode(&tbs_data));
eprintln!("TBSData2 len (Rust): {}", tbs_data.len());
let signer =
matter_crypto::RingSigner::from_pkcs8(&hex::decode(&fx.inputs.responder_pkcs8).unwrap())
.expect("load signer");
let sig = signer.sign_p256_sha256(&tbs_data).expect("sign");
eprintln!("Signature (Rust/ring): {}", hex::encode(sig));
eprintln!(
"signer public_key: {}",
hex::encode(signer.public_key().as_bytes())
);
}
#[test]
fn debug_tbs_data3_and_signature() {
use matter_codec::{Tag, TlvWriter};
use matter_crypto::CaseSigner;
use p256::elliptic_curve::sec1::ToEncodedPoint;
use p256::{NonZeroScalar, SecretKey};
let fx = load_fixture("handshake-new-session");
let noc_bytes = hex::decode(&fx.inputs.initiator_noc).unwrap();
let cert = MatterCertificate::from_tlv(&noc_bytes).expect("parse initiator NOC");
let re_encoded_noc = cert.to_tlv().expect("re-encode initiator NOC");
eprintln!("initiator_noc raw: {}", hex::encode(&noc_bytes));
eprintln!("initiator_noc re-encoded: {}", hex::encode(&re_encoded_noc));
eprintln!("roundtrip matches: {}", noc_bytes == re_encoded_noc);
let resp_eph_priv_bytes = hex_to_array::<32>(&fx.inputs.responder_eph_priv);
let init_eph_priv_bytes = hex_to_array::<32>(&fx.inputs.initiator_eph_priv);
let resp_eph_scalar = NonZeroScalar::from_repr(resp_eph_priv_bytes.into()).unwrap();
let resp_eph_sk = SecretKey::new(resp_eph_scalar.into());
let resp_eph_pub_encoded = resp_eph_sk.public_key().to_encoded_point(false);
let mut resp_eph_pub = [0u8; 65];
resp_eph_pub.copy_from_slice(resp_eph_pub_encoded.as_bytes());
let init_eph_scalar = NonZeroScalar::from_repr(init_eph_priv_bytes.into()).unwrap();
let init_eph_sk = SecretKey::new(init_eph_scalar.into());
let init_eph_pub_encoded = init_eph_sk.public_key().to_encoded_point(false);
let mut init_eph_pub = [0u8; 65];
init_eph_pub.copy_from_slice(init_eph_pub_encoded.as_bytes());
let icac_re_encoded: Option<Vec<u8>> = fx.inputs.icac_noc.as_deref().map(|h| {
let icac_bytes = hex::decode(h).unwrap();
let icac_cert = MatterCertificate::from_tlv(&icac_bytes).expect("parse ICAC");
icac_cert.to_tlv().expect("re-encode ICAC")
});
let mut tbs_data = Vec::new();
{
let mut w = TlvWriter::new(&mut tbs_data);
w.start_structure(Tag::Anonymous).unwrap();
w.put_bytes(Tag::Context(1), &re_encoded_noc).unwrap();
if let Some(icac) = &icac_re_encoded {
w.put_bytes(Tag::Context(2), icac).unwrap();
}
w.put_bytes(Tag::Context(3), &init_eph_pub).unwrap(); w.put_bytes(Tag::Context(4), &resp_eph_pub).unwrap(); w.end_container().unwrap();
}
eprintln!("TBSData3 (Rust): {}", hex::encode(&tbs_data));
eprintln!("TBSData3 len (Rust): {}", tbs_data.len());
let signer =
matter_crypto::RingSigner::from_pkcs8(&hex::decode(&fx.inputs.initiator_pkcs8).unwrap())
.expect("load initiator signer");
let sig = signer.sign_p256_sha256(&tbs_data).expect("sign");
eprintln!("Sigma3 Signature (Rust): {}", hex::encode(sig));
eprintln!(
"initiator signer public_key: {}",
hex::encode(signer.public_key().as_bytes())
);
}
#[test]
fn fixture_noc_roundtrips_through_matter_cert() {
let fx = load_fixture("handshake-new-session");
for (label, noc_hex) in [
("responder_noc", &fx.inputs.responder_noc),
("initiator_noc", &fx.inputs.initiator_noc),
] {
let noc_bytes = hex::decode(noc_hex).unwrap();
let cert = MatterCertificate::from_tlv(&noc_bytes).expect("parse NOC");
let re_encoded = cert.to_tlv().expect("re-encode NOC");
assert_eq!(
noc_bytes, re_encoded,
"{label}: MatterCertificate::to_tlv must produce byte-identical output"
);
}
}
#[test]
fn matter_js_byte_parity_new_session() {
let fx = load_fixture("handshake-new-session");
let ipk = hex_to_array::<16>(&fx.inputs.ipk);
let rcac_pub = hex_to_array::<65>(&fx.inputs.rcac_public_key);
let roots = build_trusted_roots(&fx.inputs.rcac_noc);
let icac_hex = fx.inputs.icac_noc.as_deref();
let initiator_creds = build_credentials_from_fixture(
&fx.inputs.initiator_noc,
&fx.inputs.initiator_pkcs8,
icac_hex,
fx.inputs.fabric_id,
fx.inputs.initiator_node_id,
ipk,
rcac_pub,
);
let responder_creds = build_credentials_from_fixture(
&fx.inputs.responder_noc,
&fx.inputs.responder_pkcs8,
icac_hex,
fx.inputs.fabric_id,
fx.inputs.responder_node_id,
ipk,
rcac_pub,
);
let mut initiator = case_initiator_with_eph_key(
initiator_creds,
roots.clone(),
fx.inputs.responder_node_id,
fx.inputs.fabric_id,
hex_to_array::<32>(&fx.inputs.initiator_eph_priv),
hex_to_array::<32>(&fx.inputs.initiator_random),
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let mut responder = case_responder_with_eph_key_and_resumption_id(
responder_creds,
roots,
hex_to_array::<32>(&fx.inputs.responder_eph_priv),
hex_to_array::<32>(&fx.inputs.responder_random),
hex_to_array::<16>(
fx.inputs
.new_resumption_id
.as_deref()
.expect("fixture has new_resumption_id"),
),
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let our_sigma1 = initiator.start().unwrap();
assert_eq!(
hex::encode(&our_sigma1),
fx.messages.sigma1,
"Sigma1 byte parity"
);
let outcome = responder.handle_sigma1(&our_sigma1).unwrap();
assert!(
matches!(outcome, Sigma1Outcome::NewSession),
"expected NewSession outcome"
);
let our_sigma2 = responder.next_message().unwrap();
assert_eq!(
hex::encode(&our_sigma2),
fx.messages.sigma2.as_deref().expect("fixture has sigma2"),
"Sigma2 byte parity"
);
initiator.handle_sigma2(&our_sigma2).unwrap();
let our_sigma3 = initiator.next_message().unwrap();
assert_eq!(
hex::encode(&our_sigma3),
fx.messages.sigma3.as_deref().expect("fixture has sigma3"),
"Sigma3 byte parity"
);
responder.handle_sigma3(&our_sigma3).unwrap();
let init_out = initiator.finish().unwrap();
let resp_out = responder.finish().unwrap();
assert_eq!(
init_out.keys.i2r_key, resp_out.keys.i2r_key,
"i2r session keys must agree"
);
assert_eq!(
init_out.keys.r2i_key, resp_out.keys.r2i_key,
"r2i session keys must agree"
);
assert_eq!(init_out.peer.node_id, fx.inputs.responder_node_id);
assert_eq!(resp_out.peer.node_id, fx.inputs.initiator_node_id);
}
#[test]
#[allow(clippy::too_many_lines)] fn matter_js_byte_parity_resumption_accepted() {
let fx = load_fixture("handshake-resumption-accepted");
let ipk = hex_to_array::<16>(&fx.inputs.ipk);
let rcac_pub = hex_to_array::<65>(&fx.inputs.rcac_public_key);
let roots = build_trusted_roots(&fx.inputs.rcac_noc);
let resumption_id = ResumptionId(hex_to_array::<16>(
fx.inputs
.resumption_id
.as_deref()
.expect("fixture has resumption_id"),
));
let shared_secret = hex_to_array::<32>(
fx.inputs
.resumption_shared_secret
.as_deref()
.expect("fixture has resumption_shared_secret"),
);
let icac_hex = fx.inputs.icac_noc.as_deref();
let initiator_creds = build_credentials_from_fixture(
&fx.inputs.initiator_noc,
&fx.inputs.initiator_pkcs8,
icac_hex,
fx.inputs.fabric_id,
fx.inputs.initiator_node_id,
ipk,
rcac_pub,
);
let responder_creds = build_credentials_from_fixture(
&fx.inputs.responder_noc,
&fx.inputs.responder_pkcs8,
icac_hex,
fx.inputs.fabric_id,
fx.inputs.responder_node_id,
ipk,
rcac_pub,
);
let responder_noc_for_peer =
MatterCertificate::from_tlv(&hex::decode(&fx.inputs.responder_noc).unwrap()).unwrap();
let record = ResumptionRecord {
id: resumption_id,
shared_secret,
peer: PeerInfo {
node_id: fx.inputs.responder_node_id,
fabric_id: fx.inputs.fabric_id,
noc: responder_noc_for_peer,
session_id: 0,
},
expires_at: None,
};
let mut initiator = case_initiator_with_resumption_eph_key(
initiator_creds,
roots.clone(),
fx.inputs.responder_node_id,
fx.inputs.fabric_id,
record.clone(),
hex_to_array::<32>(&fx.inputs.initiator_eph_priv),
hex_to_array::<32>(&fx.inputs.initiator_random),
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let mut responder = case_responder_with_eph_key_and_resumption_id(
responder_creds,
roots,
hex_to_array::<32>(&fx.inputs.responder_eph_priv),
hex_to_array::<32>(&fx.inputs.responder_random),
[0u8; 16],
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let our_sigma1 = initiator.start().unwrap();
assert_eq!(
hex::encode(&our_sigma1),
fx.messages.sigma1,
"Sigma1 (resumption) byte parity"
);
let outcome = responder.handle_sigma1(&our_sigma1).unwrap();
let presented_id = match outcome {
Sigma1Outcome::ResumptionRequested { id } => id,
Sigma1Outcome::NewSession => panic!("expected ResumptionRequested"),
};
assert_eq!(
presented_id, resumption_id,
"presented resumption_id must match fixture"
);
responder.accept_resumption(record).unwrap();
let our_sigma2_resume = responder.next_message().unwrap();
assert_eq!(
hex::encode(&our_sigma2_resume),
fx.messages
.sigma2_resume
.as_deref()
.expect("fixture has sigma2_resume"),
"Sigma2_Resume byte parity"
);
initiator.handle_sigma2_resume(&our_sigma2_resume).unwrap();
let init_out = initiator.finish().unwrap();
let resp_out = responder.finish().unwrap();
assert_eq!(
init_out.keys.i2r_key, resp_out.keys.i2r_key,
"resumed i2r keys must agree"
);
assert_eq!(
init_out.keys.r2i_key, resp_out.keys.r2i_key,
"resumed r2i keys must agree"
);
}
#[test]
#[allow(clippy::too_many_lines)] fn matter_js_byte_parity_resumption_declined() {
let fx = load_fixture("handshake-resumption-declined");
let ipk = hex_to_array::<16>(&fx.inputs.ipk);
let rcac_pub = hex_to_array::<65>(&fx.inputs.rcac_public_key);
let roots = build_trusted_roots(&fx.inputs.rcac_noc);
let icac_hex = fx.inputs.icac_noc.as_deref();
let initiator_creds = build_credentials_from_fixture(
&fx.inputs.initiator_noc,
&fx.inputs.initiator_pkcs8,
icac_hex,
fx.inputs.fabric_id,
fx.inputs.initiator_node_id,
ipk,
rcac_pub,
);
let responder_creds = build_credentials_from_fixture(
&fx.inputs.responder_noc,
&fx.inputs.responder_pkcs8,
icac_hex,
fx.inputs.fabric_id,
fx.inputs.responder_node_id,
ipk,
rcac_pub,
);
let bogus_id = ResumptionId(hex_to_array::<16>(
fx.inputs
.resumption_id
.as_deref()
.expect("bogus resumption_id in fixture"),
));
let responder_noc_for_peer =
MatterCertificate::from_tlv(&hex::decode(&fx.inputs.responder_noc).unwrap()).unwrap();
let bogus_record = ResumptionRecord {
id: bogus_id,
shared_secret: hex_to_array::<32>(
fx.inputs
.resumption_shared_secret
.as_deref()
.expect("fixture has resumption_shared_secret"),
),
peer: PeerInfo {
node_id: fx.inputs.responder_node_id,
fabric_id: fx.inputs.fabric_id,
noc: responder_noc_for_peer,
session_id: 0,
},
expires_at: None,
};
let mut initiator = case_initiator_with_resumption_eph_key(
initiator_creds,
roots.clone(),
fx.inputs.responder_node_id,
fx.inputs.fabric_id,
bogus_record,
hex_to_array::<32>(&fx.inputs.initiator_eph_priv),
hex_to_array::<32>(&fx.inputs.initiator_random),
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let mut responder = case_responder_with_eph_key_and_resumption_id(
responder_creds,
roots,
hex_to_array::<32>(&fx.inputs.responder_eph_priv),
hex_to_array::<32>(&fx.inputs.responder_random),
hex_to_array::<16>(
fx.inputs
.new_resumption_id
.as_deref()
.expect("fixture has new_resumption_id"),
),
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let our_sigma1 = initiator.start().unwrap();
assert_eq!(
hex::encode(&our_sigma1),
fx.messages.sigma1,
"Sigma1 (declined resumption) byte parity"
);
responder.handle_sigma1(&our_sigma1).unwrap();
responder.reject_resumption().unwrap();
let our_sigma2 = responder.next_message().unwrap();
assert_eq!(
hex::encode(&our_sigma2),
fx.messages
.sigma2
.as_deref()
.expect("fixture has sigma2 (fallback path)"),
"Sigma2 (fallback) byte parity"
);
initiator.handle_sigma2(&our_sigma2).unwrap();
let our_sigma3 = initiator.next_message().unwrap();
assert_eq!(
hex::encode(&our_sigma3),
fx.messages
.sigma3
.as_deref()
.expect("fixture has sigma3 (fallback path)"),
"Sigma3 (fallback) byte parity"
);
responder.handle_sigma3(&our_sigma3).unwrap();
let init_out = initiator.finish().unwrap();
let resp_out = responder.finish().unwrap();
assert_eq!(
init_out.keys.i2r_key, resp_out.keys.i2r_key,
"i2r session keys must agree on declined-resumption path"
);
assert_eq!(
init_out.keys.r2i_key, resp_out.keys.r2i_key,
"r2i session keys must agree on declined-resumption path"
);
}