use crate::constants;
use crate::error::{Error, Result};
use crate::primitives::{hkdf, hmac};
use subtle::ConstantTimeEq;
use zeroize::{Zeroize, ZeroizeOnDrop};
const MAX_CALL_ADVANCE: u32 = 1 << 24;
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct CallKeys {
send_key: [u8; 32],
recv_key: [u8; 32],
chain_key: [u8; 32],
#[zeroize(skip)]
lower_role: bool,
#[zeroize(skip)]
step_count: u32,
}
impl CallKeys {
pub fn send_key(&self) -> &[u8; 32] {
&self.send_key
}
pub fn recv_key(&self) -> &[u8; 32] {
&self.recv_key
}
#[cfg(all(feature = "test-utils", debug_assertions))]
#[deprecated(note = "test-utils only — do not call in production code")]
pub fn chain_key_bytes(&self) -> &[u8; 32] {
&self.chain_key
}
pub fn advance(&mut self) -> Result<()> {
if self.step_count >= MAX_CALL_ADVANCE {
self.send_key.zeroize();
self.recv_key.zeroize();
self.chain_key.zeroize();
return Err(Error::ChainExhausted);
}
let mut key_a = hmac::hmac_sha3_256(&self.chain_key, constants::CALL_KEY_A_BYTE);
let mut key_b = hmac::hmac_sha3_256(&self.chain_key, constants::CALL_KEY_B_BYTE);
let mut next_chain = hmac::hmac_sha3_256(&self.chain_key, constants::CALL_CHAIN_ADV_BYTE);
self.chain_key.zeroize();
self.send_key.zeroize();
self.recv_key.zeroize();
self.chain_key = next_chain;
if self.lower_role {
self.send_key = key_a;
self.recv_key = key_b;
} else {
self.send_key = key_b;
self.recv_key = key_a;
}
key_a.zeroize();
key_b.zeroize();
next_chain.zeroize();
self.step_count += 1;
Ok(())
}
}
pub fn derive_call_keys(
root_key: &[u8; 32],
kem_ss: &[u8; 32],
call_id: &[u8; 16],
local_fp: &[u8; 32],
remote_fp: &[u8; 32],
) -> Result<CallKeys> {
if bool::from(root_key.ct_eq(&[0u8; 32])) {
return Err(Error::InvalidData);
}
if bool::from(kem_ss.ct_eq(&[0u8; 32])) {
return Err(Error::InvalidData);
}
if call_id == &[0u8; 16] {
return Err(Error::InvalidData);
}
if local_fp == remote_fp {
return Err(Error::InvalidData);
}
let mut ikm = [0u8; 48];
ikm[..32].copy_from_slice(kem_ss);
ikm[32..].copy_from_slice(call_id);
let (fp_lo, fp_hi) = if local_fp < remote_fp {
(local_fp, remote_fp)
} else {
(remote_fp, local_fp)
};
let mut info = [0u8; 10 + 32 + 32]; info[..10].copy_from_slice(constants::CALL_HKDF_INFO);
info[10..42].copy_from_slice(fp_lo);
info[42..74].copy_from_slice(fp_hi);
let mut output = [0u8; 96];
hkdf::hkdf_sha3_256(root_key, &ikm, &info, &mut output)?;
ikm.zeroize();
let lower_role = local_fp < remote_fp;
let (send_off, recv_off) = if lower_role { (0, 32) } else { (32, 0) };
let mut send_key = [0u8; 32];
let mut recv_key = [0u8; 32];
let mut chain_key = [0u8; 32];
send_key.copy_from_slice(&output[send_off..send_off + 32]);
recv_key.copy_from_slice(&output[recv_off..recv_off + 32]);
chain_key.copy_from_slice(&output[64..96]);
output.zeroize();
let keys = CallKeys {
send_key,
recv_key,
chain_key,
lower_role,
step_count: 0,
};
send_key.zeroize();
recv_key.zeroize();
chain_key.zeroize();
Ok(keys)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::{hkdf, hmac};
use std::collections::HashSet;
const RK: [u8; 32] = [0x01u8; 32];
const SS: [u8; 32] = [0x02u8; 32];
const CALL_ID: [u8; 16] = [0x03u8; 16];
const FP_LO: [u8; 32] = [0x00u8; 32]; const FP_HI: [u8; 32] = [0xFFu8; 32];
fn reference_hkdf(rk: &[u8; 32], ss: &[u8; 32], call_id: &[u8; 16]) -> [u8; 96] {
let mut ikm = [0u8; 48];
ikm[..32].copy_from_slice(ss);
ikm[32..].copy_from_slice(call_id);
let mut info = [0u8; 10 + 32 + 32];
info[..10].copy_from_slice(b"lo-call-v1");
info[10..42].copy_from_slice(&FP_LO); info[42..74].copy_from_slice(&FP_HI);
let mut output = [0u8; 96];
hkdf::hkdf_sha3_256(rk, &ikm, &info, &mut output).unwrap();
output
}
#[test]
fn derive_round_trip_matching() {
let alice = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
let bob = derive_call_keys(&RK, &SS, &CALL_ID, &FP_HI, &FP_LO).unwrap();
assert_eq!(alice.send_key(), bob.recv_key());
assert_eq!(alice.recv_key(), bob.send_key());
}
#[test]
fn derive_hkdf_kat() {
let output = reference_hkdf(&RK, &SS, &CALL_ID);
let key_a = &output[..32];
let key_b = &output[32..64];
let keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
assert_eq!(keys.send_key().as_slice(), key_a);
assert_eq!(keys.recv_key().as_slice(), key_b);
}
#[test]
fn advance_hmac_kat() {
let output = reference_hkdf(&RK, &SS, &CALL_ID);
let chain_key = &output[64..96];
let expected_a = hmac::hmac_sha3_256(chain_key, constants::CALL_KEY_A_BYTE);
let expected_b = hmac::hmac_sha3_256(chain_key, constants::CALL_KEY_B_BYTE);
let expected_chain2 = hmac::hmac_sha3_256(chain_key, constants::CALL_CHAIN_ADV_BYTE);
let expected_a2 = hmac::hmac_sha3_256(&expected_chain2, constants::CALL_KEY_A_BYTE);
let expected_b2 = hmac::hmac_sha3_256(&expected_chain2, constants::CALL_KEY_B_BYTE);
let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
keys.advance().unwrap();
assert_eq!(*keys.send_key(), expected_a);
assert_eq!(*keys.recv_key(), expected_b);
keys.advance().unwrap();
assert_eq!(*keys.send_key(), expected_a2);
assert_eq!(*keys.recv_key(), expected_b2);
}
#[test]
fn derive_different_call_ids() {
let k1 = derive_call_keys(&RK, &SS, &[0x01u8; 16], &FP_LO, &FP_HI).unwrap();
let k2 = derive_call_keys(&RK, &SS, &[0x02u8; 16], &FP_LO, &FP_HI).unwrap();
assert_ne!(k1.send_key(), k2.send_key());
}
#[test]
fn derive_different_kem_ss() {
let k1 = derive_call_keys(&RK, &[0xAAu8; 32], &CALL_ID, &FP_LO, &FP_HI).unwrap();
let k2 = derive_call_keys(&RK, &[0xBBu8; 32], &CALL_ID, &FP_LO, &FP_HI).unwrap();
assert_ne!(k1.send_key(), k2.send_key());
}
#[test]
fn derive_different_root_keys() {
let k1 = derive_call_keys(&[0xAAu8; 32], &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
let k2 = derive_call_keys(&[0xBBu8; 32], &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
assert_ne!(k1.send_key(), k2.send_key());
}
#[test]
fn role_assignment_lower_fp_is_send() {
let output = reference_hkdf(&RK, &SS, &CALL_ID);
let key_a = &output[..32];
let key_b = &output[32..64];
let lower = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
assert_eq!(lower.send_key().as_slice(), key_a);
assert_eq!(lower.recv_key().as_slice(), key_b);
let higher = derive_call_keys(&RK, &SS, &CALL_ID, &FP_HI, &FP_LO).unwrap();
assert_eq!(higher.send_key().as_slice(), key_b);
assert_eq!(higher.recv_key().as_slice(), key_a);
}
#[test]
fn advance_changes_keys() {
let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
let send_before = *keys.send_key();
let recv_before = *keys.recv_key();
keys.advance().unwrap();
assert_ne!(*keys.send_key(), send_before);
assert_ne!(*keys.recv_key(), recv_before);
}
#[test]
fn advance_preserves_role() {
let mut alice = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
let mut bob = derive_call_keys(&RK, &SS, &CALL_ID, &FP_HI, &FP_LO).unwrap();
alice.advance().unwrap();
bob.advance().unwrap();
assert_eq!(alice.send_key(), bob.recv_key());
assert_eq!(alice.recv_key(), bob.send_key());
}
#[test]
fn advance_produces_distinct_keys() {
let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
keys.advance().unwrap();
let after_first = *keys.send_key();
keys.advance().unwrap();
let after_second = *keys.send_key();
assert_ne!(after_first, after_second);
}
#[test]
fn advance_keys_independent_of_chain_key() {
let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
for _ in 0..10 {
keys.advance().unwrap();
#[allow(deprecated)]
let ck = *keys.chain_key_bytes();
assert_ne!(*keys.send_key(), ck, "send_key == chain_key after advance");
assert_ne!(*keys.recv_key(), ck, "recv_key == chain_key after advance");
}
}
#[test]
fn advance_multiple_steps() {
let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
let mut seen = HashSet::new();
for _ in 0..100 {
seen.insert(*keys.send_key());
keys.advance().unwrap();
}
assert_eq!(seen.len(), 100);
}
#[test]
fn send_recv_keys_differ() {
let keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
assert_ne!(keys.send_key(), keys.recv_key());
}
#[test]
fn keys_are_32_bytes() {
let keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
assert!(keys.send_key().iter().any(|&b| b != 0));
assert!(keys.recv_key().iter().any(|&b| b != 0));
}
#[test]
fn zero_kem_ss_rejected() {
assert!(matches!(
derive_call_keys(&RK, &[0u8; 32], &CALL_ID, &FP_LO, &FP_HI),
Err(crate::error::Error::InvalidData)
));
}
#[test]
fn zero_root_key_rejected() {
assert!(matches!(
derive_call_keys(&[0u8; 32], &SS, &CALL_ID, &FP_LO, &FP_HI),
Err(crate::error::Error::InvalidData)
));
}
#[test]
fn zero_call_id_rejected() {
assert!(matches!(
derive_call_keys(&RK, &SS, &[0u8; 16], &FP_LO, &FP_HI),
Err(crate::error::Error::InvalidData)
));
}
#[test]
fn advance_exhaustion_returns_chain_exhausted() {
let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
keys.step_count = MAX_CALL_ADVANCE - 1;
assert!(keys.advance().is_ok());
assert!(matches!(keys.advance(), Err(Error::ChainExhausted)));
}
#[test]
fn advance_exhaustion_zeroizes_keys() {
let mut keys = derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_HI).unwrap();
keys.step_count = MAX_CALL_ADVANCE - 1;
keys.advance().unwrap();
assert!(matches!(keys.advance(), Err(Error::ChainExhausted)));
assert_eq!(
*keys.send_key(),
[0u8; 32],
"send_key not zeroed after ChainExhausted"
);
assert_eq!(
*keys.recv_key(),
[0u8; 32],
"recv_key not zeroed after ChainExhausted"
);
#[allow(deprecated)]
{
assert_eq!(
*keys.chain_key_bytes(),
[0u8; 32],
"chain_key not zeroed after ChainExhausted"
);
}
}
#[test]
fn equal_fingerprints_rejected() {
assert!(matches!(
derive_call_keys(&RK, &SS, &CALL_ID, &FP_LO, &FP_LO),
Err(crate::error::Error::InvalidData)
));
}
#[test]
fn kdf_call_spec_kat() {
use hex_literal::hex;
let root_key: [u8; 32] = [0xAAu8; 32];
let kem_ss: [u8; 32] = [0xBBu8; 32];
let call_id: [u8; 16] = [0xCCu8; 16];
let local_fp: [u8; 32] = [0x11u8; 32]; let remote_fp: [u8; 32] = [0x22u8; 32];
let expected_key_a =
hex!("ed75d812373c9b3bf6bddd394a631950520503f103b492fb908621eb712b5970");
let expected_key_b =
hex!("c3e5171534e0d1f922ea4ebf318357b990eafb0fff45d8cf430639a1fe2bb1e4");
let expected_chain_key =
hex!("1427dde311aaa195b116cc98c870753179297981446d3b53e00a4a92a0d34aeb");
let keys = derive_call_keys(&root_key, &kem_ss, &call_id, &local_fp, &remote_fp).unwrap();
assert_eq!(
keys.send_key().as_slice(),
&expected_key_a,
"send_key (key_a) does not match F.15 vector"
);
assert_eq!(
keys.recv_key().as_slice(),
&expected_key_b,
"recv_key (key_b) does not match F.15 vector"
);
#[allow(deprecated)]
{
assert_eq!(
keys.chain_key_bytes().as_slice(),
&expected_chain_key,
"chain_key does not match F.15 vector"
);
}
let keys_rev =
derive_call_keys(&root_key, &kem_ss, &call_id, &remote_fp, &local_fp).unwrap();
assert_eq!(
keys_rev.send_key().as_slice(),
&expected_key_b,
"reversed: send_key should be key_b"
);
assert_eq!(
keys_rev.recv_key().as_slice(),
&expected_key_a,
"reversed: recv_key should be key_a"
);
}
#[test]
fn advance_call_chain_spec_kat() {
use hex_literal::hex;
let root_key: [u8; 32] = [0xAAu8; 32];
let kem_ss: [u8; 32] = [0xBBu8; 32];
let call_id: [u8; 16] = [0xCCu8; 16];
let local_fp: [u8; 32] = [0x11u8; 32];
let remote_fp: [u8; 32] = [0x22u8; 32];
let expected_key_a_prime =
hex!("9cf3129c6bb7ad86cb12ffc534517a4c06a472fbcddbe295a501c79aa49800e1");
let expected_key_b_prime =
hex!("f24cd7822fd611159a6e6d809c6ac148fd7b9bad65d8b4f85745869634b2dd1e");
let expected_chain_key_prime =
hex!("d3ae610c39cd9f7f8dce990b5c91634092ad0621fc01b44b24b2cb9f3638d0f2");
let mut keys =
derive_call_keys(&root_key, &kem_ss, &call_id, &local_fp, &remote_fp).unwrap();
keys.advance().unwrap();
assert_eq!(
keys.send_key().as_slice(),
&expected_key_a_prime,
"send_key (key_a') does not match F.16 vector"
);
assert_eq!(
keys.recv_key().as_slice(),
&expected_key_b_prime,
"recv_key (key_b') does not match F.16 vector"
);
#[allow(deprecated)]
{
assert_eq!(
keys.chain_key_bytes().as_slice(),
&expected_chain_key_prime,
"chain_key' does not match F.16 vector"
);
}
}
proptest::proptest! {
#[test]
fn proptest_derive_round_trip(
rk in proptest::array::uniform32(proptest::prelude::any::<u8>()),
ss in proptest::array::uniform32(proptest::prelude::any::<u8>()),
call_id in proptest::array::uniform16(proptest::prelude::any::<u8>()),
fp_a in proptest::array::uniform32(proptest::prelude::any::<u8>()),
fp_b in proptest::array::uniform32(proptest::prelude::any::<u8>()),
) {
proptest::prop_assume!(fp_a != fp_b);
proptest::prop_assume!(rk != [0u8; 32]);
proptest::prop_assume!(ss != [0u8; 32]);
proptest::prop_assume!(call_id != [0u8; 16]);
let alice = derive_call_keys(&rk, &ss, &call_id, &fp_a, &fp_b).unwrap();
let bob = derive_call_keys(&rk, &ss, &call_id, &fp_b, &fp_a).unwrap();
proptest::prop_assert_eq!(alice.send_key(), bob.recv_key());
proptest::prop_assert_eq!(alice.recv_key(), bob.send_key());
}
}
}