Skip to main content

tor_netdoc/doc/
microdesc.rs

1//! Parsing implementation for Tor microdescriptors.
2//!
3//! A "microdescriptor" is an incomplete, infrequently-changing
4//! summary of a relay's information that is generated by
5//! the directory authorities.
6//!
7//! Microdescriptors are much smaller than router descriptors, and
8//! change less frequently. For this reason, they're currently used
9//! for building circuits by all relays and clients.
10//!
11//! Microdescriptors can't be used on their own: you need to know
12//! which relay they are for, which requires a valid consensus
13//! directory.
14
15use crate::parse::keyword::Keyword;
16use crate::parse::parser::SectionRules;
17use crate::parse::tokenize::{ItemResult, NetDocReader};
18use crate::types::family::{RelayFamily, RelayFamilyId, RelayFamilyIds};
19use crate::types::misc::*;
20use crate::types::policy::PortPolicy;
21use crate::util;
22use crate::util::PeekableIterator;
23use crate::util::str::Extent;
24use crate::{AllowAnnotations, Error, NetdocErrorKind as EK, Result};
25use tor_error::internal;
26use tor_llcrypto::d;
27use tor_llcrypto::pk::{curve25519, ed25519, rsa};
28
29use derive_deftly::Deftly;
30use digest::Digest;
31use std::str::FromStr as _;
32use std::sync::Arc;
33use std::sync::LazyLock;
34use std::time;
35
36#[cfg(feature = "build_docs")]
37mod build;
38
39#[cfg(feature = "build_docs")]
40pub use build::MicrodescBuilder;
41
42/// Length of a router microdescriptor digest
43pub const DOC_DIGEST_LEN: usize = 32;
44
45/// Annotations prepended to a microdescriptor that has been stored to
46/// disk.
47#[allow(dead_code)]
48#[derive(Clone, Debug, Default)]
49pub struct MicrodescAnnotation {
50    /// A time at which this microdescriptor was last listed in some
51    /// consensus document.
52    last_listed: Option<time::SystemTime>,
53}
54
55/// The digest of a microdescriptor as used in microdesc consensuses
56pub type MdDigest = [u8; DOC_DIGEST_LEN];
57
58/// A single microdescriptor.
59///
60/// <https://spec.torproject.org/dir-spec/computing-microdescriptors.html>
61#[derive(Clone, Debug, Deftly, PartialEq, Eq)]
62#[derive_deftly(NetdocParseable)]
63#[non_exhaustive]
64pub struct Microdesc {
65    /// The legacy onion key, whose object is optional but whose item serves
66    /// as the intro line for these kind of descriptors.
67    ///
68    /// Let's keep this private for now to prevent interfacing applications
69    /// from generating microdesc's with an onion-key; they are not necessary
70    /// anymore and just waste space.
71    onion_key: OnionKeyIntro,
72
73    /// Public key used for the ntor circuit extension protocol.
74    #[deftly(netdoc(single_arg))]
75    pub ntor_onion_key: Curve25519Public,
76
77    /// Declared family for this relay.
78    #[deftly(netdoc(default))]
79    pub family: Arc<RelayFamily>,
80
81    /// Family identities for this relay.
82    #[deftly(netdoc(default))]
83    pub family_ids: RelayFamilyIds,
84
85    /// List of IPv4 ports to which this relay will exit
86    #[deftly(netdoc(keyword = "p", default))]
87    pub ipv4_policy: Arc<PortPolicy>,
88
89    /// List of IPv6 ports to which this relay will exit
90    #[deftly(netdoc(keyword = "p6", default))]
91    pub ipv6_policy: Arc<PortPolicy>,
92
93    /// Ed25519 identity for this relay
94    // TODO SPEC: Set this to "exactly once".
95    #[deftly(netdoc(keyword = "id", with = "Ed25519IdentityLine"))]
96    pub ed25519_id: Ed25519IdentityLine,
97
98    // addr is obsolete and doesn't go here any more
99    // pr is obsolete and doesn't go here any more.
100    /// The SHA256 digest of the text of this microdescriptor.  This
101    /// value is used to identify the microdescriptor when downloading
102    /// it, and when listing it in a consensus document.
103    // TODO: maybe this belongs somewhere else. Once it's used to store
104    // correlate the microdesc to a consensus, it's never used again.
105    #[deftly(netdoc(skip))]
106    pub sha256: MdDigest,
107}
108
109impl Microdesc {
110    /// Create a new MicrodescBuilder that can be used to construct
111    /// microdescriptors.
112    ///
113    /// This function is only available when the crate is built with the
114    /// `build_docs` feature.
115    ///
116    /// # Limitations
117    ///
118    /// The generated microdescriptors cannot yet be encoded, and do
119    /// not yet have correct sha256 digests. As such they are only
120    /// useful for testing.
121    #[cfg(feature = "build_docs")]
122    pub fn builder() -> MicrodescBuilder {
123        MicrodescBuilder::new()
124    }
125
126    /// Return the sha256 digest of this microdesc.
127    pub fn digest(&self) -> &MdDigest {
128        &self.sha256
129    }
130    /// Return the ntor onion key for this microdesc
131    pub fn ntor_key(&self) -> &curve25519::PublicKey {
132        &self.ntor_onion_key.0
133    }
134    /// Return the ipv4 exit policy for this microdesc
135    pub fn ipv4_policy(&self) -> &Arc<PortPolicy> {
136        &self.ipv4_policy
137    }
138    /// Return the ipv6 exit policy for this microdesc
139    pub fn ipv6_policy(&self) -> &Arc<PortPolicy> {
140        &self.ipv6_policy
141    }
142    /// Return the relay family for this microdesc
143    pub fn family(&self) -> &RelayFamily {
144        self.family.as_ref()
145    }
146    /// Return the ed25519 identity for this microdesc, if its
147    /// Ed25519 identity is well-formed.
148    pub fn ed25519_id(&self) -> &ed25519::Ed25519Identity {
149        &self.ed25519_id.pk.0
150    }
151    /// Return a list of family ids for this microdesc.
152    pub fn family_ids(&self) -> &[RelayFamilyId] {
153        self.family_ids.as_ref()
154    }
155}
156
157/// Intro line for a [`Microdesc`].
158///
159/// The object (the onion key) is deprecated and optional, but the item itself
160/// must be present, because it is used to mark the start of the netdoc.
161#[derive(Debug, Clone, Default, Deftly, PartialEq, Eq)]
162#[derive_deftly(ItemValueParseable)]
163struct OnionKeyIntro(#[deftly(netdoc(object))] Option<rsa::PublicKey>);
164
165/// A microdescriptor annotated with additional data
166///
167/// TODO: rename this.
168#[allow(dead_code)]
169#[derive(Clone, Debug)]
170pub struct AnnotatedMicrodesc {
171    /// The microdescriptor
172    md: Microdesc,
173    /// The annotations for the microdescriptor
174    ann: MicrodescAnnotation,
175    /// Where did we find the microdescriptor with the originally parsed
176    /// string?
177    location: Option<Extent>,
178}
179
180impl AnnotatedMicrodesc {
181    /// Consume this annotated microdesc and discard its annotations.
182    pub fn into_microdesc(self) -> Microdesc {
183        self.md
184    }
185
186    /// Return a reference to the microdescriptor within this annotated
187    /// microdescriptor.
188    pub fn md(&self) -> &Microdesc {
189        &self.md
190    }
191
192    /// If this Microdesc was parsed from `s`, return its original text.
193    pub fn within<'a>(&self, s: &'a str) -> Option<&'a str> {
194        self.location.as_ref().and_then(|ext| ext.reconstruct(s))
195    }
196}
197
198decl_keyword! {
199    /// Keyword type for recognized objects in microdescriptors.
200    MicrodescKwd {
201        annotation "@last-listed" => ANN_LAST_LISTED,
202        "onion-key" => ONION_KEY,
203        "ntor-onion-key" => NTOR_ONION_KEY,
204        "family" => FAMILY,
205        "family-ids" => FAMILY_IDS,
206        "p" => P,
207        "p6" => P6,
208        "id" => ID,
209    }
210}
211
212/// Rules about annotations that can appear before a Microdescriptor
213static MICRODESC_ANNOTATIONS: LazyLock<SectionRules<MicrodescKwd>> = LazyLock::new(|| {
214    use MicrodescKwd::*;
215    let mut rules = SectionRules::builder();
216    rules.add(ANN_LAST_LISTED.rule().args(1..));
217    rules.add(ANN_UNRECOGNIZED.rule().may_repeat().obj_optional());
218    // unrecognized annotations are okay; anything else is a bug in this
219    // context.
220    rules.reject_unrecognized();
221    rules.build()
222});
223/// Rules about entries that must appear in an Microdesc, and how they must
224/// be formed.
225static MICRODESC_RULES: LazyLock<SectionRules<MicrodescKwd>> = LazyLock::new(|| {
226    use MicrodescKwd::*;
227
228    let mut rules = SectionRules::builder();
229    rules.add(ONION_KEY.rule().required().no_args().obj_optional());
230    rules.add(NTOR_ONION_KEY.rule().required().args(1..));
231    rules.add(FAMILY.rule().args(1..));
232    rules.add(FAMILY_IDS.rule().args(0..));
233    rules.add(P.rule().args(2..));
234    rules.add(P6.rule().args(2..));
235    rules.add(ID.rule().may_repeat().args(2..));
236    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
237    rules.build()
238});
239
240impl MicrodescAnnotation {
241    /// Extract a (possibly empty) microdescriptor annotation from a
242    /// reader.
243    #[allow(dead_code)]
244    fn parse_from_reader(
245        reader: &mut NetDocReader<'_, MicrodescKwd>,
246    ) -> Result<MicrodescAnnotation> {
247        use MicrodescKwd::*;
248
249        let mut items = reader.pause_at(|item| item.is_ok_with_non_annotation());
250        let body = MICRODESC_ANNOTATIONS.parse(&mut items)?;
251
252        let last_listed = match body.get(ANN_LAST_LISTED) {
253            None => None,
254            Some(item) => Some(item.args_as_str().parse::<Iso8601TimeSp>()?.into()),
255        };
256
257        Ok(MicrodescAnnotation { last_listed })
258    }
259}
260
261impl Microdesc {
262    /// Parse a string into a new microdescriptor.
263    pub fn parse(s: &str) -> Result<Microdesc> {
264        let mut items = crate::parse::tokenize::NetDocReader::new(s)?;
265        let (result, _) = Self::parse_from_reader(&mut items).map_err(|e| e.within(s))?;
266        items.should_be_exhausted()?;
267        Ok(result)
268    }
269
270    /// Extract a single microdescriptor from a NetDocReader.
271    fn parse_from_reader(
272        reader: &mut NetDocReader<'_, MicrodescKwd>,
273    ) -> Result<(Microdesc, Option<Extent>)> {
274        use MicrodescKwd::*;
275        let s = reader.str();
276
277        let mut first_onion_key = true;
278        // We'll pause at the next annotation, or at the _second_ onion key.
279        let mut items = reader.pause_at(|item| match item {
280            Err(_) => false,
281            Ok(item) => {
282                item.kwd().is_annotation()
283                    || if item.kwd() == ONION_KEY {
284                        let was_first = first_onion_key;
285                        first_onion_key = false;
286                        !was_first
287                    } else {
288                        false
289                    }
290            }
291        });
292
293        let body = MICRODESC_RULES.parse(&mut items)?;
294
295        // We have to start with onion-key
296        let start_pos = {
297            // unwrap here is safe because parsing would have failed
298            // had there not been at least one item.
299            #[allow(clippy::unwrap_used)]
300            let first = body.first_item().unwrap();
301            if first.kwd() != ONION_KEY {
302                return Err(EK::WrongStartingToken
303                    .with_msg(first.kwd_str().to_string())
304                    .at_pos(first.pos()));
305            }
306            // Unwrap is safe here because we are parsing these strings from s
307            #[allow(clippy::unwrap_used)]
308            util::str::str_offset(s, first.kwd_str()).unwrap()
309        };
310
311        // Legacy (tap) onion key.  We parse this to make sure it's well-formed,
312        // but then we discard it immediately, since we never want to use it.
313        //
314        // In microdescriptors, the ONION_KEY field is mandatory, but its
315        // associated object is optional.
316        {
317            let tok = body.required(ONION_KEY)?;
318            if tok.has_obj() {
319                let _: rsa::PublicKey = tok
320                    .parse_obj::<RsaPublicParse1Helper>("RSA PUBLIC KEY")?
321                    .check_len_eq(1024)?
322                    .check_exponent(65537)?
323                    .into();
324            }
325        }
326
327        // Ntor onion key
328        let ntor_onion_key = body
329            .required(NTOR_ONION_KEY)?
330            .parse_arg::<Curve25519Public>(0)?;
331
332        // family
333        //
334        // (We don't need to add the relay's own ID to this family, as we do in
335        // RouterDescs: the authorities already took care of that for us.)
336        let family = body
337            .maybe(FAMILY)
338            .parse_args_as_str::<RelayFamily>()?
339            .unwrap_or_else(RelayFamily::new)
340            .intern();
341
342        // Family ids (happy families case).
343        let family_ids = body
344            .maybe(FAMILY_IDS)
345            .args_as_str()
346            .unwrap_or("")
347            .split_ascii_whitespace()
348            .map(RelayFamilyId::from_str)
349            .collect::<Result<RelayFamilyIds>>()?;
350
351        // exit policies.
352        let ipv4_policy = body
353            .maybe(P)
354            .parse_args_as_str::<PortPolicy>()?
355            .unwrap_or_else(PortPolicy::new_reject_all);
356        let ipv6_policy = body
357            .maybe(P6)
358            .parse_args_as_str::<PortPolicy>()?
359            .unwrap_or_else(PortPolicy::new_reject_all);
360
361        // ed25519 identity
362        let ed25519_id = {
363            let id_tok = body
364                .slice(ID)
365                .iter()
366                .find(|item| item.arg(0) == Some("ed25519"));
367            match id_tok {
368                None => {
369                    return Err(EK::MissingToken.with_msg("id ed25519"));
370                }
371                Some(tok) => Ed25519IdentityLine {
372                    alg: Ed25519AlgorithmString::Ed25519,
373                    pk: tok.parse_arg::<Ed25519Public>(1)?,
374                },
375            }
376        };
377
378        let end_pos = {
379            // unwrap here is safe because parsing would have failed
380            // had there not been at least one item.
381            #[allow(clippy::unwrap_used)]
382            let last_item = body.last_item().unwrap();
383            last_item.offset_after(s).ok_or_else(|| {
384                Error::from(internal!("last item was not within source string"))
385                    .at_pos(last_item.end_pos())
386            })?
387        };
388
389        let text = &s[start_pos..end_pos];
390        let sha256 = d::Sha256::digest(text.as_bytes()).into();
391
392        let location = Extent::new(s, text);
393
394        let md = Microdesc {
395            onion_key: Default::default(),
396            sha256,
397            ntor_onion_key,
398            family,
399            ipv4_policy: ipv4_policy.intern(),
400            ipv6_policy: ipv6_policy.intern(),
401            ed25519_id,
402            family_ids,
403        };
404        Ok((md, location))
405    }
406}
407
408/// Consume tokens from 'reader' until the next token is the beginning
409/// of a microdescriptor: an annotation or an ONION_KEY.  If no such
410/// token exists, advance to the end of the reader.
411fn advance_to_next_microdesc(reader: &mut NetDocReader<'_, MicrodescKwd>, annotated: bool) {
412    use MicrodescKwd::*;
413    loop {
414        let item = reader.peek();
415        match item {
416            Some(Ok(t)) => {
417                let kwd = t.kwd();
418                if (annotated && kwd.is_annotation()) || kwd == ONION_KEY {
419                    return;
420                }
421            }
422            Some(Err(_)) => {
423                // We skip over broken tokens here.
424                //
425                // (This case can't happen in practice, since if there had been
426                // any error tokens, they would have been handled as part of
427                // handling the previous microdesc.)
428            }
429            None => {
430                return;
431            }
432        };
433        let _ = reader.next();
434    }
435}
436
437/// An iterator that parses one or more (possibly annotated)
438/// microdescriptors from a string.
439#[derive(Debug)]
440pub struct MicrodescReader<'a> {
441    /// True if we accept annotations; false otherwise.
442    annotated: bool,
443    /// An underlying reader to give us Items for the microdescriptors
444    reader: NetDocReader<'a, MicrodescKwd>,
445}
446
447impl<'a> MicrodescReader<'a> {
448    /// Construct a MicrodescReader to take microdescriptors from a string
449    /// 's'.
450    pub fn new(s: &'a str, allow: &AllowAnnotations) -> Result<Self> {
451        let reader = NetDocReader::new(s)?;
452        let annotated = allow == &AllowAnnotations::AnnotationsAllowed;
453        Ok(MicrodescReader { annotated, reader })
454    }
455
456    /// If we're annotated, parse an annotation from the reader. Otherwise
457    /// return a default annotation.
458    fn take_annotation(&mut self) -> Result<MicrodescAnnotation> {
459        if self.annotated {
460            MicrodescAnnotation::parse_from_reader(&mut self.reader)
461        } else {
462            Ok(MicrodescAnnotation::default())
463        }
464    }
465
466    /// Parse a (possibly annotated) microdescriptor from the reader.
467    ///
468    /// On error, parsing stops after the first failure.
469    fn take_annotated_microdesc_raw(&mut self) -> Result<AnnotatedMicrodesc> {
470        let ann = self.take_annotation()?;
471        let (md, location) = Microdesc::parse_from_reader(&mut self.reader)?;
472        Ok(AnnotatedMicrodesc { md, ann, location })
473    }
474
475    /// Parse a (possibly annotated) microdescriptor from the reader.
476    ///
477    /// On error, advance the reader to the start of the next microdescriptor.
478    fn take_annotated_microdesc(&mut self) -> Result<AnnotatedMicrodesc> {
479        let pos_orig = self.reader.pos();
480        let result = self.take_annotated_microdesc_raw();
481        if result.is_err() {
482            if self.reader.pos() == pos_orig {
483                // No tokens were consumed from the reader.  We need to
484                // drop at least one token to ensure we aren't looping.
485                //
486                // (This might not be able to happen, but it's easier to
487                // explicitly catch this case than it is to prove that
488                // it's impossible.)
489                let _ = self.reader.next();
490            }
491            advance_to_next_microdesc(&mut self.reader, self.annotated);
492        }
493        result
494    }
495}
496
497impl<'a> Iterator for MicrodescReader<'a> {
498    type Item = Result<AnnotatedMicrodesc>;
499    fn next(&mut self) -> Option<Self::Item> {
500        // If there is no next token, we're at the end.
501        self.reader.peek()?;
502
503        Some(
504            self.take_annotated_microdesc()
505                .map_err(|e| e.within(self.reader.str())),
506        )
507    }
508}
509
510#[cfg(test)]
511mod test {
512    // @@ begin test lint list maintained by maint/add_warning @@
513    #![allow(clippy::bool_assert_comparison)]
514    #![allow(clippy::clone_on_copy)]
515    #![allow(clippy::dbg_macro)]
516    #![allow(clippy::mixed_attributes_style)]
517    #![allow(clippy::print_stderr)]
518    #![allow(clippy::print_stdout)]
519    #![allow(clippy::single_char_pattern)]
520    #![allow(clippy::unwrap_used)]
521    #![allow(clippy::unchecked_time_subtraction)]
522    #![allow(clippy::useless_vec)]
523    #![allow(clippy::needless_pass_by_value)]
524    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
525    use super::*;
526    use hex_literal::hex;
527    const TESTDATA: &str = include_str!("../../testdata/microdesc1.txt");
528    const TESTDATA2: &str = include_str!("../../testdata/microdesc2.txt");
529    const TESTDATA3: &str = include_str!("../../testdata/microdesc3.txt");
530    const TESTDATA4: &str = include_str!("../../testdata/microdesc4.txt");
531
532    fn read_bad(fname: &str) -> String {
533        use std::fs;
534        use std::path::PathBuf;
535        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
536        path.push("testdata");
537        path.push("bad-mds");
538        path.push(fname);
539
540        fs::read_to_string(path).unwrap()
541    }
542
543    #[test]
544    fn parse_single() -> Result<()> {
545        let _md = Microdesc::parse(TESTDATA)?;
546        Ok(())
547    }
548
549    #[test]
550    fn parse_no_tap_key() -> Result<()> {
551        let _md = Microdesc::parse(TESTDATA3)?;
552        Ok(())
553    }
554
555    #[test]
556    fn parse_multi() -> Result<()> {
557        use humantime::parse_rfc3339;
558        let mds: Result<Vec<_>> =
559            MicrodescReader::new(TESTDATA2, &AllowAnnotations::AnnotationsAllowed)?.collect();
560        let mds = mds?;
561        assert_eq!(mds.len(), 4);
562
563        assert_eq!(
564            mds[0].ann.last_listed.unwrap(),
565            parse_rfc3339("2020-01-27T18:52:09Z").unwrap()
566        );
567        assert_eq!(
568            mds[0].md().digest(),
569            &hex!("38c71329a87098cb341c46c9c62bd646622b4445f7eb985a0e6adb23a22ccf4f")
570        );
571        assert_eq!(
572            mds[0].md().ntor_key().as_bytes(),
573            &hex!("5e895d65304a3a1894616660143f7af5757fe08bc18045c7855ee8debb9e6c47")
574        );
575        assert!(mds[0].md().ipv4_policy().allows_port(993));
576        assert!(mds[0].md().ipv6_policy().allows_port(993));
577        assert!(!mds[0].md().ipv4_policy().allows_port(25));
578        assert!(!mds[0].md().ipv6_policy().allows_port(25));
579        assert_eq!(
580            mds[0].md().ed25519_id().as_bytes(),
581            &hex!("2d85fdc88e6c1bcfb46897fca1dba6d1354f93261d68a79e0b5bc170dd923084")
582        );
583
584        Ok(())
585    }
586
587    #[test]
588    fn parse_family_ids() -> Result<()> {
589        let mds: Vec<AnnotatedMicrodesc> =
590            MicrodescReader::new(TESTDATA4, &AllowAnnotations::AnnotationsNotAllowed)?
591                .collect::<Result<_>>()?;
592        assert_eq!(mds.len(), 2);
593        let md0 = mds[0].md();
594        let md1 = mds[1].md();
595        assert!(md0.family_ids().is_empty());
596        assert_eq!(
597            md1.family_ids(),
598            &[
599                "ed25519:dXMgdGhlIHRyaXVtcGguICAgIC1UaG9tYXMgUGFpbmU"
600                    .parse()
601                    .unwrap(),
602                "other:Example".parse().unwrap()
603            ]
604        );
605        assert!(matches!(md1.family_ids()[0], RelayFamilyId::Ed25519(_)));
606
607        Ok(())
608    }
609
610    #[test]
611    fn test_bad() {
612        use crate::Pos;
613        use crate::types::policy::PolicyError;
614        fn check(fname: &str, e: &Error) {
615            let content = read_bad(fname);
616            let res = Microdesc::parse(&content);
617            assert!(res.is_err());
618            assert_eq!(&res.err().unwrap(), e);
619        }
620
621        check(
622            "wrong-start",
623            &EK::WrongStartingToken
624                .with_msg("family")
625                .at_pos(Pos::from_line(1, 1)),
626        );
627        check(
628            "bogus-policy",
629            &EK::BadPolicy
630                .at_pos(Pos::from_line(9, 1))
631                .with_source(PolicyError::InvalidPort),
632        );
633        check("wrong-id", &EK::MissingToken.with_msg("id ed25519"));
634    }
635
636    #[test]
637    fn test_recover() -> Result<()> {
638        let mut data = read_bad("wrong-start");
639        data += TESTDATA;
640        data += &read_bad("wrong-id");
641
642        let res: Vec<Result<_>> =
643            MicrodescReader::new(&data, &AllowAnnotations::AnnotationsAllowed)?.collect();
644
645        assert_eq!(res.len(), 3);
646        assert!(res[0].is_err());
647        assert!(res[1].is_ok());
648        assert!(res[2].is_err());
649        Ok(())
650    }
651
652    /// Checks whether parse2 works on [`Microdesc`].
653    ///
654    /// Certain values such as public keys are hardcoded and can be simply
655    /// replaced by a copy and paste in the case one replaces the testdata2
656    /// vector's in the future.
657    #[test]
658    fn parse2() {
659        use tor_llcrypto::pk::ed25519::Ed25519Identity;
660
661        use crate::parse2;
662
663        let md = include_str!("../../testdata2/cached-microdescs.new");
664        let mds = parse2::parse_netdoc_multiple::<Microdesc>(&parse2::ParseInput::new(
665            md,
666            "../../testdata2/cached-microdescs.new",
667        ))
668        .unwrap();
669
670        assert_eq!(mds.len(), 7);
671        assert_eq!(
672            mds[0],
673            Microdesc {
674                onion_key: OnionKeyIntro(rsa::PublicKey::from_der(
675                    pem::parse(
676                        "
677-----BEGIN RSA PUBLIC KEY-----
678MIGJAoGBANF8Zgxp8amY1esYdPj2Ada1ORiVB/A4sgKLQ5ij/wsasO3yjjLcvHRB
679UJ0mAQWql/nauvjnKUeZFcGm3t7q0v3F9uUsOGTAZ/IKh31UQAm5OS/TJyf8IHky
680Yl0wCKpUZFHs5CHsajLSfXZKHkwfqRXFEJu9aMtmQdQFfqE9JOJHAgMBAAE=
681-----END RSA PUBLIC KEY-----
682                        "
683                    )
684                    .unwrap()
685                    .contents()
686                )),
687                sha256: [0; 32],
688                ntor_onion_key: curve25519::PublicKey::from(<[u8; 32]>::from(
689                    FixedB64::<32>::from_str("I1S8JfcqPPHWVTxfjq/eGmGiu/OtR+fF0Z86Ge1mq3s")
690                        .unwrap()
691                ))
692                .into(),
693                family: Default::default(),
694                ipv4_policy: Default::default(),
695                ipv6_policy: Default::default(),
696                ed25519_id: Ed25519Identity::from(<[u8; 32]>::from(
697                    FixedB64::<32>::from_str("yhO6nETO5AUdvJbLgPnw4mFjozGXWMCqOp30nY6nM8E")
698                        .unwrap()
699                ))
700                .into(),
701                family_ids: Default::default(),
702            }
703        );
704    }
705
706    /// Manual test for happy families.
707    // TODO: This should be included in testdata2/ but that would require the
708    // chutney/shadow integration test to actually do families at all.
709    #[test]
710    fn parse2_happy_family() {
711        use tor_llcrypto::pk::ed25519::Ed25519Identity;
712
713        use crate::parse2::{self, ParseInput};
714        use std::iter;
715
716        // A microdescriptor taken from the wild containing happy families.
717        const MICRODESC: &str = "\
718onion-key
719-----BEGIN RSA PUBLIC KEY-----
720MIGJAoGBAMk57F7qGHVadBJ6m4028w13I1Qk67Ee0JU88w7NObKBph3DQYjgYs4e
721eUdiW4Gdsx8w/xOuK0foCo0O8Iqq5MXtVcpUP/N+5uB7SVvGdJFsKw21KdIc6v8g
722ACZAijw5ZPOdhLbyLQyFHNV8zXUov1dlx/Fb9M3lPMVevnDbuKM5AgMBAAE=
723-----END RSA PUBLIC KEY-----
724ntor-onion-key fhhP23UKD4L2jehA5gopAo5b6NSoB+kZN5Q4ULv3Zww
725family $4CFFD403DAB89A689F3FDB80B5366E46D879E736 $4D6C1486939A42D7FFE69BCD9F3FDAA86C743433 $73955E6A69BA5E0827F48206CAD78C045BBE8873 $8DBA9ADCA5B3A3AB6D2B4F88AC2F96614D33DAB3 $B29E3E30443F897F48B86765F1BC1DB917F5DF46 $CD642E7E722979580B6D631697772C0B72BCF25C $D9E7B6A73C8278274081B77D373ECCE4552E75FB $F2515315FE0DB7456194CABC503B526B49951415
726family-ids ed25519:b54cKgML0ykRyhdIRcq1xtW19iEVsMYnGNbdY+vvcas
727id ed25519 /MU/FVKRGcZAy8XFnzLS6Dgcg6s1VpYeFjkwb6+CVhw
728";
729
730        let md = parse2::parse_netdoc::<Microdesc>(&ParseInput::new(MICRODESC, "")).unwrap();
731        assert_eq!(
732            md.family_ids,
733            RelayFamilyIds::from_iter(iter::once(RelayFamilyId::Ed25519(
734                Ed25519Identity::from_base64("b54cKgML0ykRyhdIRcq1xtW19iEVsMYnGNbdY+vvcas")
735                    .unwrap()
736            )))
737        );
738    }
739}