use zrtp::*;
fn sample_hello() -> Hello {
Hello {
version: VERSION_1_10,
client_id: *b"rust-zrtp-demo ",
hash_image_h3: [0x11; 32],
zid: [0x22; 12],
signature_capable: true,
mitm_capable: false,
passive_capable: true,
hashes: vec![algos::HASH_S256],
ciphers: vec![algos::CIPHER_AES1, algos::CIPHER_AES3],
auth_tags: vec![algos::AUTH_HS32],
key_agreements: vec![algos::KEYAGREE_DH3K, algos::KEYAGREE_MULT],
sas_types: vec![algos::SAS_B32],
mac: [0x33; 8],
}
}
#[test]
fn hello_roundtrip() {
let hello = sample_hello();
assert_eq!(
Message::decode(&Message::Hello(hello.clone()).encode()).unwrap(),
Message::Hello(hello)
);
}
#[test]
fn hello_validation_rejects_too_many_algorithms() {
let mut hello = sample_hello();
hello.hashes = vec![algos::HASH_S256; 8];
assert!(hello.validate().is_err());
}
#[test]
fn malformed_packet_bad_crc_is_rejected() {
let packet = Packet {
sequence: 1,
ssrc: 2,
message: Message::Hello(sample_hello()),
};
let mut bytes = packet.encode();
let last = bytes.len() - 1;
bytes[last] ^= 0x01;
assert!(matches!(
Packet::decode(&bytes),
Err(Error::InvalidCrc { .. })
));
}
#[test]
fn malformed_packet_too_short_is_rejected() {
assert!(matches!(
Packet::decode(&[0u8; 15]),
Err(Error::PacketTooShort)
));
}
#[test]
fn malformed_packet_bad_preamble_is_rejected() {
let mut packet = Packet {
sequence: 1,
ssrc: 2,
message: Message::Hello(sample_hello()),
}
.encode();
packet[0] = 0x00;
packet[1] = 0x00;
assert!(matches!(
Packet::decode(&packet),
Err(Error::InvalidPreamble(_))
));
}
#[test]
fn malformed_message_bad_length_is_rejected() {
let mut bytes = Message::Hello(sample_hello()).encode();
bytes[2] = 0;
bytes[3] = 0;
assert!(matches!(Message::decode(&bytes), Err(Error::InvalidLength)));
}
#[test]
fn malformed_message_non_word_aligned_is_rejected() {
let mut bytes = Message::Hello(sample_hello()).encode();
bytes.pop();
assert!(matches!(
Message::decode(&bytes),
Err(Error::NonWordAligned)
));
}
#[test]
fn hello_decode_rejects_truncated_body() {
let mut hello = sample_hello().encode();
hello.truncate(84);
assert!(matches!(
Hello::decode(&hello[12..]),
Err(Error::Truncated("Hello"))
));
}
#[test]
fn unknown_message_roundtrip_preserves_body() {
let raw = RawMessage {
message_type: MessageType(*b"TestMsg!"),
body: vec![0xaa, 0xbb, 0xcc, 0xdd],
};
let encoded = raw.encode();
let decoded = Message::decode(&encoded).unwrap();
assert_eq!(decoded, Message::Unknown(raw));
}
#[test]
fn commit_roundtrip() {
let commit = Commit {
hash_image_h2: [0x44; 32],
zid: [0x55; 12],
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_DH3K,
sas: algos::SAS_B32,
nonce: CommitNonce::Hvi([0x66; 32]),
mac: [0x77; 8],
};
assert_eq!(
Message::decode(&Message::Commit(commit.clone()).encode()).unwrap(),
Message::Commit(commit)
);
}
#[test]
fn commit_decode_rejects_truncated_body() {
let mut commit = Commit {
hash_image_h2: [0x44; 32],
zid: [0x55; 12],
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_DH3K,
sas: algos::SAS_B32,
nonce: CommitNonce::Hvi([0x66; 32]),
mac: [0x77; 8],
}
.encode();
commit.truncate(84);
assert!(matches!(
Commit::decode(&commit[12..]),
Err(Error::Truncated("Commit"))
));
}
#[test]
fn hello_decode_rejects_invalid_version() {
let mut encoded = Message::Hello(sample_hello()).encode();
encoded[12..16].copy_from_slice(b"0.99");
assert!(matches!(
Hello::decode(&encoded[12..]).and_then(|hello| hello.validate()),
Err(Error::InvalidField("version"))
));
}
#[test]
fn commit_validation_rejects_wrong_multistream_nonce_shape() {
let commit = Commit {
hash_image_h2: [0x44; 32],
zid: [0x55; 12],
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_MULT,
sas: algos::SAS_B32,
nonce: CommitNonce::Hvi([0x66; 32]),
mac: [0x77; 8],
};
assert!(commit.validate().is_err());
}
#[test]
fn preshared_commit_validation_rejects_wrong_nonce_shape() {
let commit = Commit {
hash_image_h2: [0x44; 32],
zid: [0x55; 12],
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_PRSH,
sas: algos::SAS_B32,
nonce: CommitNonce::MultistreamNonce([0x66; 16]),
mac: [0x77; 8],
};
assert!(commit.validate().is_err());
}
#[test]
fn confirm_content_roundtrip() {
let content = ConfirmContent {
h0: [1; 32],
signature_length_words: 2,
flags: ConfirmFlags {
disclosure: true,
allow_clear: false,
sas_verified: true,
pbx_enrollment: false,
},
cache_expiration_interval: 99,
signature_type: Some(SignatureType::OpenPgp),
signature: vec![0xaa; 4],
};
let bytes = content.encode_plaintext().unwrap();
assert_eq!(ConfirmContent::decode_plaintext(&bytes).unwrap(), content);
}
#[test]
fn confirm_plaintext_rejects_bad_length() {
assert!(matches!(
ConfirmContent::decode_plaintext(&[0u8; 39]),
Err(Error::InvalidLength)
));
}
#[test]
fn sasrelay_content_roundtrip() {
let content = SasRelayContent {
h0: [2; 32],
trusted_sas: [3; 4],
signature_length_words: 2,
flags: ConfirmFlags::default(),
rendering_scheme: SasType::B32,
sashash: [4; 32],
signature_type: Some(SignatureType::OpenPgp),
signature: vec![9; 4],
};
let bytes = content.encode_plaintext().unwrap();
assert_eq!(SasRelayContent::decode_plaintext(&bytes).unwrap(), content);
}
#[test]
fn sasrelay_plaintext_rejects_bad_length() {
assert!(matches!(
SasRelayContent::decode_plaintext(&[0u8; 75]),
Err(Error::InvalidLength)
));
}
#[test]
fn packet_roundtrip() {
let packet = Packet {
sequence: 7,
ssrc: 0x11223344,
message: Message::Hello(sample_hello()),
};
assert_eq!(Packet::decode(&packet.encode()).unwrap(), packet);
}
#[test]
fn packet_decode_rejects_bad_magic_cookie() {
let mut packet = Packet {
sequence: 7,
ssrc: 0x11223344,
message: Message::Hello(sample_hello()),
}
.encode();
packet[4..8].copy_from_slice(b"NOPE");
assert!(matches!(
Packet::decode(&packet),
Err(Error::InvalidMagicCookie(_))
));
}
#[test]
fn pingack_roundtrip() {
let msg = PingAck {
version: VERSION_1_10,
local_endpoint_hash: [1; 8],
remote_endpoint_hash: [2; 8],
received_ping_ssrc: 3,
};
assert_eq!(
Message::decode(&Message::PingAck(msg.clone()).encode()).unwrap(),
Message::PingAck(msg)
);
}
#[test]
fn ping_decode_rejects_bad_length() {
assert!(matches!(
Ping::decode(&[0u8; 11]),
Err(Error::InvalidLength)
));
}
#[test]
fn pingack_decode_rejects_bad_length() {
let ack = PingAck {
version: VERSION_1_10,
local_endpoint_hash: [1; 8],
remote_endpoint_hash: [2; 8],
received_ping_ssrc: 3,
};
let mut bytes = Message::PingAck(ack).encode();
bytes.truncate(20);
assert!(matches!(Message::decode(&bytes), Err(Error::InvalidLength)));
}
#[test]
fn negotiation_works() {
let local = sample_hello();
let remote = Hello {
ciphers: vec![algos::CIPHER_AES3, algos::CIPHER_AES1],
..sample_hello()
};
let n = negotiate_algorithms(&local, &remote).unwrap();
assert_eq!(n.hash, HashAlgorithm::Sha256);
assert_eq!(n.cipher, CipherAlgorithm::Aes256);
}
#[test]
fn negotiation_rejects_ec38_without_sha384_hash() {
let local = Hello {
key_agreements: vec![algos::KEYAGREE_EC38],
..sample_hello()
};
let remote = Hello {
key_agreements: vec![algos::KEYAGREE_EC38],
..sample_hello()
};
assert!(matches!(
negotiate_algorithms(&local, &remote),
Err(Error::InvalidField("EC38 requires S384/N384"))
));
}
#[cfg(feature = "crypto")]
#[test]
fn engine_progresses() {
let local = sample_hello();
let remote = Hello {
zid: [0x99; 12],
..sample_hello()
};
let mut engine = ZrtpEngine::new(Role::Initiator, local, MemorySharedSecretStore::default());
assert!(matches!(
engine.start()[0],
EngineEvent::Send(Message::Hello(_))
));
let ev = engine.receive(&Message::Hello(remote)).unwrap();
assert!(ev
.iter()
.any(|e| matches!(e, EngineEvent::Send(Message::Commit(_)))));
assert_eq!(engine.state(), HandshakeState::CommitSent);
assert_eq!(
retransmit_ack_for(&Message::Commit(engine.last_commit().cloned().unwrap())),
Some(RetransmitAck::DhPart1OrConfirm1)
);
}
#[cfg(feature = "crypto")]
#[test]
fn engine_rejects_out_of_order_confirm1() {
let mut engine = ZrtpEngine::new(
Role::Initiator,
sample_hello(),
MemorySharedSecretStore::default(),
);
let confirm = Confirm {
confirm_mac: [0; 8],
cfb_iv: [0; 16],
encrypted: vec![0; 40],
};
assert!(engine.receive(&Message::Confirm1(confirm)).is_err());
assert_eq!(engine.state(), HandshakeState::Discovery);
}
#[cfg(feature = "crypto")]
#[test]
fn conf2ack_ignored_before_confirm2sent() {
let mut engine = ZrtpEngine::new(
Role::Initiator,
sample_hello(),
MemorySharedSecretStore::default(),
);
assert!(engine.receive(&Message::Conf2Ack).unwrap().is_empty());
assert_eq!(engine.state(), HandshakeState::Discovery);
}
#[cfg(feature = "crypto")]
fn drive_initiator_to_confirm2_sent() -> ZrtpEngine<MemorySharedSecretStore> {
let initiator_hello = sample_hello();
let responder_hello = Hello {
zid: [0x99; 12],
key_agreements: vec![algos::KEYAGREE_EC25],
sas_types: vec![algos::SAS_B32],
..sample_hello()
};
let mut initiator = ZrtpEngine::new(
Role::Initiator,
initiator_hello.clone(),
MemorySharedSecretStore::default(),
);
let mut responder = ZrtpEngine::new(
Role::Responder,
responder_hello.clone(),
MemorySharedSecretStore::default(),
);
let _ = initiator.start();
let commit = initiator
.receive(&Message::Hello(responder_hello))
.unwrap()
.into_iter()
.find_map(|e| match e {
EngineEvent::Send(Message::Commit(c)) => Some(c),
_ => None,
})
.unwrap();
let _ = responder.receive(&Message::Hello(initiator_hello)).unwrap();
let _ = responder.receive(&Message::Commit(commit.clone())).unwrap();
let dh1 = initiator
.receive(&Message::Commit(commit))
.unwrap()
.into_iter()
.find_map(|e| match e {
EngineEvent::Send(Message::DhPart1(dh)) => Some(dh),
_ => None,
})
.unwrap();
let responder_events = responder.receive(&Message::DhPart1(dh1)).unwrap();
let dh2 = responder_events
.iter()
.find_map(|e| match e {
EngineEvent::Send(Message::DhPart2(dh)) => Some(dh.clone()),
_ => None,
})
.unwrap();
let responder_confirm1 = responder_events
.iter()
.find_map(|e| match e {
EngineEvent::Send(Message::Confirm1(c)) => Some(c.clone()),
_ => None,
})
.unwrap();
let _ = initiator.receive(&Message::DhPart2(dh2)).unwrap();
let _ = initiator
.receive(&Message::Confirm1(responder_confirm1))
.unwrap();
assert_eq!(initiator.state(), HandshakeState::Confirm2Sent);
initiator
}
#[test]
fn retransmit_schedule_caps() {
let sched = RetransmitSchedule::post_hello();
assert_eq!(sched.at(0), Some(150));
assert_eq!(sched.at(1), Some(300));
assert_eq!(sched.at(5), Some(1200));
assert_eq!(sched.at(50), None);
}
#[test]
fn retransmit_state_stops_on_expected_ack() {
let state = RetransmitState::new(Message::Confirm2(Confirm {
confirm_mac: [0; 8],
cfb_iv: [0; 16],
encrypted: vec![0; 40],
}))
.unwrap();
assert!(state.stops_on(&Message::Conf2Ack, false));
assert!(state.stops_on(
&Message::PingAck(PingAck {
version: VERSION_1_10,
local_endpoint_hash: [0; 8],
remote_endpoint_hash: [0; 8],
received_ping_ssrc: 0,
}),
true
));
}
#[test]
fn retransmit_state_advances_until_exhausted() {
let mut state = RetransmitState::new(Message::Hello(sample_hello())).unwrap();
while !state.is_exhausted() {
state.advance();
}
assert!(state.is_exhausted());
}
#[test]
fn secret_store_roundtrip() {
let mut store = MemorySharedSecretStore::default();
let entry = CacheEntry {
local_zid: [1; 12],
remote_zid: [2; 12],
secrets: RetainedSecrets {
rs1: vec![3; 32],
rs2: vec![4; 32],
verified: true,
expiration_interval: 60,
},
};
store.store(entry.clone()).unwrap();
assert_eq!(store.load([1; 12], [2; 12]).unwrap(), Some(entry.clone()));
store.clear([1; 12], [2; 12]).unwrap();
assert_eq!(store.load([1; 12], [2; 12]).unwrap(), None);
}
#[cfg(feature = "crypto")]
#[test]
fn crypto_confirm_roundtrip() {
let content = ConfirmContent {
h0: [7; 32],
signature_length_words: 0,
flags: ConfirmFlags::default(),
cache_expiration_interval: 0,
signature_type: None,
signature: vec![],
};
let plaintext = encode_confirm_content(&content).unwrap();
let key = [9u8; 16];
let mac_key = [8u8; 32];
let confirm = finalize_confirm(
HashAlgorithm::Sha256,
&mac_key,
encrypt_confirm(CipherAlgorithm::Aes128, &key, &plaintext).unwrap(),
)
.unwrap();
assert!(verify_confirm(HashAlgorithm::Sha256, &mac_key, &confirm).unwrap());
let decoded =
decode_confirm_content(&decrypt_confirm(CipherAlgorithm::Aes128, &key, &confirm).unwrap())
.unwrap();
assert_eq!(decoded, content);
}
#[cfg(feature = "crypto")]
#[test]
fn session_secrets_drive_confirm_build_and_open() {
let s0 = vec![7u8; 32];
let secrets = derive_session_secrets(
HashAlgorithm::Sha256,
CipherAlgorithm::Aes128,
&s0,
&[1; 12],
&[2; 12],
)
.unwrap();
let content = ConfirmContent {
h0: [9; 32],
signature_length_words: 0,
flags: ConfirmFlags::default(),
cache_expiration_interval: 3,
signature_type: None,
signature: vec![],
};
let confirm = build_confirm_message(
HashAlgorithm::Sha256,
CipherAlgorithm::Aes128,
&secrets.confirm_key_initiator,
&secrets.confirm_mac_key_initiator,
&content,
)
.unwrap();
let opened = open_confirm_message(
HashAlgorithm::Sha256,
CipherAlgorithm::Aes128,
&secrets.confirm_key_initiator,
&secrets.confirm_mac_key_initiator,
&confirm,
)
.unwrap();
assert_eq!(opened, content);
assert_eq!(secrets.srtp_key_initiator.len(), 16);
assert_eq!(secrets.srtp_salt_initiator.len(), 14);
assert_eq!(secrets.srtp_key_responder.len(), 16);
assert_eq!(secrets.srtp_salt_responder.len(), 14);
assert_eq!(secrets.zrtp_session_key.len(), 32);
}
#[cfg(feature = "crypto")]
#[test]
fn ecdh_shared_secret_matches() {
let (sk1, pk1) = generate_dh_keypair(KeyAgreement::EcP256).unwrap();
let (sk2, pk2) = generate_dh_keypair(KeyAgreement::EcP256).unwrap();
let s1 = compute_dh_shared(&sk1, &pk2).unwrap();
let s2 = compute_dh_shared(&sk2, &pk1).unwrap();
assert_eq!(s1, s2);
}
#[cfg(feature = "crypto")]
#[test]
fn ecdh_p521_shared_secret_matches() {
let (sk1, pk1) = generate_dh_keypair(KeyAgreement::EcP521).unwrap();
let (sk2, pk2) = generate_dh_keypair(KeyAgreement::EcP521).unwrap();
let s1 = compute_dh_shared(&sk1, &pk2).unwrap();
let s2 = compute_dh_shared(&sk2, &pk1).unwrap();
assert_eq!(s1, s2);
}
#[cfg(feature = "crypto")]
#[test]
fn modp_dh3072_shared_secret_matches() {
let (sk1, pk1) = generate_dh_keypair(KeyAgreement::Dh3072).unwrap();
let (sk2, pk2) = generate_dh_keypair(KeyAgreement::Dh3072).unwrap();
let s1 = compute_dh_shared(&sk1, &pk2).unwrap();
let s2 = compute_dh_shared(&sk2, &pk1).unwrap();
assert_eq!(s1, s2);
assert_eq!(s1.len(), 384);
}
#[cfg(feature = "crypto")]
#[test]
fn b256_uses_pgp_words() {
let rendered = sas_render(SasType::B256, 0x0001_0000).unwrap();
assert_eq!(rendered, "aardvark adviser");
}
#[cfg(feature = "crypto")]
#[test]
fn engine_emits_sas_after_dh_exchange() {
let local = sample_hello();
let remote = Hello {
zid: [0x99; 12],
key_agreements: vec![algos::KEYAGREE_EC25],
sas_types: vec![algos::SAS_B32],
..sample_hello()
};
let mut initiator = ZrtpEngine::new(
Role::Initiator,
local.clone(),
MemorySharedSecretStore::default(),
);
let mut responder = ZrtpEngine::new(
Role::Responder,
remote.clone(),
MemorySharedSecretStore::default(),
);
let _ = initiator.start();
let commit = initiator
.receive(&Message::Hello(remote))
.unwrap()
.into_iter()
.find_map(|e| match e {
EngineEvent::Send(Message::Commit(c)) => Some(c),
_ => None,
})
.unwrap();
let _ = responder.receive(&Message::Hello(local)).unwrap();
let _ = responder.receive(&Message::Commit(commit.clone())).unwrap();
let dh1 = initiator
.receive(&Message::Commit(commit.clone()))
.unwrap()
.into_iter()
.find_map(|e| match e {
EngineEvent::Send(Message::DhPart1(dh)) => Some(dh),
_ => None,
})
.unwrap();
let responder_events = responder.receive(&Message::DhPart1(dh1)).unwrap();
let dh2 = responder_events
.iter()
.find_map(|e| match e {
EngineEvent::Send(Message::DhPart2(dh)) => Some(dh.clone()),
_ => None,
})
.unwrap();
assert!(responder_events
.iter()
.any(|e| matches!(e, EngineEvent::SasReady(_))));
let initiator_events = initiator.receive(&Message::DhPart2(dh2)).unwrap();
assert!(initiator_events
.iter()
.any(|e| matches!(e, EngineEvent::SasReady(_))));
}
#[cfg(feature = "crypto")]
#[test]
fn packet_pumped_handshake_reaches_secure_with_matching_sas() {
use std::collections::VecDeque;
#[derive(Default)]
struct Link {
queue: VecDeque<QueuedPacket>,
}
enum QueuedPacket {
ToA(Vec<u8>),
ToB(Vec<u8>),
}
impl Link {
fn send(&mut self, from_a: bool, packet: Packet) {
let bytes = packet.encode();
if from_a {
self.queue.push_back(QueuedPacket::ToB(bytes));
} else {
self.queue.push_back(QueuedPacket::ToA(bytes));
}
}
fn pop(&mut self) -> Option<(bool, Packet)> {
match self.queue.pop_front()? {
QueuedPacket::ToA(bytes) => Some((false, Packet::decode(&bytes).unwrap())),
QueuedPacket::ToB(bytes) => Some((true, Packet::decode(&bytes).unwrap())),
}
}
}
struct Peer {
is_a: bool,
sequence: u16,
ssrc: u32,
engine: ZrtpEngine<MemorySharedSecretStore>,
}
impl Peer {
fn new(is_a: bool, role: Role, hello: Hello) -> Self {
Self {
is_a,
sequence: if is_a { 1 } else { 100 },
ssrc: if is_a { 0x1111_2222 } else { 0x3333_4444 },
engine: ZrtpEngine::new(role, hello, MemorySharedSecretStore::default()),
}
}
fn emit(&mut self, message: Message, link: &mut Link) {
let packet = Packet {
sequence: self.sequence,
ssrc: self.ssrc,
message,
};
self.sequence = self.sequence.wrapping_add(1);
link.send(self.is_a, packet);
}
fn handle_packet(&mut self, packet: Packet, link: &mut Link) {
let events = self.engine.receive(&packet.message).unwrap();
for event in events {
if let EngineEvent::Send(message) = event {
self.emit(message, link);
}
}
}
}
let initiator_hello = sample_hello();
let responder_hello = Hello {
zid: [0x99; 12],
key_agreements: vec![algos::KEYAGREE_EC25],
sas_types: vec![algos::SAS_B32],
..sample_hello()
};
let mut link = Link::default();
let mut initiator = Peer::new(true, Role::Initiator, initiator_hello.clone());
let mut responder = Peer::new(false, Role::Responder, responder_hello.clone());
for event in initiator.engine.start() {
if let EngineEvent::Send(message) = event {
initiator.emit(message, &mut link);
}
}
for event in responder.engine.start() {
if let EngineEvent::Send(message) = event {
responder.emit(message, &mut link);
}
}
while let Some((to_b, packet)) = link.pop() {
if to_b {
responder.handle_packet(packet, &mut link);
} else {
initiator.handle_packet(packet, &mut link);
}
}
assert!(initiator.engine.last_sas().is_some());
assert!(responder.engine.last_sas().is_some());
assert_eq!(initiator.engine.last_sas(), responder.engine.last_sas());
assert_eq!(initiator.engine.state(), HandshakeState::Secure);
assert_eq!(responder.engine.state(), HandshakeState::Secure);
}
#[test]
fn preshared_commit_requires_cached_secrets() {
let local = sample_hello();
let remote = Hello {
zid: [0x77; 12],
key_agreements: vec![algos::KEYAGREE_PRSH],
..sample_hello()
};
let mut engine = ZrtpEngine::new(
Role::Responder,
local.clone(),
MemorySharedSecretStore::default(),
);
let _ = engine.receive(&Message::Hello(remote.clone())).unwrap();
let commit = Commit {
hash_image_h2: [0x44; 32],
zid: remote.zid,
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_PRSH,
sas: algos::SAS_B32,
nonce: CommitNonce::PresharedNonce {
nonce: [0x66; 16],
key_id: [0x42; 8],
},
mac: [0x77; 8],
};
assert!(engine.receive(&Message::Commit(commit)).is_err());
}
#[cfg(feature = "crypto")]
#[test]
fn multistream_commit_uses_cached_secrets() {
let local = sample_hello();
let remote = Hello {
zid: [0x77; 12],
key_agreements: vec![algos::KEYAGREE_MULT],
..sample_hello()
};
let mut store = MemorySharedSecretStore::default();
store
.store(CacheEntry {
local_zid: local.zid,
remote_zid: remote.zid,
secrets: RetainedSecrets {
rs1: vec![1; 32],
rs2: vec![2; 32],
verified: true,
expiration_interval: 30,
},
})
.unwrap();
let mut engine = ZrtpEngine::new(Role::Responder, local, store);
let _ = engine.receive(&Message::Hello(remote.clone())).unwrap();
let commit = Commit {
hash_image_h2: [0x44; 32],
zid: remote.zid,
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_MULT,
sas: algos::SAS_B32,
nonce: CommitNonce::MultistreamNonce([0x66; 16]),
mac: [0x77; 8],
};
let events = engine.receive(&Message::Commit(commit)).unwrap();
assert!(engine.session_secrets().is_some());
assert!(events
.iter()
.any(|e| matches!(e, EngineEvent::Send(Message::Confirm1(_)))));
}
#[cfg(feature = "crypto")]
#[test]
fn retained_secret_ids_are_stable() {
let ids1 = retained_secret_ids(
HashAlgorithm::Sha256,
Some(&[1; 32]),
Some(&[2; 32]),
None,
None,
)
.unwrap();
let ids2 = retained_secret_ids(
HashAlgorithm::Sha256,
Some(&[1; 32]),
Some(&[2; 32]),
None,
None,
)
.unwrap();
assert_eq!(ids1, ids2);
}
#[cfg(feature = "crypto")]
#[test]
fn retained_secret_match_helper_works() {
let ids = retained_secret_ids(
HashAlgorithm::Sha256,
Some(&[1; 32]),
Some(&[2; 32]),
None,
None,
)
.unwrap();
let matches = match_retained_secret_ids(&ids, &ids.rs1, &[0; 8], &[0; 8], &[0; 8]);
assert!(matches.rs1);
assert!(!matches.rs2);
}
#[cfg(feature = "crypto")]
#[test]
fn role_specific_secret_ids_differ() {
let h3 = [9u8; 32];
let a = retained_secret_ids_for_role(
HashAlgorithm::Sha256,
SecretIdRole::Initiator,
&h3,
Some(&[1; 32]),
None,
None,
None,
)
.unwrap();
let b = retained_secret_ids_for_role(
HashAlgorithm::Sha256,
SecretIdRole::Responder,
&h3,
Some(&[1; 32]),
None,
None,
None,
)
.unwrap();
assert_ne!(a.rs1, b.rs1);
}
#[cfg(feature = "crypto")]
#[test]
fn shared_secret_selection_prefers_rs1_then_rs2() {
let local = sample_hello();
let remote = Hello {
zid: [0x77; 12],
..sample_hello()
};
let mut store = MemorySharedSecretStore::default();
store
.store(CacheEntry {
local_zid: local.zid,
remote_zid: remote.zid,
secrets: RetainedSecrets {
rs1: vec![1; 32],
rs2: vec![2; 32],
verified: true,
expiration_interval: 30,
},
})
.unwrap();
let mut engine = ZrtpEngine::new(Role::Responder, local.clone(), store);
let _ = engine.receive(&Message::Hello(remote.clone())).unwrap();
let ids = retained_secret_ids_for_role(
HashAlgorithm::Sha256,
SecretIdRole::Responder,
&remote.hash_image_h3,
Some(&[1; 32]),
Some(&[2; 32]),
None,
None,
)
.unwrap();
let dh1 = DhPart1 {
hash_image_h1: [0; 32],
rs1_idr: ids.rs1,
rs2_idr: ids.rs2,
auxsecret_idr: ids.aux,
pbxsecret_idr: ids.pbx,
pvr: vec![0xaa; 64],
mac: [0; 8],
};
let selection = engine.shared_secret_selection_from_dhpart1(&dh1).unwrap();
assert_eq!(selection.s1, Some(&[1u8; 32][..]));
assert!(!selection.cache_mismatch);
}
#[cfg(feature = "crypto")]
#[test]
fn duplicate_commit_nonce_is_rejected() {
let local = sample_hello();
let remote = Hello {
zid: [0x77; 12],
key_agreements: vec![algos::KEYAGREE_MULT],
..sample_hello()
};
let mut store = MemorySharedSecretStore::default();
store
.store(CacheEntry {
local_zid: local.zid,
remote_zid: remote.zid,
secrets: RetainedSecrets {
rs1: vec![1; 32],
rs2: vec![2; 32],
verified: true,
expiration_interval: 30,
},
})
.unwrap();
let mut engine = ZrtpEngine::new(Role::Responder, local, store);
let _ = engine.receive(&Message::Hello(remote.clone())).unwrap();
let commit = Commit {
hash_image_h2: [0x44; 32],
zid: remote.zid,
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_MULT,
sas: algos::SAS_B32,
nonce: CommitNonce::MultistreamNonce([0x66; 16]),
mac: [0x77; 8],
};
engine.receive(&Message::Commit(commit.clone())).unwrap();
assert!(engine.receive(&Message::Commit(commit)).is_err());
}
#[cfg(feature = "crypto")]
#[test]
fn initiator_updates_cache_on_conf2ack_or_srtp() {
let mut engine = drive_initiator_to_confirm2_sent();
let ev = engine.observe_srtp_authenticated().unwrap();
assert!(ev.iter().any(|e| matches!(e, EngineEvent::SecureOn)));
assert_eq!(engine.state(), HandshakeState::Secure);
}
#[cfg(feature = "crypto")]
#[test]
fn conf2ack_also_completes_initiator() {
let mut engine = drive_initiator_to_confirm2_sent();
let ev = engine.receive(&Message::Conf2Ack).unwrap();
assert!(ev.iter().any(|e| matches!(e, EngineEvent::SecureOn)));
assert_eq!(engine.state(), HandshakeState::Secure);
}
#[cfg(feature = "crypto")]
#[test]
fn preshared_commit_roundtrip_includes_key_id() {
let commit = Commit {
hash_image_h2: [0x44; 32],
zid: [0x55; 12],
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_PRSH,
sas: algos::SAS_B32,
nonce: CommitNonce::PresharedNonce {
nonce: [0x66; 16],
key_id: [0x77; 8],
},
mac: [0x88; 8],
};
assert_eq!(
Message::decode(&Message::Commit(commit.clone()).encode()).unwrap(),
Message::Commit(commit)
);
}
#[cfg(feature = "crypto")]
#[test]
fn hvi_is_stable_for_same_transcript() {
let hello = Message::Hello(sample_hello()).encode();
let dh = Message::DhPart2(DhPart2 {
hash_image_h1: [1; 32],
rs1_idi: [2; 8],
rs2_idi: [3; 8],
auxsecret_idi: [4; 8],
pbxsecret_idi: [5; 8],
pvi: vec![6; 64],
mac: [7; 8],
})
.encode();
let a = compute_hvi(HashAlgorithm::Sha256, &dh, &hello).unwrap();
let b = compute_hvi(HashAlgorithm::Sha256, &dh, &hello).unwrap();
assert_eq!(a, b);
}
#[cfg(feature = "crypto")]
#[test]
fn responder_rejects_bad_hvi() {
let local = Hello {
zid: [0x99; 12],
key_agreements: vec![algos::KEYAGREE_EC25],
..sample_hello()
};
let remote = sample_hello();
let mut responder = ZrtpEngine::new(
Role::Responder,
local.clone(),
MemorySharedSecretStore::default(),
);
let _ = responder.receive(&Message::Hello(remote)).unwrap();
let commit = Commit {
hash_image_h2: [0x44; 32],
zid: [0x22; 12],
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_EC25,
sas: algos::SAS_B32,
nonce: CommitNonce::Hvi([1u8; 32]),
mac: [0x77; 8],
};
let _ = responder.receive(&Message::Commit(commit)).unwrap();
let bad_dh2 = DhPart2 {
hash_image_h1: [1; 32],
rs1_idi: [0; 8],
rs2_idi: [0; 8],
auxsecret_idi: [0; 8],
pbxsecret_idi: [0; 8],
pvi: vec![6; 64],
mac: [7; 8],
};
assert!(responder.receive(&Message::DhPart2(bad_dh2)).is_err());
}
#[test]
fn commit_contention_prefers_dh_over_preshared() {
let hello = sample_hello();
let local = Commit {
hash_image_h2: [1; 32],
zid: [2; 12],
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_DH3K,
sas: algos::SAS_B32,
nonce: CommitNonce::Hvi([9; 32]),
mac: [0; 8],
};
let remote = Commit {
hash_image_h2: [1; 32],
zid: [2; 12],
hash: algos::HASH_S256,
cipher: algos::CIPHER_AES1,
auth_tag: algos::AUTH_HS32,
key_agreement: algos::KEYAGREE_PRSH,
sas: algos::SAS_B32,
nonce: CommitNonce::PresharedNonce {
nonce: [1; 16],
key_id: [2; 8],
},
mac: [0; 8],
};
assert!(
ZrtpEngine::<MemorySharedSecretStore>::resolve_commit_contention(
&hello, &local, &hello, &remote
)
.unwrap()
);
}