use p256::SecretKey;
use ring::rand::{SecureRandom, SystemRandom};
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_sigma1_resume_mic, decode_tbedata2,
derive_resume_session_keys, ecdh_shared_secret, encode_tbedata3, encode_tbs_data,
generate_ephemeral_keypair, hkdf_derive, transcript_hash, verify_sigma2_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,
};
use crate::error::{Error, Result};
const HKDF_INFO_SESSION_KEYS: &[u8] = b"SessionKeys";
#[derive(Debug)]
enum State {
AwaitingStart {
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
peer_node_id: u64,
peer_fabric_id: u64,
eph_secret: SecretKey,
eph_pub: [u8; 65],
initiator_random: [u8; 32],
initiator_session_id: u16,
resumption_record: Option<ResumptionRecord>,
},
AwaitingSigma2 {
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
peer_node_id: u64,
peer_fabric_id: u64,
eph_secret: SecretKey,
eph_pub: [u8; 65],
initiator_random: [u8; 32],
initiator_session_id: u16,
sigma1_bytes: Vec<u8>,
resumption_attempt: Option<ResumptionRecord>,
},
ReadyToSendSigma3 {
sigma3_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 CaseInitiator {
state: State,
validation_time: MatterTime,
}
impl CaseInitiator {
pub fn new(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
peer_node_id: u64,
peer_fabric_id: u64,
initiator_session_id: u16,
now: MatterTime,
) -> Result<Self> {
let rng = SystemRandom::new();
Self::new_inner(
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
initiator_session_id,
None,
now,
&rng,
)
}
#[allow(dead_code)]
pub(crate) fn new_using_rng(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
peer_node_id: u64,
peer_fabric_id: u64,
now: MatterTime,
rng: &dyn SecureRandom,
) -> Result<Self> {
Self::new_inner(
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
0,
None,
now,
rng,
)
}
pub fn new_with_resumption(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
peer_node_id: u64,
peer_fabric_id: u64,
record: ResumptionRecord,
initiator_session_id: u16,
now: MatterTime,
) -> Result<Self> {
let rng = SystemRandom::new();
Self::new_with_resumption_using_rng(
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
record,
initiator_session_id,
now,
&rng,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn new_with_resumption_using_rng(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
peer_node_id: u64,
peer_fabric_id: u64,
record: ResumptionRecord,
initiator_session_id: u16,
now: MatterTime,
rng: &dyn SecureRandom,
) -> Result<Self> {
Self::new_inner(
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
initiator_session_id,
Some(record),
now,
rng,
)
}
pub(crate) fn new_with_eph_and_random(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
peer_node_id: u64,
peer_fabric_id: u64,
eph_private_key: [u8; 32],
initiator_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::AwaitingStart {
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
eph_secret,
eph_pub,
initiator_random,
initiator_session_id: 0,
resumption_record: None,
},
validation_time: now,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn new_with_resumption_eph_and_random(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
peer_node_id: u64,
peer_fabric_id: u64,
record: ResumptionRecord,
eph_private_key: [u8; 32],
initiator_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::AwaitingStart {
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
eph_secret,
eph_pub,
initiator_random,
initiator_session_id: 0,
resumption_record: Some(record),
},
validation_time: now,
})
}
#[allow(clippy::too_many_arguments)]
fn new_inner(
credentials: CaseCredentials,
trusted_roots: TrustedRoots,
peer_node_id: u64,
peer_fabric_id: u64,
initiator_session_id: u16,
resumption_record: Option<ResumptionRecord>,
now: MatterTime,
rng: &dyn SecureRandom,
) -> Result<Self> {
let (eph_secret, eph_pub) = generate_ephemeral_keypair(rng)?;
let mut initiator_random = [0u8; 32];
rng.fill(&mut initiator_random)
.map_err(|_| Error::EphemeralKeyGenerationFailed)?;
Ok(Self {
state: State::AwaitingStart {
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
eph_secret,
eph_pub,
initiator_random,
initiator_session_id,
resumption_record,
},
validation_time: now,
})
}
pub fn expected_inbound(&self) -> Option<CaseMessageKind> {
match &self.state {
State::AwaitingSigma2 {
resumption_attempt: Some(_),
..
} => Some(CaseMessageKind::Sigma2Resume),
State::AwaitingSigma2 {
resumption_attempt: None,
..
} => Some(CaseMessageKind::Sigma2),
_ => None,
}
}
pub fn start(&mut self) -> Result<Vec<u8>> {
let prev = std::mem::replace(&mut self.state, State::Poisoned);
match prev {
State::AwaitingStart {
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
eph_secret,
eph_pub,
initiator_random,
initiator_session_id,
resumption_record,
} => {
let dest_id = compute_dest_id(
&credentials.ipk,
&credentials.rcac_public_key,
credentials.fabric_id,
peer_node_id,
&initiator_random,
);
let (resumption_id_field, initiator_resume_mic_field) = match &resumption_record {
Some(record) => {
let mic = compute_sigma1_resume_mic(
&record.shared_secret,
&initiator_random,
&record.id.0,
)?;
(Some(record.id.0), Some(mic))
}
None => (None, None),
};
let sigma1 = Sigma1 {
initiator_random,
initiator_session_id,
dest_id,
initiator_eph_pub: eph_pub,
initiator_session_params: None,
resumption_id: resumption_id_field,
initiator_resume_mic: initiator_resume_mic_field,
};
let sigma1_bytes = sigma1.encode()?;
self.state = State::AwaitingSigma2 {
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
eph_secret,
eph_pub,
initiator_random,
initiator_session_id,
sigma1_bytes: sigma1_bytes.clone(),
resumption_attempt: resumption_record,
};
Ok(sigma1_bytes)
}
other => {
self.state = other;
Err(Error::UnexpectedCaseMessage {
expected: CaseMessageKind::Sigma1,
got: CaseMessageKind::Sigma2,
})
}
}
}
pub fn handle_sigma2(&mut self, bytes: &[u8]) -> Result<()> {
let now = self.validation_time;
let prev = std::mem::replace(&mut self.state, State::Poisoned);
match prev {
State::AwaitingSigma2 {
credentials,
trusted_roots,
peer_node_id,
peer_fabric_id,
eph_secret,
eph_pub,
initiator_random: _,
initiator_session_id,
sigma1_bytes,
resumption_attempt: _, } => {
let (sigma3_bytes, session_keys, peer, local, resumption_record) = process_sigma2(
bytes,
&credentials,
&trusted_roots,
peer_node_id,
peer_fabric_id,
&eph_secret,
&eph_pub,
initiator_session_id,
&sigma1_bytes,
now,
)?;
self.state = State::ReadyToSendSigma3 {
sigma3_bytes,
session_keys,
peer,
local,
resumption_record: Some(resumption_record),
};
Ok(())
}
other => {
self.state = other;
Err(Error::UnexpectedCaseMessage {
expected: CaseMessageKind::Sigma2,
got: CaseMessageKind::Sigma3,
})
}
}
}
pub fn handle_sigma2_resume(&mut self, bytes: &[u8]) -> Result<()> {
let prev = std::mem::replace(&mut self.state, State::Poisoned);
match prev {
State::AwaitingSigma2 {
credentials,
initiator_random,
initiator_session_id,
resumption_attempt: Some(record),
trusted_roots: _,
peer_node_id: _,
peer_fabric_id: _,
eph_secret: _,
eph_pub: _,
sigma1_bytes: _,
} => {
let sigma2_resume = Sigma2Resume::decode(bytes)?;
let new_resumption_id = sigma2_resume.resumption_id;
verify_sigma2_resume_mic(
&record.shared_secret,
&initiator_random,
&new_resumption_id,
&sigma2_resume.resume_mic,
)?;
let blob = derive_resume_session_keys(
&record.shared_secret,
&initiator_random,
&record.id.0,
)?;
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 next_record = ResumptionRecord {
id: ResumptionId(new_resumption_id),
shared_secret: record.shared_secret, peer: record.peer.clone(),
expires_at: None, };
let peer = PeerInfo {
session_id: sigma2_resume.responder_session_id,
..record.peer.clone()
};
let local = LocalInfo {
node_id: credentials.node_id,
fabric_id: credentials.fabric_id,
session_id: initiator_session_id,
};
self.state = State::Complete {
session_keys,
peer,
local,
resumption_record: Some(next_record),
};
Ok(())
}
State::AwaitingSigma2 {
resumption_attempt: None,
..
} => Err(Error::UnexpectedCaseMessage {
expected: CaseMessageKind::Sigma2,
got: CaseMessageKind::Sigma2Resume,
}),
other => {
self.state = other;
Err(Error::UnexpectedCaseMessage {
expected: CaseMessageKind::Sigma2Resume,
got: CaseMessageKind::Sigma2Resume,
})
}
}
}
pub fn next_message(&mut self) -> Result<Vec<u8>> {
let prev = std::mem::replace(&mut self.state, State::Poisoned);
match prev {
State::ReadyToSendSigma3 {
sigma3_bytes,
session_keys,
peer,
local,
resumption_record,
} => {
self.state = State::Complete {
session_keys,
peer,
local,
resumption_record,
};
Ok(sigma3_bytes)
}
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_lines)]
#[allow(clippy::too_many_arguments)]
fn process_sigma2(
sigma2_bytes: &[u8],
credentials: &CaseCredentials,
trusted_roots: &TrustedRoots,
peer_node_id: u64,
peer_fabric_id: u64,
eph_secret: &SecretKey,
eph_pub: &[u8; 65],
initiator_session_id: u16,
sigma1_bytes: &[u8],
now: MatterTime,
) -> Result<(
Vec<u8>,
CaseSessionKeys,
PeerInfo,
LocalInfo,
ResumptionRecord,
)> {
let sigma2 = Sigma2::decode(sigma2_bytes)?;
let shared_secret = Zeroizing::new(ecdh_shared_secret(eph_secret, &sigma2.responder_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(&sigma2.responder_random);
sigma2_salt.extend_from_slice(&sigma2.responder_eph_pub);
sigma2_salt.extend_from_slice(&h_sigma1);
let mut s2k = Zeroizing::new([0u8; AEAD_KEY_LEN]);
hkdf_derive(
shared_secret.as_slice(),
&sigma2_salt,
HKDF_INFO_SIGMA2,
&mut *s2k,
)?;
let sigma2_decrypted = aead_decrypt(&s2k, NONCE_TBE_DATA2, b"", &sigma2.encrypted)?;
let mut peer_tbe = decode_tbedata2(&sigma2_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 verified_node_id = peer_dn
.node_id()
.ok_or(Error::PeerNodeIdMismatch(0, peer_node_id))?;
let verified_fabric_id = peer_dn.fabric_id().ok_or(Error::FabricIdMismatch {
peer: 0,
local: peer_fabric_id,
})?;
if verified_node_id != peer_node_id {
return Err(Error::PeerNodeIdMismatch(verified_node_id, peer_node_id));
}
if verified_fabric_id != peer_fabric_id {
return Err(Error::FabricIdMismatch {
peer: verified_fabric_id,
local: peer_fabric_id,
});
}
let peer_signed_data = encode_tbs_data(
&peer_tbe.peer_noc_tlv,
peer_tbe.peer_icac_tlv.as_deref(),
&sigma2.responder_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 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 our_signed_data = encode_tbs_data(
&our_noc_tlv,
our_icac_tlv.as_deref(),
eph_pub, &sigma2.responder_eph_pub, )?;
let our_signature = credentials
.signer
.sign_p256_sha256(&our_signed_data)
.map_err(Error::SigningFailed)?;
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.as_slice(),
&sigma3_salt,
HKDF_INFO_SIGMA3,
&mut *s3k,
)?;
let sigma3_plaintext = encode_tbedata3(&our_noc_tlv, our_icac_tlv.as_deref(), &our_signature)?;
let encrypted3 = aead_encrypt(&s3k, NONCE_TBE_DATA3, b"", &sigma3_plaintext)?;
let sigma3_bytes = Sigma3 {
encrypted: encrypted3,
}
.encode()?;
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.as_slice(),
&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: verified_node_id,
fabric_id: verified_fabric_id,
noc: peer_noc,
session_id: sigma2.responder_session_id,
};
let local = LocalInfo {
node_id: credentials.node_id,
fabric_id: credentials.fabric_id,
session_id: initiator_session_id,
};
let resumption_record = ResumptionRecord {
id: ResumptionId(peer_tbe.resumption_id),
shared_secret: *shared_secret,
peer: peer.clone(),
expires_at: None,
};
Ok((sigma3_bytes, session_keys, peer, local, resumption_record))
}
#[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 _initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
}
#[test]
fn start_returns_sigma1_bytes() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let mut initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let bytes = initiator.start().unwrap();
assert!(!bytes.is_empty(), "Sigma1 bytes must be non-empty");
assert_eq!(bytes[0], 0x15, "anonymous structure must start with 0x15");
}
#[test]
fn expected_inbound_after_start_is_sigma2() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let mut initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let _ = initiator.start().unwrap();
assert_eq!(initiator.expected_inbound(), Some(CaseMessageKind::Sigma2));
}
#[test]
fn start_produces_valid_sigma1() {
use crate::case::messages::Sigma1;
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let mut initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let bytes = initiator.start().unwrap();
let decoded = Sigma1::decode(&bytes).unwrap();
assert_eq!(decoded.dest_id.len(), 32);
assert_eq!(
decoded.initiator_eph_pub[0], 0x04,
"ephemeral pub key must be SEC1 uncompressed"
);
}
#[test]
fn finish_before_complete_returns_handshake_incomplete() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
assert!(matches!(
initiator.finish(),
Err(Error::HandshakeIncomplete)
));
}
#[test]
fn finish_after_start_returns_handshake_incomplete() {
let creds2 = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let fresh = CaseInitiator::new(
creds2,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
assert!(matches!(fresh.finish(), Err(Error::HandshakeIncomplete)));
}
#[test]
fn handle_sigma2_before_start_is_rejected() {
use crate::case::messages::Sigma2;
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let mut initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let dummy_sigma2 = Sigma2 {
responder_random: [0u8; 32],
responder_session_id: 1,
responder_eph_pub: [0x04; 65],
encrypted: vec![0xAA; 80],
responder_session_params: None,
};
let bytes = dummy_sigma2.encode().unwrap();
assert!(matches!(
initiator.handle_sigma2(&bytes),
Err(Error::UnexpectedCaseMessage { .. })
));
}
#[test]
fn next_message_before_handle_sigma2_is_rejected() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let mut initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let _ = initiator.start().unwrap();
assert!(matches!(
initiator.next_message(),
Err(Error::UnexpectedCaseMessage { .. })
));
}
#[test]
fn double_start_is_rejected() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let mut initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let _ = initiator.start().unwrap();
assert!(matches!(
initiator.start(),
Err(Error::UnexpectedCaseMessage { .. })
));
}
#[test]
fn expected_inbound_before_start_is_none() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
assert_eq!(initiator.expected_inbound(), None);
}
#[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 make_test_peer_info(node_id: u64, fabric_id: u64) -> PeerInfo {
PeerInfo {
node_id,
fabric_id,
noc: make_test_cert(node_id, fabric_id),
session_id: 1,
}
}
fn make_resumption_record(
secret: [u8; 32],
id: [u8; 16],
node_id: u64,
fabric_id: u64,
) -> ResumptionRecord {
ResumptionRecord {
id: ResumptionId(id),
shared_secret: secret,
peer: make_test_peer_info(node_id, fabric_id),
expires_at: None,
}
}
#[test]
fn new_with_resumption_succeeds() {
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let record = make_resumption_record([0x01u8; 32], [0x02u8; 16], 0x1234, 0x5678);
let _initiator = CaseInitiator::new_with_resumption(
creds,
empty_roots(),
0x1234,
0x5678,
record,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
}
#[test]
fn start_with_resumption_populates_sigma1_resume_fields() {
use crate::case::messages::Sigma1;
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let record = make_resumption_record([0x01u8; 32], [0x02u8; 16], 0x1234, 0x5678);
let mut initiator = CaseInitiator::new_with_resumption(
creds,
empty_roots(),
0x1234,
0x5678,
record,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let bytes = initiator.start().unwrap();
let decoded = Sigma1::decode(&bytes).unwrap();
assert!(
decoded.resumption_id.is_some(),
"Sigma1 must carry resumption_id when constructed with a record"
);
assert_eq!(
decoded.resumption_id.unwrap(),
[0x02u8; 16],
"resumption_id in Sigma1 must match the record's id"
);
assert!(
decoded.initiator_resume_mic.is_some(),
"Sigma1 must carry initiator_resume_mic when constructed with a record"
);
assert_eq!(
initiator.expected_inbound(),
Some(CaseMessageKind::Sigma2Resume)
);
}
#[test]
fn start_without_resumption_omits_sigma1_resume_fields() {
use crate::case::messages::Sigma1;
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let mut initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let bytes = initiator.start().unwrap();
let decoded = Sigma1::decode(&bytes).unwrap();
assert!(
decoded.resumption_id.is_none(),
"Sigma1 must NOT carry resumption_id on the new-session path"
);
assert!(
decoded.initiator_resume_mic.is_none(),
"Sigma1 must NOT carry initiator_resume_mic on the new-session path"
);
assert_eq!(initiator.expected_inbound(), Some(CaseMessageKind::Sigma2));
}
#[test]
fn handle_sigma2_resume_after_resumption_attempt_succeeds_with_valid_mic() {
use crate::case::messages::Sigma2Resume;
use crate::case::sigma::compute_sigma2_resume_mic;
use ring::rand::SystemRandom;
let shared_secret = [0x42u8; 32];
let old_id = [0x11u8; 16];
let new_id = [0x22u8; 16];
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let record = make_resumption_record(shared_secret, old_id, 0x1234, 0x5678);
let rng = SystemRandom::new();
let mut initiator = CaseInitiator::new_with_resumption_using_rng(
creds,
empty_roots(),
0x1234,
0x5678,
record,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
&rng,
)
.unwrap();
let sigma1_bytes = initiator.start().unwrap();
let sigma1 = crate::case::messages::Sigma1::decode(&sigma1_bytes).unwrap();
let initiator_random = sigma1.initiator_random;
let mic = compute_sigma2_resume_mic(&shared_secret, &initiator_random, &new_id).unwrap();
let sigma2_resume = Sigma2Resume {
resumption_id: new_id,
resume_mic: mic,
responder_session_id: 0xBEEF,
responder_session_params: None,
};
let sigma2_resume_bytes = sigma2_resume.encode().unwrap();
initiator
.handle_sigma2_resume(&sigma2_resume_bytes)
.unwrap();
let output = initiator.finish().unwrap();
assert!(
output.resumption_record.is_some(),
"output must carry a resumption record after successful resumption"
);
assert_eq!(
output.resumption_record.as_ref().unwrap().id.0,
new_id,
"resumption record id must be the NEW id from Sigma2_Resume"
);
assert_ne!(
output.keys.r2i_key, output.keys.i2r_key,
"r2i and i2r keys must differ"
);
}
#[test]
fn handle_sigma2_resume_rejects_invalid_mic() {
use crate::case::messages::Sigma2Resume;
use crate::case::sigma::compute_sigma2_resume_mic;
use ring::rand::SystemRandom;
let shared_secret = [0x42u8; 32];
let old_id = [0x11u8; 16];
let new_id = [0x22u8; 16];
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let record = make_resumption_record(shared_secret, old_id, 0x1234, 0x5678);
let rng = SystemRandom::new();
let mut initiator = CaseInitiator::new_with_resumption_using_rng(
creds,
empty_roots(),
0x1234,
0x5678,
record,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
&rng,
)
.unwrap();
let sigma1_bytes = initiator.start().unwrap();
let sigma1 = crate::case::messages::Sigma1::decode(&sigma1_bytes).unwrap();
let initiator_random = sigma1.initiator_random;
let mut mic =
compute_sigma2_resume_mic(&shared_secret, &initiator_random, &new_id).unwrap();
mic[0] ^= 0xFF;
let sigma2_resume = Sigma2Resume {
resumption_id: new_id,
resume_mic: mic,
responder_session_id: 0xBEEF,
responder_session_params: None,
};
let sigma2_resume_bytes = sigma2_resume.encode().unwrap();
assert!(
matches!(
initiator.handle_sigma2_resume(&sigma2_resume_bytes),
Err(Error::ResumptionMacMismatch)
),
"Corrupted MIC must be rejected with ResumptionMacMismatch"
);
}
#[test]
fn handle_sigma2_resume_without_resumption_attempt_returns_unexpected_message() {
use crate::case::messages::Sigma2Resume;
let creds = make_test_credentials(0x1234, 0x5678, [0xAB; 16], dummy_rcac_pub());
let mut initiator = CaseInitiator::new(
creds,
empty_roots(),
0x1234,
0x5678,
0x0001,
MatterTime::from_unix_secs(2_000_000_000),
)
.unwrap();
let _ = initiator.start().unwrap();
let sigma2_resume = Sigma2Resume {
resumption_id: [0xAAu8; 16],
resume_mic: [0xBBu8; 16],
responder_session_id: 1,
responder_session_params: None,
};
let bytes = sigma2_resume.encode().unwrap();
assert!(
matches!(
initiator.handle_sigma2_resume(&bytes),
Err(Error::UnexpectedCaseMessage { .. })
),
"Receiving Sigma2_Resume without having attempted resumption must fail"
);
}
}