Skip to main content

rns_core/link/
handshake.rs

1use alloc::vec::Vec;
2
3use rns_crypto::ed25519::{Ed25519PrivateKey, Ed25519PublicKey};
4use rns_crypto::hkdf::hkdf;
5use rns_crypto::x25519::{X25519PrivateKey, X25519PublicKey};
6
7use super::types::{LinkError, LinkId, LinkMode};
8use crate::constants::{LINK_ECPUBSIZE, LINK_MODE_BYTEMASK, LINK_MTU_BYTEMASK, LINK_MTU_SIZE};
9use crate::hash::truncated_hash;
10
11/// Compute link_id from a LINKREQUEST packet's hashable part.
12///
13/// The signalling bytes (if present) are stripped from the end before hashing.
14/// `extra_bytes_len` is `data.len() - ECPUBSIZE` (0 or MTU_SIZE).
15pub fn compute_link_id(hashable_part: &[u8], extra_bytes_len: usize) -> LinkId {
16    let end = if extra_bytes_len > 0 && hashable_part.len() > extra_bytes_len {
17        hashable_part.len() - extra_bytes_len
18    } else {
19        hashable_part.len()
20    };
21    truncated_hash(&hashable_part[..end])
22}
23
24/// Build signalling bytes: 3 big-endian bytes encoding MTU (21 bits) + mode (3 bits).
25///
26/// Format: `(mtu & 0x1FFFFF) + (((mode << 5) & 0xE0) << 16)`, packed as big-endian u32,
27/// then take the last 3 bytes.
28pub fn build_signalling_bytes(mtu: u32, mode: LinkMode) -> [u8; 3] {
29    let mode_bits = ((mode.mode_byte() << 5) & LINK_MODE_BYTEMASK) as u32;
30    let signalling_value = (mtu & LINK_MTU_BYTEMASK) + (mode_bits << 16);
31    let bytes = signalling_value.to_be_bytes();
32    [bytes[1], bytes[2], bytes[3]]
33}
34
35/// Parse signalling bytes → (mtu, mode).
36pub fn parse_signalling_bytes(bytes: &[u8; 3]) -> Result<(u32, LinkMode), LinkError> {
37    let mtu = ((bytes[0] as u32) << 16) | ((bytes[1] as u32) << 8) | (bytes[2] as u32);
38    let mode_byte = (bytes[0] & LINK_MODE_BYTEMASK) >> 5;
39    let mtu_val = mtu & LINK_MTU_BYTEMASK;
40    let mode = LinkMode::from_byte(mode_byte)?;
41    Ok((mtu_val, mode))
42}
43
44/// Build LINKREQUEST data: `[x25519_pub:32][ed25519_pub:32][signalling:0-3]`.
45pub fn build_linkrequest_data(
46    pub_bytes: &[u8; 32],
47    sig_pub_bytes: &[u8; 32],
48    mtu: Option<u32>,
49    mode: LinkMode,
50) -> Vec<u8> {
51    let mut data = Vec::with_capacity(LINK_ECPUBSIZE + LINK_MTU_SIZE);
52    data.extend_from_slice(pub_bytes);
53    data.extend_from_slice(sig_pub_bytes);
54    if let Some(mtu_val) = mtu {
55        let sig_bytes = build_signalling_bytes(mtu_val, mode);
56        data.extend_from_slice(&sig_bytes);
57    }
58    data
59}
60
61/// Parse LINKREQUEST data. Returns `(x25519_pub, ed25519_pub, mtu, mode)`.
62pub fn parse_linkrequest_data(
63    data: &[u8],
64) -> Result<([u8; 32], [u8; 32], Option<u32>, LinkMode), LinkError> {
65    if data.len() != LINK_ECPUBSIZE && data.len() != LINK_ECPUBSIZE + LINK_MTU_SIZE {
66        return Err(LinkError::InvalidData);
67    }
68
69    let mut x25519_pub = [0u8; 32];
70    let mut ed25519_pub = [0u8; 32];
71    x25519_pub.copy_from_slice(&data[..32]);
72    ed25519_pub.copy_from_slice(&data[32..64]);
73
74    if data.len() == LINK_ECPUBSIZE + LINK_MTU_SIZE {
75        let mut sig_bytes = [0u8; 3];
76        sig_bytes.copy_from_slice(&data[LINK_ECPUBSIZE..LINK_ECPUBSIZE + LINK_MTU_SIZE]);
77        let (mtu, mode) = parse_signalling_bytes(&sig_bytes)?;
78        Ok((x25519_pub, ed25519_pub, Some(mtu), mode))
79    } else {
80        Ok((x25519_pub, ed25519_pub, None, LinkMode::Aes256Cbc))
81    }
82}
83
84/// Build LRPROOF data: `[signature:64][x25519_pub:32][signalling:0-3]`.
85///
86/// Signs: `link_id + pub_bytes + sig_pub_bytes + signalling_bytes`.
87pub fn build_lrproof(
88    link_id: &LinkId,
89    pub_bytes: &[u8; 32],
90    sig_pub_bytes: &[u8; 32],
91    sig_prv: &Ed25519PrivateKey,
92    mtu: Option<u32>,
93    mode: LinkMode,
94) -> Vec<u8> {
95    let signalling_bytes = if let Some(mtu_val) = mtu {
96        build_signalling_bytes(mtu_val, mode).to_vec()
97    } else {
98        Vec::new()
99    };
100
101    let mut signed_data = Vec::with_capacity(16 + 32 + 32 + signalling_bytes.len());
102    signed_data.extend_from_slice(link_id);
103    signed_data.extend_from_slice(pub_bytes);
104    signed_data.extend_from_slice(sig_pub_bytes);
105    signed_data.extend_from_slice(&signalling_bytes);
106
107    let signature = sig_prv.sign(&signed_data);
108
109    let mut proof_data = Vec::with_capacity(64 + 32 + signalling_bytes.len());
110    proof_data.extend_from_slice(&signature);
111    proof_data.extend_from_slice(pub_bytes);
112    proof_data.extend_from_slice(&signalling_bytes);
113
114    proof_data
115}
116
117/// Validate LRPROOF. Returns peer X25519 public key bytes on success.
118///
119/// Expects `proof_data = [signature:64][x25519_pub:32][signalling:0-3]`.
120/// Validates against `signed_data = link_id + peer_x25519_pub + peer_sig_pub + signalling`.
121pub fn validate_lrproof(
122    proof_data: &[u8],
123    link_id: &LinkId,
124    peer_sig_pub: &Ed25519PublicKey,
125    peer_sig_pub_bytes: &[u8; 32],
126) -> Result<([u8; 32], Option<u32>, LinkMode), LinkError> {
127    let sig_len = 64;
128    let pub_len = 32;
129
130    if proof_data.len() != sig_len + pub_len
131        && proof_data.len() != sig_len + pub_len + LINK_MTU_SIZE
132    {
133        return Err(LinkError::InvalidData);
134    }
135
136    let mut signature = [0u8; 64];
137    signature.copy_from_slice(&proof_data[..sig_len]);
138
139    let mut peer_pub = [0u8; 32];
140    peer_pub.copy_from_slice(&proof_data[sig_len..sig_len + pub_len]);
141
142    let signalling_bytes = &proof_data[sig_len + pub_len..];
143
144    let (mtu, mode) = if signalling_bytes.len() == LINK_MTU_SIZE {
145        let mut sb = [0u8; 3];
146        sb.copy_from_slice(signalling_bytes);
147        let (m, md) = parse_signalling_bytes(&sb)?;
148        (Some(m), md)
149    } else {
150        (None, LinkMode::Aes256Cbc)
151    };
152
153    let mut signed_data = Vec::with_capacity(16 + 32 + 32 + signalling_bytes.len());
154    signed_data.extend_from_slice(link_id);
155    signed_data.extend_from_slice(&peer_pub);
156    signed_data.extend_from_slice(peer_sig_pub_bytes);
157    signed_data.extend_from_slice(signalling_bytes);
158
159    if peer_sig_pub.verify(&signature, &signed_data) {
160        Ok((peer_pub, mtu, mode))
161    } else {
162        Err(LinkError::InvalidSignature)
163    }
164}
165
166/// Derive session key using HKDF.
167///
168/// `shared_key` is the raw ECDH output (32 bytes).
169/// Salt = link_id, context = None.
170/// Output length depends on mode: 32 for AES-128, 64 for AES-256.
171pub fn derive_session_key(
172    shared_key: &[u8; 32],
173    link_id: &LinkId,
174    mode: LinkMode,
175) -> Result<Vec<u8>, LinkError> {
176    let length = mode.derived_key_length();
177    hkdf(length, shared_key, Some(link_id), None).map_err(|_| LinkError::CryptoError)
178}
179
180/// Perform ECDH key exchange and derive session key.
181pub fn perform_key_exchange(
182    prv: &X25519PrivateKey,
183    peer_pub_bytes: &[u8; 32],
184    link_id: &LinkId,
185    mode: LinkMode,
186) -> Result<Vec<u8>, LinkError> {
187    let peer_pub = X25519PublicKey::from_bytes(peer_pub_bytes);
188    let shared_key = prv.exchange(&peer_pub);
189    derive_session_key(&shared_key, link_id, mode)
190}
191
192/// Pack RTT as msgpack float64: `0xcb` + 8 bytes big-endian.
193pub fn pack_rtt(rtt: f64) -> Vec<u8> {
194    let mut data = Vec::with_capacity(9);
195    data.push(0xcb);
196    data.extend_from_slice(&rtt.to_be_bytes());
197    data
198}
199
200/// Unpack RTT from msgpack float64.
201pub fn unpack_rtt(data: &[u8]) -> Option<f64> {
202    if data.len() == 9 && data[0] == 0xcb {
203        let mut bytes = [0u8; 8];
204        bytes.copy_from_slice(&data[1..9]);
205        Some(f64::from_be_bytes(bytes))
206    } else {
207        None
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use rns_crypto::FixedRng;
215
216    #[test]
217    fn test_signalling_bytes_roundtrip() {
218        let mtu = 500u32;
219        let mode = LinkMode::Aes256Cbc;
220        let bytes = build_signalling_bytes(mtu, mode);
221        let (parsed_mtu, parsed_mode) = parse_signalling_bytes(&bytes).unwrap();
222        assert_eq!(parsed_mtu, mtu);
223        assert_eq!(parsed_mode, mode);
224    }
225
226    #[test]
227    fn test_signalling_bytes_aes128() {
228        let mtu = 1234u32;
229        let mode = LinkMode::Aes128Cbc;
230        let bytes = build_signalling_bytes(mtu, mode);
231        let (parsed_mtu, parsed_mode) = parse_signalling_bytes(&bytes).unwrap();
232        assert_eq!(parsed_mtu, mtu);
233        assert_eq!(parsed_mode, mode);
234    }
235
236    #[test]
237    fn test_signalling_bytes_max_mtu() {
238        let mtu = LINK_MTU_BYTEMASK; // maximum 21-bit value
239        let mode = LinkMode::Aes256Cbc;
240        let bytes = build_signalling_bytes(mtu, mode);
241        let (parsed_mtu, parsed_mode) = parse_signalling_bytes(&bytes).unwrap();
242        assert_eq!(parsed_mtu, mtu);
243        assert_eq!(parsed_mode, mode);
244    }
245
246    #[test]
247    fn test_linkrequest_data_roundtrip() {
248        let pub_bytes = [0xAAu8; 32];
249        let sig_pub_bytes = [0xBBu8; 32];
250        let data =
251            build_linkrequest_data(&pub_bytes, &sig_pub_bytes, Some(500), LinkMode::Aes256Cbc);
252        assert_eq!(data.len(), LINK_ECPUBSIZE + LINK_MTU_SIZE);
253
254        let (p, s, mtu, mode) = parse_linkrequest_data(&data).unwrap();
255        assert_eq!(p, pub_bytes);
256        assert_eq!(s, sig_pub_bytes);
257        assert_eq!(mtu, Some(500));
258        assert_eq!(mode, LinkMode::Aes256Cbc);
259    }
260
261    #[test]
262    fn test_linkrequest_data_no_mtu() {
263        let pub_bytes = [0xAAu8; 32];
264        let sig_pub_bytes = [0xBBu8; 32];
265        let data = build_linkrequest_data(&pub_bytes, &sig_pub_bytes, None, LinkMode::Aes256Cbc);
266        assert_eq!(data.len(), LINK_ECPUBSIZE);
267
268        let (p, s, mtu, mode) = parse_linkrequest_data(&data).unwrap();
269        assert_eq!(p, pub_bytes);
270        assert_eq!(s, sig_pub_bytes);
271        assert_eq!(mtu, None);
272        assert_eq!(mode, LinkMode::Aes256Cbc); // default when no signalling
273    }
274
275    #[test]
276    fn test_linkrequest_data_invalid_size() {
277        let data = [0u8; 10];
278        assert_eq!(parse_linkrequest_data(&data), Err(LinkError::InvalidData));
279    }
280
281    #[test]
282    fn test_compute_link_id_no_extra() {
283        let hashable = [0x42u8; 40];
284        let id = compute_link_id(&hashable, 0);
285        assert_eq!(id.len(), 16);
286    }
287
288    #[test]
289    fn test_compute_link_id_with_extra() {
290        let hashable = [0x42u8; 43]; // 40 base + 3 signalling
291        let id_with_extra = compute_link_id(&hashable, 3);
292        let id_base = compute_link_id(&hashable[..40], 0);
293        assert_eq!(id_with_extra, id_base);
294    }
295
296    #[test]
297    fn test_lrproof_sign_verify() {
298        let mut rng = FixedRng::new(&[0x11; 64]);
299        let sig_prv = Ed25519PrivateKey::generate(&mut rng);
300        let sig_pub = sig_prv.public_key();
301        let sig_pub_bytes = sig_pub.public_bytes();
302
303        let mut rng2 = FixedRng::new(&[0x22; 64]);
304        let x_prv = rns_crypto::x25519::X25519PrivateKey::generate(&mut rng2);
305        let pub_bytes = x_prv.public_key().public_bytes();
306
307        let link_id: LinkId = [0xAA; 16];
308        let mtu = Some(500u32);
309        let mode = LinkMode::Aes256Cbc;
310
311        let proof = build_lrproof(&link_id, &pub_bytes, &sig_pub_bytes, &sig_prv, mtu, mode);
312
313        let result = validate_lrproof(&proof, &link_id, &sig_pub, &sig_pub_bytes);
314        assert!(result.is_ok());
315        let (peer_pub, parsed_mtu, parsed_mode) = result.unwrap();
316        assert_eq!(peer_pub, pub_bytes);
317        assert_eq!(parsed_mtu, mtu);
318        assert_eq!(parsed_mode, mode);
319    }
320
321    #[test]
322    fn test_lrproof_wrong_link_id() {
323        let mut rng = FixedRng::new(&[0x11; 64]);
324        let sig_prv = Ed25519PrivateKey::generate(&mut rng);
325        let sig_pub = sig_prv.public_key();
326        let sig_pub_bytes = sig_pub.public_bytes();
327
328        let pub_bytes = [0x33u8; 32];
329        let link_id: LinkId = [0xAA; 16];
330        let wrong_id: LinkId = [0xBB; 16];
331
332        let proof = build_lrproof(
333            &link_id,
334            &pub_bytes,
335            &sig_pub_bytes,
336            &sig_prv,
337            None,
338            LinkMode::Aes256Cbc,
339        );
340        let result = validate_lrproof(&proof, &wrong_id, &sig_pub, &sig_pub_bytes);
341        assert_eq!(result, Err(LinkError::InvalidSignature));
342    }
343
344    #[test]
345    fn test_derive_session_key_aes128() {
346        let shared = [0x42u8; 32];
347        let link_id = [0xAA; 16];
348        let key = derive_session_key(&shared, &link_id, LinkMode::Aes128Cbc).unwrap();
349        assert_eq!(key.len(), 32);
350    }
351
352    #[test]
353    fn test_derive_session_key_aes256() {
354        let shared = [0x42u8; 32];
355        let link_id = [0xAA; 16];
356        let key = derive_session_key(&shared, &link_id, LinkMode::Aes256Cbc).unwrap();
357        assert_eq!(key.len(), 64);
358    }
359
360    #[test]
361    fn test_rtt_pack_unpack() {
362        let rtt = 0.123456789;
363        let packed = pack_rtt(rtt);
364        assert_eq!(packed.len(), 9);
365        assert_eq!(packed[0], 0xcb);
366        let unpacked = unpack_rtt(&packed).unwrap();
367        assert_eq!(unpacked, rtt);
368    }
369
370    #[test]
371    fn test_rtt_unpack_invalid() {
372        assert_eq!(unpack_rtt(&[0xcb, 0x00]), None);
373        assert_eq!(unpack_rtt(&[0xca, 0, 0, 0, 0, 0, 0, 0, 0]), None);
374    }
375
376    #[test]
377    fn test_rtt_pack_zero() {
378        let packed = pack_rtt(0.0);
379        let unpacked = unpack_rtt(&packed).unwrap();
380        assert_eq!(unpacked, 0.0);
381    }
382}