stem_rs/descriptor/
micro.rs

1//! Microdescriptor parsing for Tor relay microdescriptors.
2//!
3//! Microdescriptors are compact relay descriptors used by Tor clients
4//! to reduce bandwidth usage. They contain a subset of server descriptor
5//! information and are referenced by their SHA-256 digest.
6//!
7//! # Overview
8//!
9//! Microdescriptors were introduced to reduce the bandwidth required for
10//! clients to learn about the Tor network. Instead of downloading full
11//! server descriptors, clients download:
12//!
13//! 1. A consensus document containing microdescriptor hashes
14//! 2. The microdescriptors themselves (much smaller than server descriptors)
15//!
16//! Microdescriptors contain only the information clients need for circuit
17//! building:
18//!
19//! - **Onion keys**: For circuit creation handshakes
20//! - **Exit policy summary**: Compact representation of allowed ports
21//! - **Protocol versions**: Supported protocol versions
22//! - **Family**: Related relays
23//!
24//! # Descriptor Format
25//!
26//! Microdescriptors use a compact text format:
27//!
28//! ```text
29//! onion-key
30//! -----BEGIN RSA PUBLIC KEY-----
31//! <base64 encoded key>
32//! -----END RSA PUBLIC KEY-----
33//! ntor-onion-key <base64 curve25519 key>
34//! a [<ipv6>]:<port>
35//! family <fingerprint> <fingerprint> ...
36//! p accept|reject <port-list>
37//! p6 accept|reject <port-list>
38//! pr <protocol>=<versions> ...
39//! id <type> <digest>
40//! ```
41//!
42//! # Annotations
43//!
44//! Microdescriptors from CollecTor archives may include annotations
45//! (lines starting with `@`) that provide metadata like when the
46//! descriptor was last seen:
47//!
48//! ```text
49//! @last-listed 2023-01-01 00:00:00
50//! onion-key
51//! ...
52//! ```
53//!
54//! # Example
55//!
56//! ```rust
57//! use stem_rs::descriptor::{Microdescriptor, Descriptor, DigestHash, DigestEncoding};
58//!
59//! let content = r#"onion-key
60//! -----BEGIN RSA PUBLIC KEY-----
61//! MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
62//! H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
63//! CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
64//! -----END RSA PUBLIC KEY-----
65//! ntor-onion-key r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=
66//! p accept 80,443
67//! "#;
68//!
69//! let desc = Microdescriptor::parse(content).unwrap();
70//! println!("Has ntor key: {}", desc.ntor_onion_key.is_some());
71//! println!("Exit policy: {}", desc.exit_policy);
72//!
73//! // Compute SHA-256 digest (used for identification)
74//! let digest = desc.digest(DigestHash::Sha256, DigestEncoding::Base64).unwrap();
75//! println!("Digest: {}", digest);
76//! ```
77//!
78//! # Digest Computation
79//!
80//! Unlike server descriptors which use SHA-1, microdescriptors are
81//! identified by their SHA-256 digest. The digest is computed over
82//! the entire microdescriptor content (excluding annotations).
83//!
84//! # See Also
85//!
86//! - [`ServerDescriptor`](super::ServerDescriptor) - Full relay descriptors
87//! - [`NetworkStatusDocument`](super::NetworkStatusDocument) - Contains microdescriptor hashes
88//! - [Python Stem Microdescriptor](https://stem.torproject.org/api/descriptor/microdescriptor.html)
89
90use std::collections::HashMap;
91use std::fmt;
92use std::net::IpAddr;
93use std::str::FromStr;
94
95use crate::exit_policy::MicroExitPolicy;
96use crate::Error;
97
98use super::{compute_digest, Descriptor, DigestEncoding, DigestHash};
99
100/// A microdescriptor containing compact relay information for clients.
101///
102/// Microdescriptors are designed to minimize bandwidth for Tor clients.
103/// They contain only the information needed for circuit building, omitting
104/// details like contact info, platform, and full exit policies.
105///
106/// # Fields Overview
107///
108/// | Field | Description |
109/// |-------|-------------|
110/// | `onion_key` | RSA key for TAP handshake (legacy) |
111/// | `ntor_onion_key` | Curve25519 key for ntor handshake |
112/// | `exit_policy` | Compact exit policy (ports only) |
113/// | `family` | Related relay fingerprints |
114/// | `protocols` | Supported protocol versions |
115///
116/// # Invariants
117///
118/// - `onion_key` is always present (required field)
119/// - `exit_policy` defaults to "reject 1-65535" if not specified
120///
121/// # Example
122///
123/// ```rust
124/// use stem_rs::descriptor::{Microdescriptor, Descriptor};
125///
126/// let content = r#"onion-key
127/// -----BEGIN RSA PUBLIC KEY-----
128/// MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
129/// H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
130/// CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
131/// -----END RSA PUBLIC KEY-----
132/// p accept 80,443
133/// "#;
134///
135/// let desc = Microdescriptor::parse(content).unwrap();
136/// assert!(desc.exit_policy.is_accept);
137/// ```
138///
139/// # Thread Safety
140///
141/// `Microdescriptor` is `Send` and `Sync` as it contains only owned data.
142#[derive(Debug, Clone, PartialEq)]
143pub struct Microdescriptor {
144    /// RSA onion key for TAP circuit handshake (PEM format).
145    ///
146    /// This is the legacy key used for the original Tor handshake.
147    /// Modern clients prefer the ntor handshake using `ntor_onion_key`.
148    pub onion_key: String,
149    /// Curve25519 onion key for ntor circuit handshake (base64).
150    ///
151    /// This is the modern key used for the ntor handshake, which provides
152    /// better security properties than the TAP handshake.
153    pub ntor_onion_key: Option<String>,
154    /// Additional addresses (IPv4 or IPv6) the relay listens on.
155    ///
156    /// Each tuple is (address, port, is_ipv6). The `a` lines in the
157    /// microdescriptor provide these additional addresses.
158    pub or_addresses: Vec<(IpAddr, u16, bool)>,
159    /// Fingerprints of related relays (same operator).
160    ///
161    /// These are typically prefixed with `$` and contain the full
162    /// 40-character hex fingerprint.
163    pub family: Vec<String>,
164    /// Compact IPv4 exit policy.
165    ///
166    /// Unlike full exit policies, microdescriptor policies only specify
167    /// which ports are accepted or rejected, not addresses.
168    pub exit_policy: MicroExitPolicy,
169    /// Compact IPv6 exit policy.
170    ///
171    /// Separate policy for IPv6 traffic, if different from IPv4.
172    pub exit_policy_v6: Option<MicroExitPolicy>,
173    /// Identity key digests by type.
174    ///
175    /// Maps key type (e.g., "rsa1024", "ed25519") to the base64-encoded
176    /// digest of that key.
177    pub identifiers: HashMap<String, String>,
178    /// Supported protocol versions.
179    ///
180    /// Maps protocol name to list of supported versions.
181    /// Common protocols include "Link", "Relay", "HSDir", etc.
182    pub protocols: HashMap<String, Vec<u32>>,
183    /// Raw bytes of the original descriptor content.
184    raw_content: Vec<u8>,
185    /// Annotations from CollecTor archives.
186    ///
187    /// Each annotation is a (key, optional_value) pair from lines
188    /// starting with `@`.
189    annotations: Vec<(String, Option<String>)>,
190    /// Lines that were not recognized during parsing.
191    unrecognized_lines: Vec<String>,
192}
193
194impl Microdescriptor {
195    /// Creates a new microdescriptor with the given onion key.
196    ///
197    /// This creates a descriptor with default values for optional fields.
198    /// The exit policy defaults to rejecting all ports.
199    ///
200    /// # Arguments
201    ///
202    /// * `onion_key` - The RSA onion key in PEM format
203    ///
204    /// # Example
205    ///
206    /// ```rust
207    /// use stem_rs::descriptor::Microdescriptor;
208    ///
209    /// let key = "-----BEGIN RSA PUBLIC KEY-----\n...\n-----END RSA PUBLIC KEY-----";
210    /// let desc = Microdescriptor::new(key.to_string());
211    /// assert!(desc.ntor_onion_key.is_none());
212    /// assert!(desc.family.is_empty());
213    /// ```
214    pub fn new(onion_key: String) -> Self {
215        Self {
216            onion_key,
217            ntor_onion_key: None,
218            or_addresses: Vec::new(),
219            family: Vec::new(),
220            exit_policy: MicroExitPolicy::parse("reject 1-65535").unwrap(),
221            exit_policy_v6: None,
222            identifiers: HashMap::new(),
223            protocols: HashMap::new(),
224            raw_content: Vec::new(),
225            annotations: Vec::new(),
226            unrecognized_lines: Vec::new(),
227        }
228    }
229
230    /// Parses a microdescriptor with additional annotations.
231    ///
232    /// This is useful when annotations are provided separately from
233    /// the descriptor content (e.g., when reading from a cache file
234    /// where annotations precede the descriptor).
235    ///
236    /// # Arguments
237    ///
238    /// * `content` - The microdescriptor content
239    /// * `annotations` - Additional annotation lines (with or without `@` prefix)
240    ///
241    /// # Errors
242    ///
243    /// Returns [`Error::Parse`] if the content is malformed.
244    ///
245    /// # Example
246    ///
247    /// ```rust
248    /// use stem_rs::descriptor::Microdescriptor;
249    ///
250    /// let content = r#"onion-key
251    /// -----BEGIN RSA PUBLIC KEY-----
252    /// MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
253    /// H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
254    /// CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
255    /// -----END RSA PUBLIC KEY-----
256    /// "#;
257    ///
258    /// let annotations = &["@last-listed 2023-01-01 00:00:00"];
259    /// let desc = Microdescriptor::parse_with_annotations(content, annotations).unwrap();
260    ///
261    /// let anns = desc.get_annotations();
262    /// assert!(anns.contains_key("last-listed"));
263    /// ```
264    pub fn parse_with_annotations(content: &str, annotations: &[&str]) -> Result<Self, Error> {
265        let mut desc = Self::parse(content)?;
266        for ann in annotations {
267            let ann = ann.trim();
268            if ann.is_empty() {
269                continue;
270            }
271            let ann = ann.strip_prefix('@').unwrap_or(ann);
272            if let Some(space_pos) = ann.find(' ') {
273                let key = ann[..space_pos].to_string();
274                let value = ann[space_pos + 1..].trim().to_string();
275                desc.annotations.push((key, Some(value)));
276            } else {
277                desc.annotations.push((ann.to_string(), None));
278            }
279        }
280        Ok(desc)
281    }
282
283    /// Returns all annotations as a map.
284    ///
285    /// Annotations are metadata lines starting with `@` that may be
286    /// present in CollecTor archives.
287    ///
288    /// # Example
289    ///
290    /// ```rust
291    /// use stem_rs::descriptor::{Microdescriptor, Descriptor};
292    ///
293    /// let content = r#"@last-listed 2023-01-01 00:00:00
294    /// onion-key
295    /// -----BEGIN RSA PUBLIC KEY-----
296    /// MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
297    /// H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
298    /// CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
299    /// -----END RSA PUBLIC KEY-----
300    /// "#;
301    ///
302    /// let desc = Microdescriptor::parse(content).unwrap();
303    /// let annotations = desc.get_annotations();
304    ///
305    /// if let Some(Some(date)) = annotations.get("last-listed") {
306    ///     println!("Last listed: {}", date);
307    /// }
308    /// ```
309    pub fn get_annotations(&self) -> HashMap<String, Option<String>> {
310        self.annotations.iter().cloned().collect()
311    }
312
313    /// Returns annotations formatted as lines with `@` prefix.
314    ///
315    /// This is useful for serializing annotations back to their
316    /// original format.
317    ///
318    /// # Example
319    ///
320    /// ```rust
321    /// use stem_rs::descriptor::{Microdescriptor, Descriptor};
322    ///
323    /// let content = r#"@last-listed 2023-01-01 00:00:00
324    /// onion-key
325    /// -----BEGIN RSA PUBLIC KEY-----
326    /// MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
327    /// H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
328    /// CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
329    /// -----END RSA PUBLIC KEY-----
330    /// "#;
331    ///
332    /// let desc = Microdescriptor::parse(content).unwrap();
333    /// for line in desc.get_annotation_lines() {
334    ///     println!("{}", line);
335    /// }
336    /// ```
337    pub fn get_annotation_lines(&self) -> Vec<String> {
338        self.annotations
339            .iter()
340            .map(|(k, v)| match v {
341                Some(val) => format!("@{} {}", k, val),
342                None => format!("@{}", k),
343            })
344            .collect()
345    }
346
347    /// Extracts a PEM block from lines starting at the given index.
348    fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
349        let mut block = String::new();
350        let mut idx = start_idx;
351        while idx < lines.len() {
352            let line = lines[idx];
353            block.push_str(line);
354            block.push('\n');
355            if line.starts_with("-----END ") {
356                break;
357            }
358            idx += 1;
359        }
360        (block.trim_end().to_string(), idx)
361    }
362
363    /// Parses an `a` (or-address) line.
364    fn parse_or_address(line: &str) -> Result<(IpAddr, u16, bool), Error> {
365        let line = line.trim();
366        if line.starts_with('[') {
367            if let Some(bracket_end) = line.find(']') {
368                let ipv6_str = &line[1..bracket_end];
369                let port_str = &line[bracket_end + 2..];
370                let addr: IpAddr = ipv6_str.parse().map_err(|_| Error::Parse {
371                    location: "a".to_string(),
372                    reason: format!("invalid IPv6 address: {}", ipv6_str),
373                })?;
374                let port: u16 = port_str.parse().map_err(|_| Error::Parse {
375                    location: "a".to_string(),
376                    reason: format!("invalid port: {}", port_str),
377                })?;
378                return Ok((addr, port, true));
379            }
380        }
381        if let Some(colon_pos) = line.rfind(':') {
382            let addr_str = &line[..colon_pos];
383            let port_str = &line[colon_pos + 1..];
384            let addr: IpAddr = addr_str.parse().map_err(|_| Error::Parse {
385                location: "a".to_string(),
386                reason: format!("invalid address: {}", addr_str),
387            })?;
388            let port: u16 = port_str.parse().map_err(|_| Error::Parse {
389                location: "a".to_string(),
390                reason: format!("invalid port: {}", port_str),
391            })?;
392            let is_ipv6 = addr.is_ipv6();
393            return Ok((addr, port, is_ipv6));
394        }
395        Err(Error::Parse {
396            location: "a".to_string(),
397            reason: format!("invalid or-address format: {}", line),
398        })
399    }
400
401    fn parse_protocols(value: &str) -> HashMap<String, Vec<u32>> {
402        let mut protocols = HashMap::new();
403        for entry in value.split_whitespace() {
404            if let Some(eq_pos) = entry.find('=') {
405                let proto_name = &entry[..eq_pos];
406                let versions_str = &entry[eq_pos + 1..];
407                let versions: Vec<u32> = versions_str
408                    .split(',')
409                    .filter_map(|v| {
410                        if let Some(dash) = v.find('-') {
411                            let start: u32 = v[..dash].parse().ok()?;
412                            let end: u32 = v[dash + 1..].parse().ok()?;
413                            Some((start..=end).collect::<Vec<_>>())
414                        } else {
415                            v.parse().ok().map(|n| vec![n])
416                        }
417                    })
418                    .flatten()
419                    .collect();
420                protocols.insert(proto_name.to_string(), versions);
421            }
422        }
423        protocols
424    }
425
426    fn parse_identifiers(
427        value: &str,
428        identifiers: &mut HashMap<String, String>,
429        validate: bool,
430    ) -> Result<(), Error> {
431        let parts: Vec<&str> = value.split_whitespace().collect();
432        if parts.len() < 2 {
433            return Err(Error::Parse {
434                location: "id".to_string(),
435                reason: format!(
436                    "'id' lines should contain both key type and digest: {}",
437                    value
438                ),
439            });
440        }
441        let key_type = parts[0].to_string();
442        let key_value = parts[1].to_string();
443        if validate && identifiers.contains_key(&key_type) {
444            return Err(Error::Parse {
445                location: "id".to_string(),
446                reason: format!(
447                    "There can only be one 'id' line per key type, but '{}' appeared multiple times",
448                    key_type
449                ),
450            });
451        }
452        identifiers.insert(key_type, key_value);
453        Ok(())
454    }
455}
456
457impl Descriptor for Microdescriptor {
458    fn parse(content: &str) -> Result<Self, Error> {
459        let raw_content = content.as_bytes().to_vec();
460        let lines: Vec<&str> = content.lines().collect();
461
462        let mut onion_key: Option<String> = None;
463        let mut ntor_onion_key: Option<String> = None;
464        let mut or_addresses: Vec<(IpAddr, u16, bool)> = Vec::new();
465        let mut family: Vec<String> = Vec::new();
466        let mut exit_policy = MicroExitPolicy::parse("reject 1-65535")?;
467        let mut exit_policy_v6: Option<MicroExitPolicy> = None;
468        let mut identifiers: HashMap<String, String> = HashMap::new();
469        let mut protocols: HashMap<String, Vec<u32>> = HashMap::new();
470        let mut unrecognized_lines: Vec<String> = Vec::new();
471        let mut annotations: Vec<(String, Option<String>)> = Vec::new();
472
473        let mut idx = 0;
474        while idx < lines.len() {
475            let line = lines[idx];
476
477            if line.starts_with('@') {
478                let ann = line.strip_prefix('@').unwrap_or(line);
479                if let Some(space_pos) = ann.find(' ') {
480                    let key = ann[..space_pos].to_string();
481                    let value = ann[space_pos + 1..].trim().to_string();
482                    annotations.push((key, Some(value)));
483                } else {
484                    annotations.push((ann.to_string(), None));
485                }
486                idx += 1;
487                continue;
488            }
489
490            let (keyword, value) = if let Some(space_pos) = line.find(' ') {
491                (&line[..space_pos], line[space_pos + 1..].trim())
492            } else {
493                (line, "")
494            };
495
496            match keyword {
497                "onion-key" => {
498                    let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
499                    onion_key = Some(block);
500                    idx = end_idx;
501                }
502                "ntor-onion-key" => {
503                    ntor_onion_key = Some(value.to_string());
504                }
505                "a" => {
506                    if let Ok(addr) = Self::parse_or_address(value) {
507                        or_addresses.push(addr);
508                    }
509                }
510                "family" => {
511                    family = value.split_whitespace().map(|s| s.to_string()).collect();
512                }
513                "p" => {
514                    exit_policy = MicroExitPolicy::parse(value)?;
515                }
516                "p6" => {
517                    exit_policy_v6 = Some(MicroExitPolicy::parse(value)?);
518                }
519                "pr" => {
520                    protocols = Self::parse_protocols(value);
521                }
522                "id" => {
523                    let _ = Self::parse_identifiers(value, &mut identifiers, false);
524                }
525                _ => {
526                    if !line.is_empty() && !line.starts_with("-----") {
527                        unrecognized_lines.push(line.to_string());
528                    }
529                }
530            }
531            idx += 1;
532        }
533
534        let onion_key = onion_key.ok_or_else(|| Error::Parse {
535            location: "onion-key".to_string(),
536            reason: "Microdescriptor must have a 'onion-key' entry".to_string(),
537        })?;
538
539        Ok(Self {
540            onion_key,
541            ntor_onion_key,
542            or_addresses,
543            family,
544            exit_policy,
545            exit_policy_v6,
546            identifiers,
547            protocols,
548            raw_content,
549            annotations,
550            unrecognized_lines,
551        })
552    }
553
554    fn to_descriptor_string(&self) -> String {
555        let mut result = String::new();
556
557        result.push_str("onion-key\n");
558        result.push_str(&self.onion_key);
559        result.push('\n');
560
561        if let Some(ref ntor_key) = self.ntor_onion_key {
562            result.push_str(&format!("ntor-onion-key {}\n", ntor_key));
563        }
564
565        for (addr, port, is_ipv6) in &self.or_addresses {
566            if *is_ipv6 {
567                result.push_str(&format!("a [{}]:{}\n", addr, port));
568            } else {
569                result.push_str(&format!("a {}:{}\n", addr, port));
570            }
571        }
572
573        if !self.family.is_empty() {
574            result.push_str(&format!("family {}\n", self.family.join(" ")));
575        }
576
577        result.push_str(&format!("p {}\n", self.exit_policy));
578
579        if let Some(ref policy_v6) = self.exit_policy_v6 {
580            result.push_str(&format!("p6 {}\n", policy_v6));
581        }
582
583        if !self.protocols.is_empty() {
584            let proto_str: Vec<String> = self
585                .protocols
586                .iter()
587                .map(|(k, v)| {
588                    let versions: Vec<String> = v.iter().map(|n| n.to_string()).collect();
589                    format!("{}={}", k, versions.join(","))
590                })
591                .collect();
592            result.push_str(&format!("pr {}\n", proto_str.join(" ")));
593        }
594
595        for (key_type, key_value) in &self.identifiers {
596            result.push_str(&format!("id {} {}\n", key_type, key_value));
597        }
598
599        result
600    }
601
602    fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
603        Ok(compute_digest(&self.raw_content, hash, encoding))
604    }
605
606    fn raw_content(&self) -> &[u8] {
607        &self.raw_content
608    }
609
610    fn unrecognized_lines(&self) -> &[String] {
611        &self.unrecognized_lines
612    }
613}
614
615impl FromStr for Microdescriptor {
616    type Err = Error;
617
618    fn from_str(s: &str) -> Result<Self, Self::Err> {
619        Self::parse(s)
620    }
621}
622
623impl fmt::Display for Microdescriptor {
624    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625        write!(f, "{}", self.to_descriptor_string())
626    }
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632
633    const FIRST_ONION_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----
634MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
635H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
636CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
637-----END RSA PUBLIC KEY-----";
638
639    const SECOND_ONION_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----
640MIGJAoGBALCOxZdpMI2WO496njSQ2M7b4IgAGATqpJmH3So7lXOa25sK6o7JipgP
641qQE83K/t/xsMIpxQ/hHkft3G78HkeXXFc9lVUzH0HmHwYEu0M+PMVULSkG36MfEl
6427WeSZzaG+Tlnh9OySAzVyTsv1ZJsTQFHH9V8wuM0GOMo9X8DFC+NAgMBAAE=
643-----END RSA PUBLIC KEY-----";
644
645    const THIRD_ONION_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----
646MIGJAoGBAOWFQHxO+5kGuhwPUX5jB7wJCrTbSU0fZwolNV1t9UaDdjGDvIjIhdit
647y2sMbyd9K8lbQO7x9rQjNst5ZicuaSOs854XQddSjm++vMdjYbOcVMqnKGSztvpd
648w/1LVWFfhcBnsGi4JMGbmP+KUZG9A8kI9deSyJhfi35jA7UepiHHAgMBAAE=
649-----END RSA PUBLIC KEY-----";
650
651    fn first_microdesc() -> &'static str {
652        "@last-listed 2013-02-24 00:18:36
653onion-key
654-----BEGIN RSA PUBLIC KEY-----
655MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
656H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
657CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
658-----END RSA PUBLIC KEY-----"
659    }
660
661    fn second_microdesc() -> &'static str {
662        "@last-listed 2013-02-24 00:18:37
663onion-key
664-----BEGIN RSA PUBLIC KEY-----
665MIGJAoGBALCOxZdpMI2WO496njSQ2M7b4IgAGATqpJmH3So7lXOa25sK6o7JipgP
666qQE83K/t/xsMIpxQ/hHkft3G78HkeXXFc9lVUzH0HmHwYEu0M+PMVULSkG36MfEl
6677WeSZzaG+Tlnh9OySAzVyTsv1ZJsTQFHH9V8wuM0GOMo9X8DFC+NAgMBAAE=
668-----END RSA PUBLIC KEY-----
669ntor-onion-key r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=
670family $6141629FA0D15A6AEAEF3A1BEB76E64C767B3174"
671    }
672
673    fn third_microdesc() -> &'static str {
674        "@last-listed 2013-02-24 00:18:36
675onion-key
676-----BEGIN RSA PUBLIC KEY-----
677MIGJAoGBAOWFQHxO+5kGuhwPUX5jB7wJCrTbSU0fZwolNV1t9UaDdjGDvIjIhdit
678y2sMbyd9K8lbQO7x9rQjNst5ZicuaSOs854XQddSjm++vMdjYbOcVMqnKGSztvpd
679w/1LVWFfhcBnsGi4JMGbmP+KUZG9A8kI9deSyJhfi35jA7UepiHHAgMBAAE=
680-----END RSA PUBLIC KEY-----
681a [2001:6b0:7:125::242]:9001
682p accept 80,443"
683    }
684
685    #[test]
686    fn test_parse_first_microdesc() {
687        let desc = Microdescriptor::parse(first_microdesc()).unwrap();
688        assert_eq!(desc.onion_key, FIRST_ONION_KEY);
689        assert_eq!(desc.ntor_onion_key, None);
690        assert!(desc.or_addresses.is_empty());
691        assert!(desc.family.is_empty());
692        assert!(!desc.exit_policy.is_accept);
693        assert!(desc.exit_policy.ports.iter().any(|p| p.is_wildcard()));
694        let annotations = desc.get_annotations();
695        assert_eq!(
696            annotations.get("last-listed"),
697            Some(&Some("2013-02-24 00:18:36".to_string()))
698        );
699    }
700
701    #[test]
702    fn test_parse_second_microdesc() {
703        let desc = Microdescriptor::parse(second_microdesc()).unwrap();
704        assert_eq!(desc.onion_key, SECOND_ONION_KEY);
705        assert_eq!(
706            desc.ntor_onion_key,
707            Some("r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=".to_string())
708        );
709        assert!(desc.or_addresses.is_empty());
710        assert_eq!(
711            desc.family,
712            vec!["$6141629FA0D15A6AEAEF3A1BEB76E64C767B3174"]
713        );
714        assert!(!desc.exit_policy.is_accept);
715        assert!(desc.exit_policy.ports.iter().any(|p| p.is_wildcard()));
716    }
717
718    #[test]
719    fn test_parse_third_microdesc() {
720        let desc = Microdescriptor::parse(third_microdesc()).unwrap();
721        assert_eq!(desc.onion_key, THIRD_ONION_KEY);
722        assert_eq!(desc.ntor_onion_key, None);
723        assert_eq!(desc.or_addresses.len(), 1);
724        let (addr, port, is_ipv6) = &desc.or_addresses[0];
725        assert_eq!(addr.to_string(), "2001:6b0:7:125::242");
726        assert_eq!(*port, 9001);
727        assert!(*is_ipv6);
728        assert!(desc.family.is_empty());
729        assert_eq!(desc.exit_policy.to_string(), "accept 80,443");
730    }
731
732    #[test]
733    fn test_minimal_microdescriptor() {
734        let content = "onion-key
735-----BEGIN RSA PUBLIC KEY-----
736MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
737H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
738CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
739-----END RSA PUBLIC KEY-----";
740        let desc = Microdescriptor::parse(content).unwrap();
741        assert_eq!(desc.ntor_onion_key, None);
742        assert!(desc.or_addresses.is_empty());
743        assert!(desc.family.is_empty());
744        assert!(!desc.exit_policy.is_accept);
745        assert!(desc.exit_policy.ports.iter().any(|p| p.is_wildcard()));
746        assert_eq!(desc.exit_policy_v6, None);
747        assert!(desc.identifiers.is_empty());
748        assert!(desc.protocols.is_empty());
749        assert!(desc.unrecognized_lines.is_empty());
750    }
751
752    #[test]
753    fn test_unrecognized_line() {
754        let content = "onion-key
755-----BEGIN RSA PUBLIC KEY-----
756MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
757H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
758CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
759-----END RSA PUBLIC KEY-----
760pepperjack is oh so tasty!";
761        let desc = Microdescriptor::parse(content).unwrap();
762        assert_eq!(desc.unrecognized_lines, vec!["pepperjack is oh so tasty!"]);
763    }
764
765    #[test]
766    fn test_a_line() {
767        let content = "onion-key
768-----BEGIN RSA PUBLIC KEY-----
769MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
770H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
771CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
772-----END RSA PUBLIC KEY-----
773a 10.45.227.253:9001
774a [fd9f:2e19:3bcf::02:9970]:9001";
775        let desc = Microdescriptor::parse(content).unwrap();
776        assert_eq!(desc.or_addresses.len(), 2);
777        let (addr1, port1, is_ipv6_1) = &desc.or_addresses[0];
778        assert_eq!(addr1.to_string(), "10.45.227.253");
779        assert_eq!(*port1, 9001);
780        assert!(!*is_ipv6_1);
781        let (addr2, port2, is_ipv6_2) = &desc.or_addresses[1];
782        assert_eq!(addr2.to_string(), "fd9f:2e19:3bcf::2:9970");
783        assert_eq!(*port2, 9001);
784        assert!(*is_ipv6_2);
785    }
786
787    #[test]
788    fn test_family() {
789        let content = "onion-key
790-----BEGIN RSA PUBLIC KEY-----
791MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
792H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
793CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
794-----END RSA PUBLIC KEY-----
795family Amunet1 Amunet2 Amunet3";
796        let desc = Microdescriptor::parse(content).unwrap();
797        assert_eq!(desc.family, vec!["Amunet1", "Amunet2", "Amunet3"]);
798    }
799
800    #[test]
801    fn test_exit_policy() {
802        let content = "onion-key
803-----BEGIN RSA PUBLIC KEY-----
804MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
805H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
806CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
807-----END RSA PUBLIC KEY-----
808p accept 80,110,143,443";
809        let desc = Microdescriptor::parse(content).unwrap();
810        assert_eq!(desc.exit_policy.to_string(), "accept 80,110,143,443");
811    }
812
813    #[test]
814    fn test_protocols() {
815        let content = "onion-key
816-----BEGIN RSA PUBLIC KEY-----
817MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
818H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
819CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
820-----END RSA PUBLIC KEY-----
821pr Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2";
822        let desc = Microdescriptor::parse(content).unwrap();
823        assert_eq!(desc.protocols.len(), 10);
824        assert_eq!(desc.protocols.get("Link"), Some(&vec![1, 2, 3, 4]));
825        assert_eq!(desc.protocols.get("Relay"), Some(&vec![1, 2]));
826    }
827
828    #[test]
829    fn test_identifier() {
830        let content = "onion-key
831-----BEGIN RSA PUBLIC KEY-----
832MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
833H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
834CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
835-----END RSA PUBLIC KEY-----
836id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4";
837        let desc = Microdescriptor::parse(content).unwrap();
838        assert_eq!(
839            desc.identifiers.get("rsa1024"),
840            Some(&"Cd47okjCHD83YGzThGBDptXs9Z4".to_string())
841        );
842    }
843
844    #[test]
845    fn test_multiple_identifiers() {
846        let content = "onion-key
847-----BEGIN RSA PUBLIC KEY-----
848MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
849H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
850CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
851-----END RSA PUBLIC KEY-----
852id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4
853id ed25519 50f6ddbecdc848dcc6b818b14d1";
854        let desc = Microdescriptor::parse(content).unwrap();
855        assert_eq!(
856            desc.identifiers.get("rsa1024"),
857            Some(&"Cd47okjCHD83YGzThGBDptXs9Z4".to_string())
858        );
859        assert_eq!(
860            desc.identifiers.get("ed25519"),
861            Some(&"50f6ddbecdc848dcc6b818b14d1".to_string())
862        );
863    }
864
865    #[test]
866    fn test_digest() {
867        let desc = Microdescriptor::parse(third_microdesc()).unwrap();
868        let digest = desc
869            .digest(DigestHash::Sha256, DigestEncoding::Base64)
870            .unwrap();
871        assert!(!digest.is_empty());
872    }
873
874    #[test]
875    fn test_missing_onion_key() {
876        let content = "ntor-onion-key r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=";
877        let result = Microdescriptor::parse(content);
878        assert!(result.is_err());
879    }
880
881    #[test]
882    fn test_proceeding_line() {
883        let content = "family Amunet1
884onion-key
885-----BEGIN RSA PUBLIC KEY-----
886MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
887H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
888CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
889-----END RSA PUBLIC KEY-----";
890        let desc = Microdescriptor::parse(content).unwrap();
891        assert_eq!(desc.family, vec!["Amunet1"]);
892    }
893
894    #[test]
895    fn test_conflicting_identifiers() {
896        let content = "onion-key
897-----BEGIN RSA PUBLIC KEY-----
898MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
899H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
900CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
901-----END RSA PUBLIC KEY-----
902id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4
903id rsa1024 50f6ddbecdc848dcc6b818b14d1";
904        let desc = Microdescriptor::parse(content).unwrap();
905        assert_eq!(
906            desc.identifiers.get("rsa1024"),
907            Some(&"50f6ddbecdc848dcc6b818b14d1".to_string())
908        );
909    }
910
911    #[test]
912    fn test_exit_policy_v6() {
913        let content = "onion-key
914-----BEGIN RSA PUBLIC KEY-----
915MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
916H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
917CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
918-----END RSA PUBLIC KEY-----
919p accept 80,443
920p6 accept 80,443";
921        let desc = Microdescriptor::parse(content).unwrap();
922        assert_eq!(desc.exit_policy.to_string(), "accept 80,443");
923        assert!(desc.exit_policy_v6.is_some());
924        assert_eq!(desc.exit_policy_v6.unwrap().to_string(), "accept 80,443");
925    }
926
927    use proptest::prelude::*;
928
929    fn valid_base64_key() -> impl Strategy<Value = String> {
930        Just("MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM\nH2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF\nCxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=".to_string())
931    }
932
933    fn valid_ntor_key() -> impl Strategy<Value = String> {
934        Just("r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=".to_string())
935    }
936
937    fn simple_microdescriptor() -> impl Strategy<Value = Microdescriptor> {
938        (
939            valid_base64_key(),
940            proptest::option::of(valid_ntor_key()),
941            proptest::collection::vec("[A-Za-z0-9]{1,19}", 0..3),
942        )
943            .prop_map(|(onion_key, ntor_key, family)| {
944                let mut desc = Microdescriptor::new(format!(
945                    "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----",
946                    onion_key
947                ));
948                desc.ntor_onion_key = ntor_key;
949                desc.family = family;
950                desc
951            })
952    }
953
954    proptest! {
955        #![proptest_config(ProptestConfig::with_cases(100))]
956
957        #[test]
958        fn prop_microdescriptor_roundtrip(desc in simple_microdescriptor()) {
959            let serialized = desc.to_descriptor_string();
960            let parsed = Microdescriptor::parse(&serialized);
961
962            prop_assert!(parsed.is_ok(), "Failed to parse serialized microdescriptor: {:?}", parsed.err());
963
964            let parsed = parsed.unwrap();
965
966            prop_assert_eq!(&desc.onion_key, &parsed.onion_key, "onion_key mismatch");
967            prop_assert_eq!(&desc.ntor_onion_key, &parsed.ntor_onion_key, "ntor_onion_key mismatch");
968            prop_assert_eq!(&desc.family, &parsed.family, "family mismatch");
969            prop_assert_eq!(desc.exit_policy.to_string(), parsed.exit_policy.to_string(), "exit_policy mismatch");
970        }
971    }
972}