1use 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
42pub const DOC_DIGEST_LEN: usize = 32;
44
45#[allow(dead_code)]
48#[derive(Clone, Debug, Default)]
49pub struct MicrodescAnnotation {
50 last_listed: Option<time::SystemTime>,
53}
54
55pub type MdDigest = [u8; DOC_DIGEST_LEN];
57
58#[derive(Clone, Debug, Deftly, PartialEq, Eq)]
62#[derive_deftly(NetdocParseable)]
63#[non_exhaustive]
64pub struct Microdesc {
65 onion_key: OnionKeyIntro,
72
73 #[deftly(netdoc(single_arg))]
75 pub ntor_onion_key: Curve25519Public,
76
77 #[deftly(netdoc(default))]
79 pub family: Arc<RelayFamily>,
80
81 #[deftly(netdoc(default))]
83 pub family_ids: RelayFamilyIds,
84
85 #[deftly(netdoc(keyword = "p", default))]
87 pub ipv4_policy: Arc<PortPolicy>,
88
89 #[deftly(netdoc(keyword = "p6", default))]
91 pub ipv6_policy: Arc<PortPolicy>,
92
93 #[deftly(netdoc(keyword = "id", with = "Ed25519IdentityLine"))]
96 pub ed25519_id: Ed25519IdentityLine,
97
98 #[deftly(netdoc(skip))]
106 pub sha256: MdDigest,
107}
108
109impl Microdesc {
110 #[cfg(feature = "build_docs")]
122 pub fn builder() -> MicrodescBuilder {
123 MicrodescBuilder::new()
124 }
125
126 pub fn digest(&self) -> &MdDigest {
128 &self.sha256
129 }
130 pub fn ntor_key(&self) -> &curve25519::PublicKey {
132 &self.ntor_onion_key.0
133 }
134 pub fn ipv4_policy(&self) -> &Arc<PortPolicy> {
136 &self.ipv4_policy
137 }
138 pub fn ipv6_policy(&self) -> &Arc<PortPolicy> {
140 &self.ipv6_policy
141 }
142 pub fn family(&self) -> &RelayFamily {
144 self.family.as_ref()
145 }
146 pub fn ed25519_id(&self) -> &ed25519::Ed25519Identity {
149 &self.ed25519_id.pk.0
150 }
151 pub fn family_ids(&self) -> &[RelayFamilyId] {
153 self.family_ids.as_ref()
154 }
155}
156
157#[derive(Debug, Clone, Default, Deftly, PartialEq, Eq)]
162#[derive_deftly(ItemValueParseable)]
163struct OnionKeyIntro(#[deftly(netdoc(object))] Option<rsa::PublicKey>);
164
165#[allow(dead_code)]
169#[derive(Clone, Debug)]
170pub struct AnnotatedMicrodesc {
171 md: Microdesc,
173 ann: MicrodescAnnotation,
175 location: Option<Extent>,
178}
179
180impl AnnotatedMicrodesc {
181 pub fn into_microdesc(self) -> Microdesc {
183 self.md
184 }
185
186 pub fn md(&self) -> &Microdesc {
189 &self.md
190 }
191
192 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 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
212static 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 rules.reject_unrecognized();
221 rules.build()
222});
223static 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 #[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 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 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 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 let start_pos = {
297 #[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 #[allow(clippy::unwrap_used)]
308 util::str::str_offset(s, first.kwd_str()).unwrap()
309 };
310
311 {
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 let ntor_onion_key = body
329 .required(NTOR_ONION_KEY)?
330 .parse_arg::<Curve25519Public>(0)?;
331
332 let family = body
337 .maybe(FAMILY)
338 .parse_args_as_str::<RelayFamily>()?
339 .unwrap_or_else(RelayFamily::new)
340 .intern();
341
342 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 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 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 #[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
408fn 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 }
429 None => {
430 return;
431 }
432 };
433 let _ = reader.next();
434 }
435}
436
437#[derive(Debug)]
440pub struct MicrodescReader<'a> {
441 annotated: bool,
443 reader: NetDocReader<'a, MicrodescKwd>,
445}
446
447impl<'a> MicrodescReader<'a> {
448 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 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 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 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 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 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 #![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 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 #[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 #[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 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}