#![cfg(feature = "crypto")]
use srt_runtime::caller::{CallerHandshake, CallerHandshakeState};
use srt_runtime::handshake_sm::{CryptoConfig, HandshakeConfig, HandshakeOutput, RejectionReason};
use srt_runtime::listener::{ListenerHandshake, ListenerHandshakeState};
use srt_runtime::packet::{ControlPacket, EncryptionField};
const PKT_SEQ_NO: u32 = 0x0000_2A2A;
fn config_with_crypto(crypto: CryptoConfig) -> HandshakeConfig {
HandshakeConfig {
encryption_field: EncryptionField::Aes128,
crypto: Some(crypto),
..HandshakeConfig::default()
}
}
struct ExchangeResult {
caller_outputs: Vec<HandshakeOutput>,
listener_outputs: Vec<HandshakeOutput>,
}
fn run_exchange(caller: &mut CallerHandshake, listener: &mut ListenerHandshake) -> ExchangeResult {
let mut caller_outputs = Vec::new();
let mut listener_outputs = Vec::new();
let induction = caller.start().unwrap();
let pkt = ControlPacket::parse(&induction).unwrap();
let outs = listener.feed(&pkt).unwrap();
listener_outputs.extend(outs.clone());
let listener_induction_resp = match &outs[0] {
HandshakeOutput::Send(b) => b.clone(),
other => panic!("expected Send, got {other:?}"),
};
let pkt = ControlPacket::parse(&listener_induction_resp).unwrap();
let outs = caller.feed(&pkt).unwrap();
caller_outputs.extend(outs.clone());
let conclusion = match &outs[0] {
HandshakeOutput::Send(b) => b.clone(),
other => panic!("expected Send, got {other:?}"),
};
if matches!(caller.state(), CallerHandshakeState::Rejected) {
return ExchangeResult {
caller_outputs,
listener_outputs,
};
}
let pkt = ControlPacket::parse(&conclusion).unwrap();
let outs = listener.feed(&pkt).unwrap();
listener_outputs.extend(outs.clone());
let listener_conclusion_resp = match &outs[0] {
HandshakeOutput::Send(b) => b.clone(),
other => panic!("expected Send, got {other:?}"),
};
if matches!(listener.state(), ListenerHandshakeState::Rejected) {
let pkt = ControlPacket::parse(&listener_conclusion_resp).unwrap();
let outs = caller.feed(&pkt).unwrap();
caller_outputs.extend(outs);
return ExchangeResult {
caller_outputs,
listener_outputs,
};
}
let pkt = ControlPacket::parse(&listener_conclusion_resp).unwrap();
let outs = caller.feed(&pkt).unwrap();
caller_outputs.extend(outs);
ExchangeResult {
caller_outputs,
listener_outputs,
}
}
#[test]
fn same_passphrase_negotiates_identical_sek_verified_by_matching_ciphertext() {
let passphrase = b"correct horse battery staple".to_vec();
let salt = [0x11u8; srt_runtime::crypto::SALT_LEN];
let sek = vec![0x42u8; 16];
let caller_cfg = config_with_crypto(CryptoConfig {
passphrase: passphrase.clone(),
salt,
sek: sek.clone(),
});
let listener_cfg = config_with_crypto(CryptoConfig {
passphrase,
salt: [0u8; srt_runtime::crypto::SALT_LEN], sek: Vec::new(), });
let mut caller = CallerHandshake::new(0x1111_1111, caller_cfg);
let mut listener = ListenerHandshake::new(0x2222_2222, 0xC0FF_EE00, listener_cfg);
run_exchange(&mut caller, &mut listener);
assert_eq!(caller.state(), CallerHandshakeState::Connected);
assert_eq!(listener.state(), ListenerHandshakeState::Connected);
let caller_negotiated = caller.negotiated().unwrap();
let listener_negotiated = listener.negotiated().unwrap();
let caller_sek = caller_negotiated
.sek
.clone()
.expect("caller negotiated a SEK");
let listener_sek = listener_negotiated
.sek
.clone()
.expect("listener negotiated a SEK");
let caller_salt = caller_negotiated.salt.expect("caller negotiated a Salt");
let listener_salt = listener_negotiated
.salt
.expect("listener negotiated a Salt");
assert_eq!(
caller_sek, listener_sek,
"negotiated SEKs must be identical"
);
assert_eq!(
caller_sek, sek,
"the negotiated SEK is the one the Caller generated"
);
assert_eq!(
caller_salt, listener_salt,
"negotiated Salts must be identical"
);
assert_eq!(caller_salt, salt);
let plaintext = b"SRT payload encryption handshake wiring test.".to_vec();
let mut caller_ciphertext = plaintext.clone();
srt_runtime::crypto::aes_ctr_apply(
&caller_sek,
&caller_salt,
PKT_SEQ_NO,
&mut caller_ciphertext,
)
.unwrap();
let mut listener_ciphertext = plaintext.clone();
srt_runtime::crypto::aes_ctr_apply(
&listener_sek,
&listener_salt,
PKT_SEQ_NO,
&mut listener_ciphertext,
)
.unwrap();
assert_eq!(
caller_ciphertext, listener_ciphertext,
"both peers' independently-derived SEK+Salt must produce identical ciphertext"
);
assert_ne!(
caller_ciphertext, plaintext,
"encryption must change the bytes"
);
}
#[test]
fn different_passphrases_are_rejected_and_never_share_a_sek() {
let salt = [0x22u8; srt_runtime::crypto::SALT_LEN];
let sek = vec![0x55u8; 16];
let caller_cfg = config_with_crypto(CryptoConfig {
passphrase: b"passphrase A".to_vec(),
salt,
sek,
});
let listener_cfg = config_with_crypto(CryptoConfig {
passphrase: b"passphrase B (different)".to_vec(),
salt: [0u8; srt_runtime::crypto::SALT_LEN],
sek: Vec::new(),
});
let mut caller = CallerHandshake::new(0x3333_3333, caller_cfg);
let mut listener = ListenerHandshake::new(0x4444_4444, 0xDEAD_BEEF, listener_cfg);
let result = run_exchange(&mut caller, &mut listener);
assert_eq!(listener.state(), ListenerHandshakeState::Rejected);
assert!(
result
.listener_outputs
.contains(&HandshakeOutput::Rejected(RejectionReason::BadSecret)),
"listener must reject with BadSecret, got {:?}",
result.listener_outputs
);
assert_eq!(caller.state(), CallerHandshakeState::Rejected);
assert!(
result
.caller_outputs
.contains(&HandshakeOutput::Rejected(RejectionReason::BadSecret)),
"caller must observe the rejection, got {:?}",
result.caller_outputs
);
assert!(caller.negotiated().is_none());
assert!(listener.negotiated().is_none());
let kek_a = srt_runtime::crypto::derive_kek(b"passphrase A", &salt, 16).unwrap();
let kek_b = srt_runtime::crypto::derive_kek(b"passphrase B (different)", &salt, 16).unwrap();
assert_ne!(
kek_a, kek_b,
"different passphrases must derive different KEKs"
);
}