Skip to main content

rns_core/
announce.rs

1use alloc::vec::Vec;
2use core::fmt;
3
4use rns_crypto::identity::Identity;
5use rns_crypto::sha256::Sha256;
6
7use crate::constants;
8use crate::hash;
9
10#[derive(Debug)]
11pub enum AnnounceError {
12    DataTooShort,
13    InvalidSignature,
14    DestinationMismatch,
15    SigningError,
16}
17
18impl fmt::Display for AnnounceError {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            AnnounceError::DataTooShort => write!(f, "Announce data too short"),
22            AnnounceError::InvalidSignature => write!(f, "Invalid announce signature"),
23            AnnounceError::DestinationMismatch => write!(f, "Destination hash mismatch"),
24            AnnounceError::SigningError => write!(f, "Could not sign announce"),
25        }
26    }
27}
28
29/// Parsed announce data.
30///
31/// Layout without ratchet:
32/// ```text
33/// [public_key:64][name_hash:10][random_hash:10][signature:64][app_data:*]
34/// ```
35///
36/// Layout with ratchet (context_flag == FLAG_SET):
37/// ```text
38/// [public_key:64][name_hash:10][random_hash:10][ratchet:32][signature:64][app_data:*]
39/// ```
40#[derive(Debug, Clone)]
41pub struct AnnounceData {
42    pub public_key: [u8; 64],
43    pub name_hash: [u8; 10],
44    pub random_hash: [u8; 10],
45    pub ratchet: Option<[u8; 32]>,
46    pub signature: [u8; 64],
47    pub app_data: Option<Vec<u8>>,
48}
49
50/// Result of a successfully validated announce.
51#[derive(Debug)]
52pub struct ValidatedAnnounce {
53    pub identity_hash: [u8; 16],
54    pub public_key: [u8; 64],
55    pub name_hash: [u8; 10],
56    pub random_hash: [u8; 10],
57    pub ratchet: Option<[u8; 32]>,
58    pub app_data: Option<Vec<u8>>,
59}
60
61impl AnnounceData {
62    /// Pack announce fields into bytes, signing with the provided identity.
63    ///
64    /// Returns (announce_data_bytes, has_ratchet).
65    pub fn pack(
66        identity: &Identity,
67        destination_hash: &[u8; 16],
68        name_hash: &[u8; 10],
69        random_hash: &[u8; 10],
70        ratchet: Option<&[u8; 32]>,
71        app_data: Option<&[u8]>,
72    ) -> Result<(Vec<u8>, bool), AnnounceError> {
73        let public_key = identity
74            .get_public_key()
75            .ok_or(AnnounceError::SigningError)?;
76
77        // Build signed data: destination_hash + public_key + name_hash + random_hash + ratchet + app_data
78        let mut signed_data = Vec::new();
79        signed_data.extend_from_slice(destination_hash);
80        signed_data.extend_from_slice(&public_key);
81        signed_data.extend_from_slice(name_hash);
82        signed_data.extend_from_slice(random_hash);
83
84        let has_ratchet = ratchet.is_some();
85        if let Some(r) = ratchet {
86            signed_data.extend_from_slice(r);
87        }
88        if let Some(ad) = app_data {
89            signed_data.extend_from_slice(ad);
90        }
91
92        let signature = identity
93            .sign(&signed_data)
94            .map_err(|_| AnnounceError::SigningError)?;
95
96        // Build announce data: public_key + name_hash + random_hash + [ratchet] + signature + [app_data]
97        let mut announce_data = Vec::new();
98        announce_data.extend_from_slice(&public_key);
99        announce_data.extend_from_slice(name_hash);
100        announce_data.extend_from_slice(random_hash);
101        if let Some(r) = ratchet {
102            announce_data.extend_from_slice(r);
103        }
104        announce_data.extend_from_slice(&signature);
105        if let Some(ad) = app_data {
106            announce_data.extend_from_slice(ad);
107        }
108
109        Ok((announce_data, has_ratchet))
110    }
111
112    /// Parse announce bytes into structured data.
113    ///
114    /// `has_ratchet` corresponds to context_flag == FLAG_SET.
115    pub fn unpack(data: &[u8], has_ratchet: bool) -> Result<Self, AnnounceError> {
116        let keysize = constants::KEYSIZE / 8; // 64
117        let name_hash_len = constants::NAME_HASH_LENGTH / 8; // 10
118        let sig_len = constants::SIGLENGTH / 8; // 64
119        let ratchet_size = constants::RATCHETSIZE / 8; // 32
120
121        let min_len = if has_ratchet {
122            keysize + name_hash_len + 10 + ratchet_size + sig_len
123        } else {
124            keysize + name_hash_len + 10 + sig_len
125        };
126
127        if data.len() < min_len {
128            return Err(AnnounceError::DataTooShort);
129        }
130
131        let mut public_key = [0u8; 64];
132        public_key.copy_from_slice(&data[..keysize]);
133
134        let mut name_hash = [0u8; 10];
135        name_hash.copy_from_slice(&data[keysize..keysize + name_hash_len]);
136
137        let mut random_hash = [0u8; 10];
138        random_hash.copy_from_slice(&data[keysize + name_hash_len..keysize + name_hash_len + 10]);
139
140        let (ratchet, sig_start) = if has_ratchet {
141            let mut ratchet = [0u8; 32];
142            ratchet.copy_from_slice(
143                &data[keysize + name_hash_len + 10..keysize + name_hash_len + 10 + ratchet_size],
144            );
145            (Some(ratchet), keysize + name_hash_len + 10 + ratchet_size)
146        } else {
147            (None, keysize + name_hash_len + 10)
148        };
149
150        let mut signature = [0u8; 64];
151        signature.copy_from_slice(&data[sig_start..sig_start + sig_len]);
152
153        // Determine app_data
154        // From Python: app_data is present if len(data) > keysize + name_hash_len + 10 + sig_len [+ ratchetsize]
155        let app_data_start = sig_start + sig_len;
156        let app_data = if data.len() > app_data_start {
157            Some(data[app_data_start..].to_vec())
158        } else {
159            None
160        };
161
162        Ok(AnnounceData {
163            public_key,
164            name_hash,
165            random_hash,
166            ratchet,
167            signature,
168            app_data,
169        })
170    }
171
172    /// Feed each part of the signed data, in order, to `f`:
173    /// destination_hash + public_key + name_hash + random_hash + ratchet + app_data.
174    ///
175    /// Python builds signed_data with `app_data = b""` when there is no app data,
176    /// so absent optional fields contribute nothing.
177    fn for_each_signed_part(&self, destination_hash: &[u8; 16], mut f: impl FnMut(&[u8])) {
178        f(destination_hash);
179        f(&self.public_key);
180        f(&self.name_hash);
181        f(&self.random_hash);
182        if let Some(ref ratchet) = self.ratchet {
183            f(ratchet);
184        }
185        if let Some(ref ad) = self.app_data {
186            f(ad);
187        }
188    }
189
190    /// The bytes covered by the announce signature.
191    fn signed_data(&self, destination_hash: &[u8; 16]) -> Vec<u8> {
192        let mut signed_data = Vec::new();
193        self.for_each_signed_part(destination_hash, |part| signed_data.extend_from_slice(part));
194        signed_data
195    }
196
197    /// Key for the announce signature cache: SHA-256 over the signed data
198    /// followed by the signature.
199    ///
200    /// Two announces share a key only when every signed field and the
201    /// signature are identical, so a cached verification result applies to
202    /// exactly the announce it was computed for.
203    pub fn signature_cache_key(&self, destination_hash: &[u8; 16]) -> [u8; 32] {
204        let mut hasher = Sha256::new();
205        self.for_each_signed_part(destination_hash, |part| hasher.update(part));
206        hasher.update(&self.signature);
207        hasher.digest()
208    }
209
210    /// Check that `destination_hash == truncated_hash(name_hash || identity_hash)`.
211    fn check_destination_binding(
212        &self,
213        destination_hash: &[u8; 16],
214        identity_hash: &[u8; 16],
215    ) -> Result<(), AnnounceError> {
216        let mut hash_material = [0u8; 26];
217        hash_material[..10].copy_from_slice(&self.name_hash);
218        hash_material[10..].copy_from_slice(identity_hash);
219
220        let expected_hash = hash::truncated_hash(&hash_material);
221
222        if &expected_hash != destination_hash {
223            return Err(AnnounceError::DestinationMismatch);
224        }
225        Ok(())
226    }
227
228    fn to_validated(&self, identity_hash: [u8; 16]) -> ValidatedAnnounce {
229        ValidatedAnnounce {
230            identity_hash,
231            public_key: self.public_key,
232            name_hash: self.name_hash,
233            random_hash: self.random_hash,
234            ratchet: self.ratchet,
235            app_data: self.app_data.clone(),
236        }
237    }
238
239    /// Validate an announce: verify signature and check destination hash.
240    ///
241    /// Follows Python Identity.validate_announce():
242    /// 1. Create Identity from public_key
243    /// 2. Build signed_data = destination_hash + public_key + name_hash + random_hash + ratchet + app_data
244    /// 3. Verify signature over signed_data
245    /// 4. Compute identity_hash = truncated_hash(public_key)
246    /// 5. expected_hash = truncated_hash(name_hash || identity_hash)
247    /// 6. Verify expected_hash == destination_hash
248    pub fn validate(
249        &self,
250        destination_hash: &[u8; 16],
251    ) -> Result<ValidatedAnnounce, AnnounceError> {
252        // Create identity from public key. Key bytes that do not decode to a
253        // valid Ed25519 point cannot carry a valid signature, so the announce
254        // is rejected the same way (Python returns False from validate_announce).
255        let announced_identity = Identity::from_public_key(&self.public_key)
256            .map_err(|_| AnnounceError::InvalidSignature)?;
257
258        // Verify signature
259        let signed_data = self.signed_data(destination_hash);
260        if !announced_identity.verify(&self.signature, &signed_data) {
261            return Err(AnnounceError::InvalidSignature);
262        }
263
264        // Verify destination hash
265        let identity_hash = *announced_identity.hash();
266        self.check_destination_binding(destination_hash, &identity_hash)?;
267
268        Ok(self.to_validated(identity_hash))
269    }
270
271    /// Build a `ValidatedAnnounce` for an announce whose signature cache key
272    /// (see [`Self::signature_cache_key`]) is already in the signature cache.
273    ///
274    /// The signature was verified for an identical announce earlier; the
275    /// structural checks are repeated here: the identity hash is recomputed
276    /// from `public_key` and the destination hash binding is checked. The
277    /// public key decoded successfully when that earlier announce was
278    /// verified, since the cache key covers the key bytes.
279    pub fn to_validated_with_cached_signature(
280        &self,
281        destination_hash: &[u8; 16],
282    ) -> Result<ValidatedAnnounce, AnnounceError> {
283        let identity_hash = hash::truncated_hash(&self.public_key);
284        self.check_destination_binding(destination_hash, &identity_hash)?;
285        Ok(self.to_validated(identity_hash))
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use crate::destination;
293
294    #[test]
295    fn test_pack_unpack_roundtrip_no_ratchet() {
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 = [0xAA; 10];
302
303        let (data, has_ratchet) =
304            AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
305        assert!(!has_ratchet);
306
307        let parsed = AnnounceData::unpack(&data, false).unwrap();
308        assert_eq!(parsed.public_key, identity.get_public_key().unwrap());
309        assert_eq!(parsed.name_hash, nh);
310        assert_eq!(parsed.random_hash, random);
311        assert!(parsed.ratchet.is_none());
312        assert!(parsed.app_data.is_none());
313    }
314
315    #[test]
316    fn test_pack_unpack_roundtrip_with_ratchet() {
317        let identity = Identity::from_private_key(&[0x42; 64]);
318        let id_hash = *identity.hash();
319
320        let nh = destination::name_hash("testapp", &["aspect"]);
321        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
322        let random = [0xBB; 10];
323        let ratchet = [0xCC; 32];
324
325        let (data, has_ratchet) =
326            AnnounceData::pack(&identity, &dh, &nh, &random, Some(&ratchet), None).unwrap();
327        assert!(has_ratchet);
328
329        let parsed = AnnounceData::unpack(&data, true).unwrap();
330        assert_eq!(parsed.ratchet.unwrap(), ratchet);
331    }
332
333    #[test]
334    fn test_pack_unpack_with_app_data() {
335        let identity = Identity::from_private_key(&[0x42; 64]);
336        let id_hash = *identity.hash();
337
338        let nh = destination::name_hash("testapp", &["aspect"]);
339        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
340        let random = [0xDD; 10];
341        let app_data = b"hello app data";
342
343        let (data, _) =
344            AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(app_data)).unwrap();
345
346        let parsed = AnnounceData::unpack(&data, false).unwrap();
347        assert_eq!(parsed.app_data.as_deref(), Some(app_data.as_slice()));
348    }
349
350    #[test]
351    fn test_validate_valid_announce() {
352        let identity = Identity::from_private_key(&[0x42; 64]);
353        let id_hash = *identity.hash();
354
355        let nh = destination::name_hash("testapp", &["aspect"]);
356        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
357        let random = [0xEE; 10];
358
359        let (data, _) =
360            AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(b"data")).unwrap();
361
362        let parsed = AnnounceData::unpack(&data, false).unwrap();
363        let validated = parsed.validate(&dh).unwrap();
364
365        assert_eq!(validated.identity_hash, id_hash);
366        assert_eq!(validated.name_hash, nh);
367    }
368
369    #[test]
370    fn test_validate_tampered_signature() {
371        let identity = Identity::from_private_key(&[0x42; 64]);
372        let id_hash = *identity.hash();
373
374        let nh = destination::name_hash("testapp", &["aspect"]);
375        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
376        let random = [0xFF; 10];
377
378        let (mut data, _) = AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
379
380        // Tamper with the signature (located at offset 84 = 64 + 10 + 10)
381        data[84] ^= 0xFF;
382
383        let parsed = AnnounceData::unpack(&data, false).unwrap();
384        assert!(parsed.validate(&dh).is_err());
385    }
386
387    #[test]
388    fn test_validate_rejects_announce_with_invalid_signing_key() {
389        let identity = Identity::from_private_key(&[0x42; 64]);
390        let id_hash = *identity.hash();
391
392        let nh = destination::name_hash("testapp", &["aspect"]);
393        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
394        let random = [0x22; 10];
395
396        let (mut data, _) = AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
397
398        // Replace the Ed25519 half of the public key (bytes 32..64) with
399        // y = 2, which is not a valid compressed point encoding.
400        data[32..64].copy_from_slice(&[0; 32]);
401        data[32] = 2;
402
403        let parsed = AnnounceData::unpack(&data, false).unwrap();
404        assert!(matches!(
405            parsed.validate(&dh),
406            Err(AnnounceError::InvalidSignature)
407        ));
408    }
409
410    #[test]
411    fn test_validate_wrong_destination_hash() {
412        let identity = Identity::from_private_key(&[0x42; 64]);
413        let id_hash = *identity.hash();
414
415        let nh = destination::name_hash("testapp", &["aspect"]);
416        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
417        let random = [0x11; 10];
418
419        let (data, _) = AnnounceData::pack(&identity, &dh, &nh, &random, None, None).unwrap();
420
421        let parsed = AnnounceData::unpack(&data, false).unwrap();
422        let wrong_hash = [0x00; 16];
423        assert!(parsed.validate(&wrong_hash).is_err());
424    }
425
426    #[test]
427    fn test_to_validated_with_cached_signature_matches_validate() {
428        let identity = Identity::from_private_key(&[0x42; 64]);
429        let id_hash = *identity.hash();
430
431        let nh = destination::name_hash("testapp", &["aspect"]);
432        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
433        let random = [0xAB; 10];
434
435        let (data, _) =
436            AnnounceData::pack(&identity, &dh, &nh, &random, None, Some(b"appdata")).unwrap();
437
438        let parsed = AnnounceData::unpack(&data, false).unwrap();
439        let validated = parsed.validate(&dh).unwrap();
440        let unchecked = parsed.to_validated_with_cached_signature(&dh).unwrap();
441
442        assert_eq!(validated.identity_hash, unchecked.identity_hash);
443        assert_eq!(validated.public_key, unchecked.public_key);
444        assert_eq!(validated.name_hash, unchecked.name_hash);
445        assert_eq!(validated.random_hash, unchecked.random_hash);
446        assert_eq!(validated.ratchet, unchecked.ratchet);
447        assert_eq!(validated.app_data, unchecked.app_data);
448    }
449    fn signed_announce(
450        app_data: Option<&[u8]>,
451        ratchet: Option<&[u8; 32]>,
452    ) -> (AnnounceData, [u8; 16]) {
453        let identity = Identity::from_private_key(&[0x42; 64]);
454        let id_hash = *identity.hash();
455        let nh = destination::name_hash("testapp", &["aspect"]);
456        let dh = destination::destination_hash("testapp", &["aspect"], Some(&id_hash));
457        let (data, has_ratchet) =
458            AnnounceData::pack(&identity, &dh, &nh, &[0x31; 10], ratchet, app_data).unwrap();
459        (AnnounceData::unpack(&data, has_ratchet).unwrap(), dh)
460    }
461
462    #[test]
463    fn test_cached_signature_path_checks_destination_binding() {
464        let (parsed, dh) = signed_announce(None, None);
465        let mut other_dh = dh;
466        other_dh[0] ^= 0x01;
467        assert!(matches!(
468            parsed.to_validated_with_cached_signature(&other_dh),
469            Err(AnnounceError::DestinationMismatch)
470        ));
471
472        let other = Identity::from_private_key(&[0x43; 64]);
473        let mut swapped = parsed.clone();
474        swapped.public_key = other.get_public_key().unwrap();
475        assert!(matches!(
476            swapped.to_validated_with_cached_signature(&dh),
477            Err(AnnounceError::DestinationMismatch)
478        ));
479    }
480
481    #[test]
482    fn test_signature_cache_key_covers_every_signed_field() {
483        let ratchet = [0x44; 32];
484        let (base, dh) = signed_announce(Some(b"app"), Some(&ratchet));
485        let key = base.signature_cache_key(&dh);
486        assert_eq!(key, base.clone().signature_cache_key(&dh));
487
488        let mut other_dh = dh;
489        other_dh[15] ^= 0x01;
490        assert_ne!(key, base.signature_cache_key(&other_dh));
491
492        let mut variants = Vec::new();
493        let mut v = base.clone();
494        v.public_key[0] ^= 0x01;
495        variants.push(v);
496        let mut v = base.clone();
497        v.name_hash[0] ^= 0x01;
498        variants.push(v);
499        let mut v = base.clone();
500        v.random_hash[0] ^= 0x01;
501        variants.push(v);
502        let mut v = base.clone();
503        v.ratchet = None;
504        variants.push(v);
505        let mut v = base.clone();
506        v.ratchet.as_mut().unwrap()[0] ^= 0x01;
507        variants.push(v);
508        let mut v = base.clone();
509        v.app_data = None;
510        variants.push(v);
511        let mut v = base.clone();
512        v.app_data = Some(b"apq".to_vec());
513        variants.push(v);
514        let mut v = base.clone();
515        v.signature[0] ^= 0x01;
516        variants.push(v);
517
518        for variant in &variants {
519            assert_ne!(key, variant.signature_cache_key(&dh));
520        }
521    }
522
523    #[test]
524    fn test_signature_cache_key_is_hash_of_signed_data_and_signature() {
525        let (parsed, dh) = signed_announce(Some(b"payload"), None);
526        let mut material = parsed.signed_data(&dh);
527        material.extend_from_slice(&parsed.signature);
528        assert_eq!(parsed.signature_cache_key(&dh), hash::full_hash(&material));
529    }
530}