use p256::SecretKey;
use ring::rand::{SecureRandom, SystemRandom};
use subtle::ConstantTimeEq;
use zeroize::Zeroizing;
use matter_cert::{CertificateChain, MatterCertificate, MatterTime, Signature, TrustedRoots};
use crate::case::messages::{Sigma1, Sigma2, Sigma2Resume, Sigma3};
use crate::case::sigma::{
aead_decrypt, aead_encrypt, compute_dest_id, compute_sigma2_resume_mic, decode_tbedata3,
derive_resume_session_keys, ecdh_shared_secret, encode_tbedata2, encode_tbs_data,
generate_ephemeral_keypair, hkdf_derive, transcript_hash, verify_sigma1_resume_mic,
AEAD_KEY_LEN, HKDF_INFO_SIGMA2, HKDF_INFO_SIGMA3, NONCE_TBE_DATA2, NONCE_TBE_DATA3,
};
use crate::case::{
CaseCredentials, CaseMessageKind, CaseSessionKeys, CaseSessionOutput, LocalInfo, PeerInfo,
ResumptionId, ResumptionRecord, Sigma1Outcome,
};
use crate::error::{Error, Result};
const HKDF_INFO_SESSION_KEYS: &[u8] = b"SessionKeys";
#[derive(Debug)]
enum State {
AwaitingSigma1 {
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
eph_secret: SecretKey,
eph_pub: [u8; 65],
responder_random: [u8; 32],
responder_session_id: u16,
},
ReadyToSendSigma2 {
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
sigma2_bytes: Vec<u8>,
sigma1_bytes: Vec<u8>,
shared_secret: Zeroizing<[u8; 32]>,
initiator_random: [u8; 32],
responder_random: [u8; 32],
initiator_eph_pub: [u8; 65],
eph_pub: [u8; 65],
initiator_session_id: u16,
responder_session_id: u16,
resumption_id: [u8; 16],
},
AwaitingSigma3 {
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
sigma1_bytes: Vec<u8>,
sigma2_bytes: Vec<u8>,
shared_secret: Zeroizing<[u8; 32]>,
#[allow(dead_code)]
initiator_random: [u8; 32],
#[allow(dead_code)]
responder_random: [u8; 32],
initiator_eph_pub: [u8; 65],
eph_pub: [u8; 65],
initiator_session_id: u16,
responder_session_id: u16,
resumption_id: [u8; 16],
},
AwaitingResumptionDecision {
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
eph_secret: SecretKey,
eph_pub: [u8; 65],
responder_random: [u8; 32],
responder_session_id: u16,
initiator_random: [u8; 32],
initiator_eph_pub: [u8; 65],
initiator_session_id: u16,
sigma1_bytes: Vec<u8>,
resumption_id_presented: [u8; 16],
initiator_resume_mic_received: [u8; 16],
},
ReadyToSendSigma2Resume {
sigma2_resume_bytes: Vec<u8>,
session_keys: CaseSessionKeys,
peer: PeerInfo,
local: LocalInfo,
resumption_record: Option<ResumptionRecord>,
},
Complete {
session_keys: CaseSessionKeys,
peer: PeerInfo,
local: LocalInfo,
resumption_record: Option<ResumptionRecord>,
},
Poisoned,
}
pub struct CaseResponder {
state: State,
validation_time: MatterTime,
new_resumption_id_override: Option<[u8; 16]>,
}
impl CaseResponder {
pub fn new(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
responder_session_id: u16,
now: MatterTime,
) -> Result<Self> {
let rng = SystemRandom::new();
Self::new_using_rng(credentials, trusted_roots, responder_session_id, now, &rng)
}
pub(crate) fn new_using_rng(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
responder_session_id: u16,
now: MatterTime,
rng: &dyn SecureRandom,
) -> Result<Self> {
let (eph_secret, eph_pub) = generate_ephemeral_keypair(rng)?;
let mut responder_random = [0u8; 32];
rng.fill(&mut responder_random)
.map_err(|_| Error::EphemeralKeyGenerationFailed)?;
Ok(Self {
state: State::AwaitingSigma1 {
credentials,
trusted_roots,
eph_secret,
eph_pub,
responder_random,
responder_session_id,
},
validation_time: now,
new_resumption_id_override: None,
})
}
pub(crate) fn new_with_eph_and_random(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
eph_private_key: [u8; 32],
responder_random: [u8; 32],
now: MatterTime,
) -> Result<Self> {
use p256::elliptic_curve::sec1::ToEncodedPoint;
use p256::NonZeroScalar;
let scalar_opt = NonZeroScalar::from_repr(eph_private_key.into());
let scalar =
Option::<NonZeroScalar>::from(scalar_opt).ok_or(Error::EphemeralKeyGenerationFailed)?;
let eph_secret = SecretKey::new(scalar.into());
let encoded = eph_secret.public_key().to_encoded_point(false);
let mut eph_pub = [0u8; 65];
eph_pub.copy_from_slice(encoded.as_bytes());
Ok(Self {
state: State::AwaitingSigma1 {
credentials,
trusted_roots,
eph_secret,
eph_pub,
responder_random,
responder_session_id: 0,
},
validation_time: now,
new_resumption_id_override: None,
})
}
pub(crate) fn set_new_resumption_id_override(&mut self, id: [u8; 16]) {
self.new_resumption_id_override = Some(id);
}
fn fresh_resumption_id(&self) -> Result<[u8; 16]> {
if let Some(id) = self.new_resumption_id_override {
return Ok(id);
}
let mut id = [0u8; 16];
SystemRandom::new()
.fill(&mut id)
.map_err(|_| Error::EphemeralKeyGenerationFailed)?;
Ok(id)
}
pub fn expected_inbound(&self) -> Option<CaseMessageKind> {
match &self.state {
State::AwaitingSigma1 { .. } => Some(CaseMessageKind::Sigma1),
State::AwaitingSigma3 { .. } => Some(CaseMessageKind::Sigma3),
_ => None,
}
}
#[allow(clippy::too_many_lines)]
pub fn handle_sigma1(&mut self, bytes: &[u8]) -> Result<Sigma1Outcome> {
let prev = std::mem::replace(&mut self.state, State::Poisoned);
match prev {
State::AwaitingSigma1 {
credentials,
trusted_roots,
eph_secret,
eph_pub,
responder_random,
responder_session_id,
} => {
let sigma1 = match Sigma1::decode(bytes) {
Ok(s) => s,
Err(e) => {
self.state = State::AwaitingSigma1 {
credentials,
trusted_roots,
eph_secret,
eph_pub,
responder_random,
responder_session_id,
};
return Err(e);
}
};
let expected_dest_id = compute_dest_id(
&credentials.ipk,
&credentials.rcac_public_key,
credentials.fabric_id,
credentials.node_id,
&sigma1.initiator_random,
);
let dest_id_matches: bool = expected_dest_id.ct_eq(&sigma1.dest_id).into();
if !dest_id_matches {
self.state = State::AwaitingSigma1 {
credentials,
trusted_roots,
eph_secret,
eph_pub,
responder_random,
responder_session_id,
};
return Err(Error::InvalidParameter);
}
let initiator_eph_pub = sigma1.initiator_eph_pub;
let initiator_random = sigma1.initiator_random;
let initiator_session_id = sigma1.initiator_session_id;
let sigma1_bytes = bytes.to_vec();
if let (Some(resumption_id), Some(resume_mic)) =
(sigma1.resumption_id, sigma1.initiator_resume_mic)
{
self.state = State::AwaitingResumptionDecision {
credentials,
trusted_roots,
eph_secret,
eph_pub,
responder_random,
responder_session_id,
initiator_random,
initiator_eph_pub,
initiator_session_id,
sigma1_bytes,
resumption_id_presented: resumption_id,
initiator_resume_mic_received: resume_mic,
};
return Ok(Sigma1Outcome::ResumptionRequested {
id: ResumptionId(resumption_id),
});
}
let (sigma2_bytes, shared_secret, resumption_id) =
match self.fresh_resumption_id().and_then(|rid| {
build_sigma2(
bytes,
&sigma1,
&credentials,
&eph_secret,
&eph_pub,
&responder_random,
responder_session_id,
&rid,
)
.map(|(bytes, secret)| (bytes, secret, rid))
}) {
Ok((bytes, secret, rid)) => (bytes, Zeroizing::new(secret), rid),
Err(e) => {
self.state = State::AwaitingSigma1 {
credentials,
trusted_roots,
eph_secret,
eph_pub,
responder_random,
responder_session_id,
};
return Err(e);
}
};
self.state = State::ReadyToSendSigma2 {
credentials,
trusted_roots,
sigma2_bytes,
sigma1_bytes,
shared_secret,
initiator_random,
responder_random,
initiator_eph_pub,
eph_pub,
initiator_session_id,
responder_session_id,
resumption_id,
};
Ok(Sigma1Outcome::NewSession)
}
other => {
self.state = other;
Err(Error::UnexpectedCaseMessage {
expected: CaseMessageKind::Sigma1,
got: CaseMessageKind::Sigma3,
})
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn accept_resumption(&mut self, record: ResumptionRecord) -> Result<()> {
let prev = std::mem::replace(&mut self.state, State::Poisoned);
match prev {
State::AwaitingResumptionDecision {
credentials,
trusted_roots: _, eph_secret: _, eph_pub: _, responder_random: _, responder_session_id,
initiator_random,
initiator_eph_pub: _,
initiator_session_id,
sigma1_bytes: _, resumption_id_presented,
initiator_resume_mic_received,
} => {
if record.id != ResumptionId(resumption_id_presented) {
return Err(Error::InvalidParameter);
}
verify_sigma1_resume_mic(
&record.shared_secret,
&initiator_random,
&resumption_id_presented,
&initiator_resume_mic_received,
)?;
let new_resumption_id = self.fresh_resumption_id()?;
let sigma2_mic = compute_sigma2_resume_mic(
&record.shared_secret,
&initiator_random,
&new_resumption_id,
)?;
let blob = derive_resume_session_keys(
&record.shared_secret,
&initiator_random,
&resumption_id_presented,
)?;
let mut i2r_key = [0u8; 16];
let mut r2i_key = [0u8; 16];
let mut attestation_challenge = [0u8; 16];
i2r_key.copy_from_slice(&blob[0..16]);
r2i_key.copy_from_slice(&blob[16..32]);
attestation_challenge.copy_from_slice(&blob[32..48]);
let session_keys = CaseSessionKeys {
i2r_key,
r2i_key,
attestation_challenge,
};
let sigma2_resume = Sigma2Resume {
resumption_id: new_resumption_id,
resume_mic: sigma2_mic,
responder_session_id,
responder_session_params: None,
};
let sigma2_resume_bytes = sigma2_resume.encode()?;
let peer = PeerInfo {
session_id: initiator_session_id,
..record.peer.clone()
};
let local = LocalInfo {
node_id: credentials.node_id,
fabric_id: credentials.fabric_id,
session_id: responder_session_id,
};
let next_record = ResumptionRecord {
id: ResumptionId(new_resumption_id),
shared_secret: record.shared_secret,
peer: record.peer.clone(),
expires_at: None, };
self.state = State::ReadyToSendSigma2Resume {
sigma2_resume_bytes,
session_keys,
peer,
local,
resumption_record: Some(next_record),
};
Ok(())
}
other => {
self.state = other;
Err(Error::UnexpectedCaseMessage {
expected: CaseMessageKind::Sigma1,
got: CaseMessageKind::Sigma1,
})
}
}
}
pub fn reject_resumption(&mut self) -> Result<()> {
let prev = std::mem::replace(&mut self.state, State::Poisoned);
match prev {
State::AwaitingResumptionDecision {
credentials,
trusted_roots,
eph_secret,
eph_pub,
responder_random,
responder_session_id,
initiator_random,
initiator_eph_pub,
initiator_session_id,
sigma1_bytes,
resumption_id_presented: _,
initiator_resume_mic_received: _,
} => {
let sigma1 = Sigma1::decode(&sigma1_bytes)?;
let resumption_id = self.fresh_resumption_id()?;
let (sigma2_bytes, shared_secret) = build_sigma2(
&sigma1_bytes,
&sigma1,
&credentials,
&eph_secret,
&eph_pub,
&responder_random,
responder_session_id,
&resumption_id,
)?;
let shared_secret = Zeroizing::new(shared_secret);
self.state = State::ReadyToSendSigma2 {
credentials,
trusted_roots,
sigma2_bytes,
sigma1_bytes,
shared_secret,
initiator_random,
responder_random,
initiator_eph_pub,
eph_pub,
initiator_session_id,
responder_session_id,
resumption_id,
};
Ok(())
}
other => {
self.state = other;
Err(Error::UnexpectedCaseMessage {
expected: CaseMessageKind::Sigma1,
got: CaseMessageKind::Sigma1,
})
}
}
}
pub fn next_message(&mut self) -> Result<Vec<u8>> {
let prev = std::mem::replace(&mut self.state, State::Poisoned);
match prev {
State::ReadyToSendSigma2 {
credentials,
trusted_roots,
sigma2_bytes,
sigma1_bytes,
shared_secret,
initiator_random,
responder_random,
initiator_eph_pub,
eph_pub,
initiator_session_id,
responder_session_id,
resumption_id,
} => {
self.state = State::AwaitingSigma3 {
credentials,
trusted_roots,
sigma1_bytes,
sigma2_bytes: sigma2_bytes.clone(),
shared_secret,
initiator_random,
responder_random,
initiator_eph_pub,
eph_pub,
initiator_session_id,
responder_session_id,
resumption_id,
};
Ok(sigma2_bytes)
}
State::ReadyToSendSigma2Resume {
sigma2_resume_bytes,
session_keys,
peer,
local,
resumption_record,
} => {
self.state = State::Complete {
session_keys,
peer,
local,
resumption_record,
};
Ok(sigma2_resume_bytes)
}
other => {
self.state = other;
Err(Error::UnexpectedCaseMessage {
expected: CaseMessageKind::Sigma2,
got: CaseMessageKind::Sigma1,
})
}
}
}
pub fn handle_sigma3(&mut self, bytes: &[u8]) -> Result<()> {
let now = self.validation_time;
let prev = std::mem::replace(&mut self.state, State::Poisoned);
match prev {
State::AwaitingSigma3 {
credentials,
trusted_roots,
sigma1_bytes,
sigma2_bytes,
shared_secret,
initiator_random: _,
responder_random: _,
initiator_eph_pub,
eph_pub,
initiator_session_id,
responder_session_id,
resumption_id,
} => {
let (session_keys, peer, local) = match process_sigma3(
bytes,
&credentials,
&trusted_roots,
&shared_secret,
&sigma1_bytes,
&sigma2_bytes,
&initiator_eph_pub,
&eph_pub,
initiator_session_id,
responder_session_id,
now,
) {
Ok(v) => v,
Err(e) => {
return Err(e);
}
};
let resumption_record = ResumptionRecord {
id: ResumptionId(resumption_id),
shared_secret: *shared_secret,
peer: peer.clone(),
expires_at: None,
};
self.state = State::Complete {
session_keys,
peer,
local,
resumption_record: Some(resumption_record),
};
Ok(())
}
other => {
self.state = other;
Err(Error::UnexpectedCaseMessage {
expected: CaseMessageKind::Sigma3,
got: CaseMessageKind::Sigma1,
})
}
}
}
pub fn finish(self) -> Result<CaseSessionOutput> {
match self.state {
State::Complete {
session_keys,
peer,
local,
resumption_record,
} => Ok(CaseSessionOutput {
keys: session_keys,
peer,
local,
resumption_record,
}),
_ => Err(Error::HandshakeIncomplete),
}
}
}
#[allow(clippy::too_many_arguments)]
fn build_sigma2(
sigma1_bytes: &[u8],
sigma1: &Sigma1,
credentials: &CaseCredentials,
eph_secret: &SecretKey,
eph_pub: &[u8; 65],
responder_random: &[u8; 32],
responder_session_id: u16,
resumption_id: &[u8; 16],
) -> Result<(Vec<u8>, [u8; 32])> {
let shared_secret = ecdh_shared_secret(eph_secret, &sigma1.initiator_eph_pub)?;
let h_sigma1 = transcript_hash(&[sigma1_bytes]);
let mut sigma2_salt: Vec<u8> = Vec::with_capacity(16 + 32 + 65 + 32);
sigma2_salt.extend_from_slice(&credentials.ipk);
sigma2_salt.extend_from_slice(responder_random);
sigma2_salt.extend_from_slice(eph_pub);
sigma2_salt.extend_from_slice(&h_sigma1);
let mut s2k = Zeroizing::new([0u8; AEAD_KEY_LEN]);
hkdf_derive(&shared_secret, &sigma2_salt, HKDF_INFO_SIGMA2, &mut *s2k)?;
let our_noc_tlv = credentials
.noc
.to_tlv()
.map_err(|_| Error::SigningFailed(crate::case::signer::SignerError::Internal))?;
let our_icac_tlv: Option<Vec<u8>> = match &credentials.icac {
Some(icac) => Some(
icac.to_tlv()
.map_err(|_| Error::SigningFailed(crate::case::signer::SignerError::Internal))?,
),
None => None,
};
let tbs_data2 = encode_tbs_data(
&our_noc_tlv,
our_icac_tlv.as_deref(),
eph_pub, &sigma1.initiator_eph_pub, )?;
let our_signature = credentials
.signer
.sign_p256_sha256(&tbs_data2)
.map_err(Error::SigningFailed)?;
let tbedata2_plaintext = encode_tbedata2(
&our_noc_tlv,
our_icac_tlv.as_deref(),
&our_signature,
resumption_id,
)?;
let encrypted2 = aead_encrypt(&s2k, NONCE_TBE_DATA2, b"", &tbedata2_plaintext)?;
let sigma2 = Sigma2 {
responder_random: *responder_random,
responder_session_id,
responder_eph_pub: *eph_pub,
encrypted: encrypted2,
responder_session_params: None,
};
let sigma2_bytes = sigma2.encode()?;
Ok((sigma2_bytes, shared_secret))
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_arguments)]
fn process_sigma3(
sigma3_bytes: &[u8],
credentials: &CaseCredentials,
trusted_roots: &TrustedRoots,
shared_secret: &[u8; 32],
sigma1_bytes: &[u8],
sigma2_bytes: &[u8],
initiator_eph_pub: &[u8; 65],
eph_pub: &[u8; 65],
initiator_session_id: u16,
responder_session_id: u16,
now: MatterTime,
) -> Result<(CaseSessionKeys, PeerInfo, LocalInfo)> {
let sigma3 = Sigma3::decode(sigma3_bytes)?;
let h_s1_s2 = transcript_hash(&[sigma1_bytes, sigma2_bytes]);
let mut sigma3_salt: Vec<u8> = Vec::with_capacity(16 + 32);
sigma3_salt.extend_from_slice(&credentials.ipk);
sigma3_salt.extend_from_slice(&h_s1_s2);
let mut s3k = Zeroizing::new([0u8; AEAD_KEY_LEN]);
hkdf_derive(shared_secret, &sigma3_salt, HKDF_INFO_SIGMA3, &mut *s3k)?;
let sigma3_decrypted = aead_decrypt(&s3k, NONCE_TBE_DATA3, b"", &sigma3.encrypted)?;
let mut peer_tbe = decode_tbedata3(&sigma3_decrypted)?;
let mut chain_certs: Vec<MatterCertificate> = match peer_tbe.peer_icac.take() {
Some(icac) => vec![peer_tbe.peer_noc, icac],
None => vec![peer_tbe.peer_noc],
};
CertificateChain::new(&chain_certs)
.validate(trusted_roots, now)
.map_err(Error::InvalidPeerNocChain)?;
let peer_noc = chain_certs.swap_remove(0);
let peer_dn = peer_noc.subject();
let peer_node_id = peer_dn
.node_id()
.ok_or(Error::PeerNodeIdMismatch(0, credentials.node_id))?;
let peer_fabric_id = peer_dn.fabric_id().ok_or(Error::FabricIdMismatch {
peer: 0,
local: credentials.fabric_id,
})?;
if peer_fabric_id != credentials.fabric_id {
return Err(Error::FabricIdMismatch {
peer: peer_fabric_id,
local: credentials.fabric_id,
});
}
let peer_signed_data = encode_tbs_data(
&peer_tbe.peer_noc_tlv,
peer_tbe.peer_icac_tlv.as_deref(),
initiator_eph_pub, eph_pub, )?;
let peer_sig =
Signature::from_slice(&peer_tbe.peer_signature).map_err(|_| Error::PeerSignatureInvalid)?;
peer_noc
.public_key()
.verify(&peer_signed_data, &peer_sig)
.map_err(|_| Error::PeerSignatureInvalid)?;
let h_all = transcript_hash(&[sigma1_bytes, sigma2_bytes, sigma3_bytes]);
let mut session_salt: Vec<u8> = Vec::with_capacity(16 + 32);
session_salt.extend_from_slice(&credentials.ipk);
session_salt.extend_from_slice(&h_all);
let mut keys_blob = Zeroizing::new([0u8; 48]);
hkdf_derive(
shared_secret,
&session_salt,
HKDF_INFO_SESSION_KEYS,
&mut *keys_blob,
)?;
let mut i2r_key = [0u8; 16];
let mut r2i_key = [0u8; 16];
let mut attestation_challenge = [0u8; 16];
i2r_key.copy_from_slice(&keys_blob[0..16]);
r2i_key.copy_from_slice(&keys_blob[16..32]);
attestation_challenge.copy_from_slice(&keys_blob[32..48]);
let session_keys = CaseSessionKeys {
i2r_key,
r2i_key,
attestation_challenge,
};
let peer = PeerInfo {
node_id: peer_node_id,
fabric_id: peer_fabric_id,
noc: peer_noc,
session_id: initiator_session_id,
};
let local = LocalInfo {
node_id: credentials.node_id,
fabric_id: credentials.fabric_id,
session_id: responder_session_id,
};
Ok((session_keys, peer, local))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use crate::case::signer::{CaseSigner, RingSigner};
use matter_cert::test_support::{build_unsigned, TestCertFields};
use matter_cert::{
BasicConstraints, DistinguishedName, DnAttribute, Extensions, MatterTime, TrustAnchor,
TrustedRoots,
};
fn make_test_cert(node_id: u64, fabric_id: u64) -> MatterCertificate {
let (signer, _) = RingSigner::generate().unwrap();
let pk_bytes = *signer.public_key().as_bytes();
let pub_key = matter_cert::PublicKey::new(pk_bytes).unwrap();
let subject = DistinguishedName::new(vec![
DnAttribute::FabricId(fabric_id),
DnAttribute::NodeId(node_id),
]);
let issuer = DistinguishedName::new(vec![DnAttribute::RcacId(1)]);
let extensions = Extensions::builder()
.basic_constraints(Some(BasicConstraints::new(false, None)))
.build();
build_unsigned(TestCertFields {
serial: vec![1],
issuer,
not_before: MatterTime::from_unix_secs(0),
not_after: MatterTime::NO_EXPIRY,
subject,
public_key: pub_key,
extensions,
signature: matter_cert::Signature::new([0u8; 64]),
})
}
fn make_test_credentials(
node_id: u64,
fabric_id: u64,
ipk: [u8; 16],
rcac_public_key: [u8; 65],
) -> CaseCredentials {
let (signer, _) = RingSigner::generate().unwrap();
let noc = make_test_cert(node_id, fabric_id);
CaseCredentials {
noc,
icac: None,
signer: Box::new(signer),
fabric_id,
node_id,
ipk,
rcac_public_key,
}
}
fn empty_roots() -> TrustedRoots {
TrustedRoots::new()
}
fn dummy_rcac_pub() -> [u8; 65] {
let mut k = [0u8; 65];
k[0] = 0x04;
k
}
#[test]
fn new_succeeds_with_valid_credentials() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let _responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
}
#[test]
fn expected_inbound_initially_is_sigma1() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
assert_eq!(responder.expected_inbound(), Some(CaseMessageKind::Sigma1));
}
#[test]
fn expected_inbound_after_next_message_is_sigma3() {
use crate::case::messages::Sigma1;
let ipk = [0xAB; 16];
let mut rcac_pub = [0u8; 65];
rcac_pub[0] = 0x04;
let creds = make_test_credentials(0x1234, 0x5678, ipk, rcac_pub);
let mut responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let initiator_random = [0x42u8; 32];
let dest_id = compute_dest_id(&ipk, &rcac_pub, 0x5678, 0x1234, &initiator_random);
let sigma1 = Sigma1 {
initiator_random,
initiator_session_id: 1,
dest_id,
initiator_eph_pub: {
let rng = ring::rand::SystemRandom::new();
let (_, pub_bytes) = generate_ephemeral_keypair(&rng).unwrap();
pub_bytes
},
initiator_session_params: None,
resumption_id: None,
initiator_resume_mic: None,
};
let sigma1_bytes = sigma1.encode().unwrap();
let outcome = responder.handle_sigma1(&sigma1_bytes).unwrap();
assert_eq!(outcome, Sigma1Outcome::NewSession);
let _ = responder.next_message().unwrap();
assert_eq!(responder.expected_inbound(), Some(CaseMessageKind::Sigma3));
}
#[test]
fn handle_sigma1_unknown_dest_id_returns_invalid_parameter() {
use crate::case::messages::Sigma1;
let ipk = [0xAB; 16];
let rcac_pub = dummy_rcac_pub();
let creds = make_test_credentials(0x1234, 0x5678, ipk, rcac_pub);
let mut responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let sigma1 = Sigma1 {
initiator_random: [0x11; 32],
initiator_session_id: 1,
dest_id: [0xFF; 32], initiator_eph_pub: {
let rng = ring::rand::SystemRandom::new();
let (_, pub_bytes) = generate_ephemeral_keypair(&rng).unwrap();
pub_bytes
},
initiator_session_params: None,
resumption_id: None,
initiator_resume_mic: None,
};
let sigma1_bytes = sigma1.encode().unwrap();
assert!(matches!(
responder.handle_sigma1(&sigma1_bytes),
Err(Error::InvalidParameter)
));
}
fn build_sigma1_for_responder(
ipk: &[u8; 16],
rcac_pub: &[u8; 65],
node_id: u64,
fabric_id: u64,
initiator_random: [u8; 32],
resumption_id: Option<[u8; 16]>,
resume_mic: Option<[u8; 16]>,
) -> Vec<u8> {
let dest_id = compute_dest_id(ipk, rcac_pub, fabric_id, node_id, &initiator_random);
let rng = ring::rand::SystemRandom::new();
let (_, eph_pub) = generate_ephemeral_keypair(&rng).unwrap();
let sigma1 = Sigma1 {
initiator_random,
initiator_session_id: 7,
dest_id,
initiator_eph_pub: eph_pub,
initiator_session_params: None,
resumption_id,
initiator_resume_mic: resume_mic,
};
sigma1.encode().unwrap()
}
#[test]
fn handle_sigma1_with_resumption_fields_returns_resumption_requested() {
let ipk = [0xAB; 16];
let rcac_pub = dummy_rcac_pub();
let creds = make_test_credentials(0x1234, 0x5678, ipk, rcac_pub);
let mut responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let initiator_random = [0x42u8; 32];
let resumption_id = [0xCC; 16];
let resume_mic = [0xDD; 16];
let sigma1_bytes = build_sigma1_for_responder(
&ipk,
&rcac_pub,
0x1234,
0x5678,
initiator_random,
Some(resumption_id),
Some(resume_mic),
);
let outcome = responder.handle_sigma1(&sigma1_bytes).unwrap();
assert_eq!(
outcome,
Sigma1Outcome::ResumptionRequested {
id: ResumptionId(resumption_id),
}
);
}
#[test]
fn handle_sigma1_with_only_resumption_id_takes_new_session_path() {
let ipk = [0xAB; 16];
let rcac_pub = dummy_rcac_pub();
let creds = make_test_credentials(0x1234, 0x5678, ipk, rcac_pub);
let mut responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let initiator_random = [0x42u8; 32];
let sigma1_bytes = build_sigma1_for_responder(
&ipk,
&rcac_pub,
0x1234,
0x5678,
initiator_random,
Some([0xCC; 16]), None, );
let outcome = responder.handle_sigma1(&sigma1_bytes).unwrap();
assert_eq!(outcome, Sigma1Outcome::NewSession);
}
#[test]
fn next_message_before_handle_sigma1_is_rejected() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let mut responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
assert!(matches!(
responder.next_message(),
Err(Error::UnexpectedCaseMessage { .. })
));
}
#[test]
fn handle_sigma3_before_sigma1_is_rejected() {
use crate::case::messages::Sigma3;
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let mut responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let dummy_sigma3 = Sigma3 {
encrypted: vec![0xAA; 80],
};
let bytes = dummy_sigma3.encode().unwrap();
assert!(matches!(
responder.handle_sigma3(&bytes),
Err(Error::UnexpectedCaseMessage { .. })
));
}
#[test]
fn finish_before_complete_returns_handshake_incomplete() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
assert!(matches!(
responder.finish(),
Err(Error::HandshakeIncomplete)
));
}
#[test]
fn trusted_roots_with_anchor_is_non_empty() {
let rcac = make_test_cert(0, 0x5678);
let anchor = TrustAnchor::from_root_cert(&rcac);
let mut roots = TrustedRoots::new();
roots.add(anchor);
assert!(!roots.is_empty());
assert_eq!(roots.len(), 1);
}
fn build_valid_resumption_setup(
ipk: &[u8; 16],
rcac_pub: &[u8; 65],
node_id: u64,
fabric_id: u64,
) -> (Vec<u8>, ResumptionRecord, [u8; 32]) {
use crate::case::sigma::compute_sigma1_resume_mic;
let shared_secret = [0x55u8; 32];
let resumption_id = [0xAA; 16];
let initiator_random = [0x11; 32];
let mic =
compute_sigma1_resume_mic(&shared_secret, &initiator_random, &resumption_id).unwrap();
let sigma1_bytes = build_sigma1_for_responder(
ipk,
rcac_pub,
node_id,
fabric_id,
initiator_random,
Some(resumption_id),
Some(mic),
);
let noc = make_test_cert(node_id + 1, fabric_id);
let peer = PeerInfo {
node_id: node_id + 1,
fabric_id,
noc,
session_id: 99,
};
let record = ResumptionRecord {
id: ResumptionId(resumption_id),
shared_secret,
peer,
expires_at: None,
};
(sigma1_bytes, record, initiator_random)
}
#[test]
fn accept_resumption_rejects_wrong_id() {
let ipk = [0xAB; 16];
let rcac_pub = dummy_rcac_pub();
let creds = make_test_credentials(0x1234, 0x5678, ipk, rcac_pub);
let mut responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let (sigma1_bytes, mut record, _) =
build_valid_resumption_setup(&ipk, &rcac_pub, 0x1234, 0x5678);
let outcome = responder.handle_sigma1(&sigma1_bytes).unwrap();
assert!(matches!(outcome, Sigma1Outcome::ResumptionRequested { .. }));
record.id = ResumptionId([0xFF; 16]);
assert!(matches!(
responder.accept_resumption(record),
Err(Error::InvalidParameter)
));
}
#[test]
fn accept_resumption_rejects_invalid_mic() {
let ipk = [0xAB; 16];
let rcac_pub = dummy_rcac_pub();
let creds = make_test_credentials(0x1234, 0x5678, ipk, rcac_pub);
let mut responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let (sigma1_bytes, mut record, _) =
build_valid_resumption_setup(&ipk, &rcac_pub, 0x1234, 0x5678);
let outcome = responder.handle_sigma1(&sigma1_bytes).unwrap();
assert!(matches!(outcome, Sigma1Outcome::ResumptionRequested { .. }));
record.shared_secret = [0xFF; 32];
assert!(matches!(
responder.accept_resumption(record),
Err(Error::ResumptionMacMismatch)
));
}
#[test]
fn reject_resumption_transitions_to_new_session_path() {
use crate::case::messages::Sigma2;
let ipk = [0xAB; 16];
let rcac_pub = dummy_rcac_pub();
let creds = make_test_credentials(0x1234, 0x5678, ipk, rcac_pub);
let mut responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let (sigma1_bytes, _record, _) =
build_valid_resumption_setup(&ipk, &rcac_pub, 0x1234, 0x5678);
let outcome = responder.handle_sigma1(&sigma1_bytes).unwrap();
assert!(matches!(outcome, Sigma1Outcome::ResumptionRequested { .. }));
responder.reject_resumption().unwrap();
let outbound = responder.next_message().unwrap();
assert!(
!outbound.is_empty(),
"next_message after reject_resumption must return Sigma2 bytes"
);
Sigma2::decode(&outbound).unwrap();
}
#[test]
fn accept_resumption_then_next_message_returns_sigma2_resume() {
use crate::case::messages::Sigma2Resume;
let ipk = [0xAB; 16];
let rcac_pub = dummy_rcac_pub();
let creds = make_test_credentials(0x1234, 0x5678, ipk, rcac_pub);
let mut responder = CaseResponder::new(
creds,
empty_roots(),
0x0002,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let (sigma1_bytes, record, _) =
build_valid_resumption_setup(&ipk, &rcac_pub, 0x1234, 0x5678);
let old_id = record.id;
let outcome = responder.handle_sigma1(&sigma1_bytes).unwrap();
assert!(matches!(outcome, Sigma1Outcome::ResumptionRequested { .. }));
responder.accept_resumption(record).unwrap();
let outbound = responder.next_message().unwrap();
assert!(!outbound.is_empty());
let sigma2_resume = Sigma2Resume::decode(&outbound).unwrap();
assert_ne!(
sigma2_resume.resumption_id, old_id.0,
"Sigma2_Resume must carry a fresh resumption_id"
);
let output = responder.finish().unwrap();
let next_record = output.resumption_record.unwrap();
assert_eq!(
next_record.id.0, sigma2_resume.resumption_id,
"CaseSessionOutput resumption_record.id must match Sigma2_Resume resumption_id"
);
}
}