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