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