Skip to main content

rns_core/
announce.rs

1use alloc::vec::Vec;
2use core::fmt;
3
4use rns_crypto::identity::Identity;
5
6use crate::constants;
7use crate::hash;
8
9#[derive(Debug)]
10pub enum AnnounceError {
11    DataTooShort,
12    InvalidSignature,
13    DestinationMismatch,
14    SigningError,
15}
16
17impl fmt::Display for AnnounceError {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            AnnounceError::DataTooShort => write!(f, "Announce data too short"),
21            AnnounceError::InvalidSignature => write!(f, "Invalid announce signature"),
22            AnnounceError::DestinationMismatch => write!(f, "Destination hash mismatch"),
23            AnnounceError::SigningError => write!(f, "Could not sign announce"),
24        }
25    }
26}
27
28/// Parsed announce data.
29///
30/// Layout without ratchet:
31/// ```text
32/// [public_key:64][name_hash:10][random_hash:10][signature:64][app_data:*]
33/// ```
34///
35/// Layout with ratchet (context_flag == FLAG_SET):
36/// ```text
37/// [public_key:64][name_hash:10][random_hash:10][ratchet:32][signature:64][app_data:*]
38/// ```
39#[derive(Debug, Clone)]
40pub struct AnnounceData {
41    pub public_key: [u8; 64],
42    pub name_hash: [u8; 10],
43    pub random_hash: [u8; 10],
44    pub ratchet: Option<[u8; 32]>,
45    pub signature: [u8; 64],
46    pub app_data: Option<Vec<u8>>,
47}
48
49/// Result of a successfully validated announce.
50#[derive(Debug)]
51pub struct ValidatedAnnounce {
52    pub identity_hash: [u8; 16],
53    pub public_key: [u8; 64],
54    pub name_hash: [u8; 10],
55    pub random_hash: [u8; 10],
56    pub ratchet: Option<[u8; 32]>,
57    pub app_data: Option<Vec<u8>>,
58}
59
60impl AnnounceData {
61    /// Pack announce fields into bytes, signing with the provided identity.
62    ///
63    /// Returns (announce_data_bytes, has_ratchet).
64    pub fn pack(
65        identity: &Identity,
66        destination_hash: &[u8; 16],
67        name_hash: &[u8; 10],
68        random_hash: &[u8; 10],
69        ratchet: Option<&[u8; 32]>,
70        app_data: Option<&[u8]>,
71    ) -> Result<(Vec<u8>, bool), AnnounceError> {
72        let public_key = identity
73            .get_public_key()
74            .ok_or(AnnounceError::SigningError)?;
75
76        // Build signed data: destination_hash + public_key + name_hash + random_hash + ratchet + app_data
77        let mut signed_data = Vec::new();
78        signed_data.extend_from_slice(destination_hash);
79        signed_data.extend_from_slice(&public_key);
80        signed_data.extend_from_slice(name_hash);
81        signed_data.extend_from_slice(random_hash);
82
83        let has_ratchet = ratchet.is_some();
84        if let Some(r) = ratchet {
85            signed_data.extend_from_slice(r);
86        }
87        if let Some(ad) = app_data {
88            signed_data.extend_from_slice(ad);
89        }
90
91        let signature = identity
92            .sign(&signed_data)
93            .map_err(|_| AnnounceError::SigningError)?;
94
95        // Build announce data: public_key + name_hash + random_hash + [ratchet] + signature + [app_data]
96        let mut announce_data = Vec::new();
97        announce_data.extend_from_slice(&public_key);
98        announce_data.extend_from_slice(name_hash);
99        announce_data.extend_from_slice(random_hash);
100        if let Some(r) = ratchet {
101            announce_data.extend_from_slice(r);
102        }
103        announce_data.extend_from_slice(&signature);
104        if let Some(ad) = app_data {
105            announce_data.extend_from_slice(ad);
106        }
107
108        Ok((announce_data, has_ratchet))
109    }
110
111    /// Parse announce bytes into structured data.
112    ///
113    /// `has_ratchet` corresponds to context_flag == FLAG_SET.
114    pub fn unpack(data: &[u8], has_ratchet: bool) -> Result<Self, AnnounceError> {
115        let keysize = constants::KEYSIZE / 8; // 64
116        let name_hash_len = constants::NAME_HASH_LENGTH / 8; // 10
117        let sig_len = constants::SIGLENGTH / 8; // 64
118        let ratchet_size = constants::RATCHETSIZE / 8; // 32
119
120        let min_len = if has_ratchet {
121            keysize + name_hash_len + 10 + ratchet_size + sig_len
122        } else {
123            keysize + name_hash_len + 10 + sig_len
124        };
125
126        if data.len() < min_len {
127            return Err(AnnounceError::DataTooShort);
128        }
129
130        let mut public_key = [0u8; 64];
131        public_key.copy_from_slice(&data[..keysize]);
132
133        let mut name_hash = [0u8; 10];
134        name_hash.copy_from_slice(&data[keysize..keysize + name_hash_len]);
135
136        let mut random_hash = [0u8; 10];
137        random_hash.copy_from_slice(&data[keysize + name_hash_len..keysize + name_hash_len + 10]);
138
139        let (ratchet, sig_start) = if has_ratchet {
140            let mut ratchet = [0u8; 32];
141            ratchet.copy_from_slice(
142                &data[keysize + name_hash_len + 10..keysize + name_hash_len + 10 + ratchet_size],
143            );
144            (Some(ratchet), keysize + name_hash_len + 10 + ratchet_size)
145        } else {
146            (None, keysize + name_hash_len + 10)
147        };
148
149        let mut signature = [0u8; 64];
150        signature.copy_from_slice(&data[sig_start..sig_start + sig_len]);
151
152        // Determine app_data
153        // From Python: app_data is present if len(data) > keysize + name_hash_len + 10 + sig_len [+ ratchetsize]
154        let app_data_start = sig_start + sig_len;
155        let app_data = if data.len() > app_data_start {
156            Some(data[app_data_start..].to_vec())
157        } else {
158            None
159        };
160
161        Ok(AnnounceData {
162            public_key,
163            name_hash,
164            random_hash,
165            ratchet,
166            signature,
167            app_data,
168        })
169    }
170
171    /// Validate an announce: verify signature and check destination hash.
172    ///
173    /// Follows Python Identity.validate_announce():
174    /// 1. Create Identity from public_key
175    /// 2. Build signed_data = destination_hash + public_key + name_hash + random_hash + ratchet + app_data
176    /// 3. Verify signature over signed_data
177    /// 4. Compute identity_hash = truncated_hash(public_key)
178    /// 5. expected_hash = truncated_hash(name_hash || identity_hash)
179    /// 6. Verify expected_hash == destination_hash
180    pub fn validate(
181        &self,
182        destination_hash: &[u8; 16],
183    ) -> Result<ValidatedAnnounce, AnnounceError> {
184        // Create identity from public key
185        let announced_identity = Identity::from_public_key(&self.public_key);
186
187        // Build signed data
188        let mut signed_data = Vec::new();
189        signed_data.extend_from_slice(destination_hash);
190        signed_data.extend_from_slice(&self.public_key);
191        signed_data.extend_from_slice(&self.name_hash);
192        signed_data.extend_from_slice(&self.random_hash);
193
194        if let Some(ref ratchet) = self.ratchet {
195            signed_data.extend_from_slice(ratchet);
196        }
197
198        // When building signed_data, use app_data bytes (or empty if None)
199        // Python uses app_data = b"" for building signed_data when no app_data
200        if let Some(ref ad) = self.app_data {
201            signed_data.extend_from_slice(ad);
202        }
203
204        // Verify signature
205        if !announced_identity.verify(&self.signature, &signed_data) {
206            return Err(AnnounceError::InvalidSignature);
207        }
208
209        // Verify destination hash
210        let identity_hash = *announced_identity.hash();
211
212        let mut hash_material = Vec::new();
213        hash_material.extend_from_slice(&self.name_hash);
214        hash_material.extend_from_slice(&identity_hash);
215
216        let expected_hash = hash::truncated_hash(&hash_material);
217
218        if &expected_hash != destination_hash {
219            return Err(AnnounceError::DestinationMismatch);
220        }
221
222        Ok(ValidatedAnnounce {
223            identity_hash,
224            public_key: self.public_key,
225            name_hash: self.name_hash,
226            random_hash: self.random_hash,
227            ratchet: self.ratchet,
228            app_data: self.app_data.clone(),
229        })
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::destination;
237
238    #[test]
239    fn test_pack_unpack_roundtrip_no_ratchet() {
240        let identity = Identity::from_private_key(&[0x42; 64]);
241        let id_hash = *identity.hash();
242
243        let nh = destination::name_hash("testapp", &["aspect"]);
244        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
245        let random = [0xAA; 10];
246
247        let (data, has_ratchet) =
248            AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
249        assert!(!has_ratchet);
250
251        let parsed = AnnounceData::unpack(&data, false).unwrap();
252        assert_eq!(parsed.public_key, identity.get_public_key().unwrap());
253        assert_eq!(parsed.name_hash, nh);
254        assert_eq!(parsed.random_hash, random);
255        assert!(parsed.ratchet.is_none());
256        assert!(parsed.app_data.is_none());
257    }
258
259    #[test]
260    fn test_pack_unpack_roundtrip_with_ratchet() {
261        let identity = Identity::from_private_key(&[0x42; 64]);
262        let id_hash = *identity.hash();
263
264        let nh = destination::name_hash("testapp", &["aspect"]);
265        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
266        let random = [0xBB; 10];
267        let ratchet = [0xCC; 32];
268
269        let (data, has_ratchet) =
270            AnnounceData::pack(&identity, &dh, &nh, &random, Some(&ratchet), None).unwrap();
271        assert!(has_ratchet);
272
273        let parsed = AnnounceData::unpack(&data, true).unwrap();
274        assert_eq!(parsed.ratchet.unwrap(), ratchet);
275    }
276
277    #[test]
278    fn test_pack_unpack_with_app_data() {
279        let identity = Identity::from_private_key(&[0x42; 64]);
280        let id_hash = *identity.hash();
281
282        let nh = destination::name_hash("testapp", &["aspect"]);
283        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
284        let random = [0xDD; 10];
285        let app_data = b"hello app data";
286
287        let (data, _) =
288            AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(app_data)).unwrap();
289
290        let parsed = AnnounceData::unpack(&data, false).unwrap();
291        assert_eq!(parsed.app_data.as_deref(), Some(app_data.as_slice()));
292    }
293
294    #[test]
295    fn test_validate_valid_announce() {
296        let identity = Identity::from_private_key(&[0x42; 64]);
297        let id_hash = *identity.hash();
298
299        let nh = destination::name_hash("testapp", &["aspect"]);
300        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
301        let random = [0xEE; 10];
302
303        let (data, _) =
304            AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(b"data")).unwrap();
305
306        let parsed = AnnounceData::unpack(&data, false).unwrap();
307        let validated = parsed.validate(&dh).unwrap();
308
309        assert_eq!(validated.identity_hash, id_hash);
310        assert_eq!(validated.name_hash, nh);
311    }
312
313    #[test]
314    fn test_validate_tampered_signature() {
315        let identity = Identity::from_private_key(&[0x42; 64]);
316        let id_hash = *identity.hash();
317
318        let nh = destination::name_hash("testapp", &["aspect"]);
319        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
320        let random = [0xFF; 10];
321
322        let (mut data, _) =
323            AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
324
325        // Tamper with the signature (located at offset 84 = 64 + 10 + 10)
326        data[84] ^= 0xFF;
327
328        let parsed = AnnounceData::unpack(&data, false).unwrap();
329        assert!(parsed.validate(&dh).is_err());
330    }
331
332    #[test]
333    fn test_validate_wrong_destination_hash() {
334        let identity = Identity::from_private_key(&[0x42; 64]);
335        let id_hash = *identity.hash();
336
337        let nh = destination::name_hash("testapp", &["aspect"]);
338        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
339        let random = [0x11; 10];
340
341        let (data, _) =
342            AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
343
344        let parsed = AnnounceData::unpack(&data, false).unwrap();
345        let wrong_hash = [0x00; 16];
346        assert!(parsed.validate(&wrong_hash).is_err());
347    }
348}