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)`.
62#[allow(clippy::type_complexity)]
63pub fn parse_linkrequest_data(
64    data: &[u8],
65) -> Result<([u8; 32], [u8; 32], Option<u32>, LinkMode), LinkError> {
66    if data.len() != LINK_ECPUBSIZE && data.len() != LINK_ECPUBSIZE + LINK_MTU_SIZE {
67        return Err(LinkError::InvalidData);
68    }
69
70    let mut x25519_pub = [0u8; 32];
71    let mut ed25519_pub = [0u8; 32];
72    x25519_pub.copy_from_slice(&data[..32]);
73    ed25519_pub.copy_from_slice(&data[32..64]);
74
75    if data.len() == LINK_ECPUBSIZE + LINK_MTU_SIZE {
76        let mut sig_bytes = [0u8; 3];
77        sig_bytes.copy_from_slice(&data[LINK_ECPUBSIZE..LINK_ECPUBSIZE + LINK_MTU_SIZE]);
78        let (mtu, mode) = parse_signalling_bytes(&sig_bytes)?;
79        Ok((x25519_pub, ed25519_pub, Some(mtu), mode))
80    } else {
81        Ok((x25519_pub, ed25519_pub, None, LinkMode::Aes256Cbc))
82    }
83}
84
85/// Stamp value carried by an incoming LINKREQUEST.
86///
87/// The X25519 public key is the stamp and the destination hash followed by
88/// the Ed25519 public key is the material, matching upstream link request
89/// stamping.
90pub fn link_request_stamp_value(
91    dest_hash: &[u8; 16],
92    data: &[u8],
93) -> Option<rns_crypto::stamp_value::StampValue> {
94    let (peer_pub, peer_sig_pub, _, _) = parse_linkrequest_data(data).ok()?;
95    let mut material = [0u8; 48];
96    material[..16].copy_from_slice(dest_hash);
97    material[16..].copy_from_slice(&peer_sig_pub);
98    Some(rns_crypto::identity::stamp_value(&material, &peer_pub))
99}
100
101/// Whether a LINKREQUEST's X25519 key meets `cost` as a stamp over
102/// `dest_hash || peer_sig_pub` (`stamp_valid(peer_pub_bytes, cost,
103/// stamp_material)`, `Link.validate_request` since upstream 796ef2d4), or
104/// `None` when the request does not parse.
105pub fn link_request_stamp_valid(
106    dest_hash: &[u8; 16],
107    data: &[u8],
108    cost: crate::stamp::StampCost,
109) -> Option<bool> {
110    let (peer_pub, peer_sig_pub, _, _) = parse_linkrequest_data(data).ok()?;
111    let mut material = [0u8; 48];
112    material[..16].copy_from_slice(dest_hash);
113    material[16..].copy_from_slice(&peer_sig_pub);
114    Some(crate::stamp::stamp_valid(&peer_pub, cost, &material))
115}
116
117/// Build LRPROOF data: `[signature:64][x25519_pub:32][signalling:0-3]`.
118///
119/// Signs: `link_id + pub_bytes + sig_pub_bytes + signalling_bytes`.
120pub fn build_lrproof(
121    link_id: &LinkId,
122    pub_bytes: &[u8; 32],
123    sig_pub_bytes: &[u8; 32],
124    sig_prv: &Ed25519PrivateKey,
125    mtu: Option<u32>,
126    mode: LinkMode,
127) -> Vec<u8> {
128    let signalling_bytes = if let Some(mtu_val) = mtu {
129        build_signalling_bytes(mtu_val, mode).to_vec()
130    } else {
131        Vec::new()
132    };
133
134    let mut signed_data = Vec::with_capacity(16 + 32 + 32 + signalling_bytes.len());
135    signed_data.extend_from_slice(link_id);
136    signed_data.extend_from_slice(pub_bytes);
137    signed_data.extend_from_slice(sig_pub_bytes);
138    signed_data.extend_from_slice(&signalling_bytes);
139
140    let signature = sig_prv.sign(&signed_data);
141
142    let mut proof_data = Vec::with_capacity(64 + 32 + signalling_bytes.len());
143    proof_data.extend_from_slice(&signature);
144    proof_data.extend_from_slice(pub_bytes);
145    proof_data.extend_from_slice(&signalling_bytes);
146
147    proof_data
148}
149
150/// Validate LRPROOF. Returns peer X25519 public key bytes on success.
151///
152/// Expects `proof_data = [signature:64][x25519_pub:32][signalling:0-3]`.
153/// Validates against `signed_data = link_id + peer_x25519_pub + peer_sig_pub + signalling`.
154pub fn validate_lrproof(
155    proof_data: &[u8],
156    link_id: &LinkId,
157    peer_sig_pub: &Ed25519PublicKey,
158    peer_sig_pub_bytes: &[u8; 32],
159) -> Result<([u8; 32], Option<u32>, LinkMode), LinkError> {
160    let sig_len = 64;
161    let pub_len = 32;
162
163    if proof_data.len() != sig_len + pub_len
164        && proof_data.len() != sig_len + pub_len + LINK_MTU_SIZE
165    {
166        return Err(LinkError::InvalidData);
167    }
168
169    let mut signature = [0u8; 64];
170    signature.copy_from_slice(&proof_data[..sig_len]);
171
172    let mut peer_pub = [0u8; 32];
173    peer_pub.copy_from_slice(&proof_data[sig_len..sig_len + pub_len]);
174
175    let signalling_bytes = &proof_data[sig_len + pub_len..];
176
177    let (mtu, mode) = if signalling_bytes.len() == LINK_MTU_SIZE {
178        let mut sb = [0u8; 3];
179        sb.copy_from_slice(signalling_bytes);
180        let (m, md) = parse_signalling_bytes(&sb)?;
181        (Some(m), md)
182    } else {
183        (None, LinkMode::Aes256Cbc)
184    };
185
186    let mut signed_data = Vec::with_capacity(16 + 32 + 32 + signalling_bytes.len());
187    signed_data.extend_from_slice(link_id);
188    signed_data.extend_from_slice(&peer_pub);
189    signed_data.extend_from_slice(peer_sig_pub_bytes);
190    signed_data.extend_from_slice(signalling_bytes);
191
192    if peer_sig_pub.verify(&signature, &signed_data) {
193        Ok((peer_pub, mtu, mode))
194    } else {
195        Err(LinkError::InvalidSignature)
196    }
197}
198
199/// Derive session key using HKDF.
200///
201/// `shared_key` is the raw ECDH output (32 bytes).
202/// Salt = link_id, context = None.
203/// Output length depends on mode: 32 for AES-128, 64 for AES-256.
204pub fn derive_session_key(
205    shared_key: &[u8; 32],
206    link_id: &LinkId,
207    mode: LinkMode,
208) -> Result<Vec<u8>, LinkError> {
209    let length = mode.derived_key_length();
210    hkdf(length, shared_key, Some(link_id), None).map_err(|_| LinkError::CryptoError)
211}
212
213/// Perform ECDH key exchange and derive session key.
214pub fn perform_key_exchange(
215    prv: &X25519PrivateKey,
216    peer_pub_bytes: &[u8; 32],
217    link_id: &LinkId,
218    mode: LinkMode,
219) -> Result<Vec<u8>, LinkError> {
220    let peer_pub = X25519PublicKey::from_bytes(peer_pub_bytes);
221    let shared_key = prv.exchange(&peer_pub);
222    derive_session_key(&shared_key, link_id, mode)
223}
224
225/// Pack RTT as msgpack float64: `0xcb` + 8 bytes big-endian.
226pub fn pack_rtt(rtt: f64) -> Vec<u8> {
227    let mut data = Vec::with_capacity(9);
228    data.push(0xcb);
229    data.extend_from_slice(&rtt.to_be_bytes());
230    data
231}
232
233/// Unpack RTT from msgpack float64.
234///
235/// Returns `None` for malformed input and for values that are not a finite,
236/// non-negative number of seconds.
237pub fn unpack_rtt(data: &[u8]) -> Option<f64> {
238    if data.len() == 9 && data[0] == 0xcb {
239        let mut bytes = [0u8; 8];
240        bytes.copy_from_slice(&data[1..9]);
241        let rtt = f64::from_be_bytes(bytes);
242        (rtt.is_finite() && rtt >= 0.0).then_some(rtt)
243    } else {
244        None
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use rns_crypto::FixedRng;
252
253    #[test]
254    fn test_signalling_bytes_roundtrip() {
255        let mtu = 500u32;
256        let mode = LinkMode::Aes256Cbc;
257        let bytes = build_signalling_bytes(mtu, mode);
258        let (parsed_mtu, parsed_mode) = parse_signalling_bytes(&bytes).unwrap();
259        assert_eq!(parsed_mtu, mtu);
260        assert_eq!(parsed_mode, mode);
261    }
262
263    #[test]
264    fn test_signalling_bytes_aes128() {
265        let mtu = 1234u32;
266        let mode = LinkMode::Aes128Cbc;
267        let bytes = build_signalling_bytes(mtu, mode);
268        let (parsed_mtu, parsed_mode) = parse_signalling_bytes(&bytes).unwrap();
269        assert_eq!(parsed_mtu, mtu);
270        assert_eq!(parsed_mode, mode);
271    }
272
273    #[test]
274    fn test_signalling_bytes_max_mtu() {
275        let mtu = LINK_MTU_BYTEMASK; // maximum 21-bit value
276        let mode = LinkMode::Aes256Cbc;
277        let bytes = build_signalling_bytes(mtu, mode);
278        let (parsed_mtu, parsed_mode) = parse_signalling_bytes(&bytes).unwrap();
279        assert_eq!(parsed_mtu, mtu);
280        assert_eq!(parsed_mode, mode);
281    }
282
283    #[test]
284    fn test_linkrequest_data_roundtrip() {
285        let pub_bytes = [0xAAu8; 32];
286        let sig_pub_bytes = [0xBBu8; 32];
287        let data =
288            build_linkrequest_data(&pub_bytes, &sig_pub_bytes, Some(500), LinkMode::Aes256Cbc);
289        assert_eq!(data.len(), LINK_ECPUBSIZE + LINK_MTU_SIZE);
290
291        let (p, s, mtu, mode) = parse_linkrequest_data(&data).unwrap();
292        assert_eq!(p, pub_bytes);
293        assert_eq!(s, sig_pub_bytes);
294        assert_eq!(mtu, Some(500));
295        assert_eq!(mode, LinkMode::Aes256Cbc);
296    }
297
298    #[test]
299    fn test_linkrequest_data_no_mtu() {
300        let pub_bytes = [0xAAu8; 32];
301        let sig_pub_bytes = [0xBBu8; 32];
302        let data = build_linkrequest_data(&pub_bytes, &sig_pub_bytes, None, LinkMode::Aes256Cbc);
303        assert_eq!(data.len(), LINK_ECPUBSIZE);
304
305        let (p, s, mtu, mode) = parse_linkrequest_data(&data).unwrap();
306        assert_eq!(p, pub_bytes);
307        assert_eq!(s, sig_pub_bytes);
308        assert_eq!(mtu, None);
309        assert_eq!(mode, LinkMode::Aes256Cbc); // default when no signalling
310    }
311
312    #[test]
313    fn test_linkrequest_data_invalid_size() {
314        let data = [0u8; 10];
315        assert_eq!(parse_linkrequest_data(&data), Err(LinkError::InvalidData));
316    }
317
318    #[test]
319    fn test_compute_link_id_no_extra() {
320        let hashable = [0x42u8; 40];
321        let id = compute_link_id(&hashable, 0);
322        assert_eq!(id.len(), 16);
323    }
324
325    #[test]
326    fn test_compute_link_id_with_extra() {
327        let hashable = [0x42u8; 43]; // 40 base + 3 signalling
328        let id_with_extra = compute_link_id(&hashable, 3);
329        let id_base = compute_link_id(&hashable[..40], 0);
330        assert_eq!(id_with_extra, id_base);
331    }
332
333    #[test]
334    fn test_lrproof_sign_verify() {
335        let mut rng = FixedRng::new(&[0x11; 64]);
336        let sig_prv = Ed25519PrivateKey::generate(&mut rng);
337        let sig_pub = sig_prv.public_key();
338        let sig_pub_bytes = sig_pub.public_bytes();
339
340        let mut rng2 = FixedRng::new(&[0x22; 64]);
341        let x_prv = rns_crypto::x25519::X25519PrivateKey::generate(&mut rng2);
342        let pub_bytes = x_prv.public_key().public_bytes();
343
344        let link_id: LinkId = [0xAA; 16];
345        let mtu = Some(500u32);
346        let mode = LinkMode::Aes256Cbc;
347
348        let proof = build_lrproof(&link_id, &pub_bytes, &sig_pub_bytes, &sig_prv, mtu, mode);
349
350        let result = validate_lrproof(&proof, &link_id, &sig_pub, &sig_pub_bytes);
351        assert!(result.is_ok());
352        let (peer_pub, parsed_mtu, parsed_mode) = result.unwrap();
353        assert_eq!(peer_pub, pub_bytes);
354        assert_eq!(parsed_mtu, mtu);
355        assert_eq!(parsed_mode, mode);
356    }
357
358    #[test]
359    fn test_lrproof_wrong_link_id() {
360        let mut rng = FixedRng::new(&[0x11; 64]);
361        let sig_prv = Ed25519PrivateKey::generate(&mut rng);
362        let sig_pub = sig_prv.public_key();
363        let sig_pub_bytes = sig_pub.public_bytes();
364
365        let pub_bytes = [0x33u8; 32];
366        let link_id: LinkId = [0xAA; 16];
367        let wrong_id: LinkId = [0xBB; 16];
368
369        let proof = build_lrproof(
370            &link_id,
371            &pub_bytes,
372            &sig_pub_bytes,
373            &sig_prv,
374            None,
375            LinkMode::Aes256Cbc,
376        );
377        let result = validate_lrproof(&proof, &wrong_id, &sig_pub, &sig_pub_bytes);
378        assert_eq!(result, Err(LinkError::InvalidSignature));
379    }
380
381    #[test]
382    fn test_derive_session_key_aes128() {
383        let shared = [0x42u8; 32];
384        let link_id = [0xAA; 16];
385        let key = derive_session_key(&shared, &link_id, LinkMode::Aes128Cbc).unwrap();
386        assert_eq!(key.len(), 32);
387    }
388
389    #[test]
390    fn test_derive_session_key_aes256() {
391        let shared = [0x42u8; 32];
392        let link_id = [0xAA; 16];
393        let key = derive_session_key(&shared, &link_id, LinkMode::Aes256Cbc).unwrap();
394        assert_eq!(key.len(), 64);
395    }
396
397    #[test]
398    fn test_rtt_pack_unpack() {
399        let rtt = 0.123456789;
400        let packed = pack_rtt(rtt);
401        assert_eq!(packed.len(), 9);
402        assert_eq!(packed[0], 0xcb);
403        let unpacked = unpack_rtt(&packed).unwrap();
404        assert_eq!(unpacked, rtt);
405    }
406
407    #[test]
408    fn test_rtt_unpack_invalid() {
409        assert_eq!(unpack_rtt(&[0xcb, 0x00]), None);
410        assert_eq!(unpack_rtt(&[0xca, 0, 0, 0, 0, 0, 0, 0, 0]), None);
411    }
412
413    #[test]
414    fn test_rtt_pack_zero() {
415        let packed = pack_rtt(0.0);
416        let unpacked = unpack_rtt(&packed).unwrap();
417        assert_eq!(unpacked, 0.0);
418    }
419
420    #[test]
421    fn unpack_rtt_rejects_non_finite_and_negative_values() {
422        for bad in [
423            f64::NAN,
424            f64::INFINITY,
425            f64::NEG_INFINITY,
426            -1.0,
427            -f64::MIN_POSITIVE,
428        ] {
429            let mut data = vec![0xcb];
430            data.extend_from_slice(&bad.to_be_bytes());
431            assert_eq!(unpack_rtt(&data), None, "rtt {bad}");
432        }
433        assert_eq!(unpack_rtt(&pack_rtt(0.0)), Some(0.0));
434        assert_eq!(unpack_rtt(&pack_rtt(-0.0)), Some(-0.0));
435        assert_eq!(unpack_rtt(&pack_rtt(2.5)), Some(2.5));
436    }
437}