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_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
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(Constructor, NetdocEncodable, NetdocParseable)]
63#[allow(clippy::exhaustive_structs)]
64pub struct Microdesc {
65 pub onion_key: MicrodescIntroItem,
68
69 #[deftly(constructor)]
71 #[deftly(netdoc(single_arg))]
72 pub ntor_onion_key: Curve25519Public,
73
74 #[deftly(netdoc(default(skip)))]
76 pub family: Intern<RelayFamily>,
77
78 #[deftly(netdoc(default(skip)))]
80 pub family_ids: RelayFamilyIds,
81
82 #[deftly(netdoc(keyword = "p", default(skip)))]
84 pub ipv4_policy: Intern<PortPolicy>,
85
86 #[deftly(netdoc(keyword = "p6", default(skip)))]
88 pub ipv6_policy: Intern<PortPolicy>,
89
90 #[deftly(constructor)]
93 #[deftly(netdoc(keyword = "id", with = "Ed25519IdentityLine"))]
94 pub ed25519_id: Ed25519IdentityLine,
95
96 #[doc(hidden)]
99 #[deftly(netdoc(skip))]
100 pub __non_exhaustive: (),
101}
102
103#[derive(Clone, Debug, Deftly, PartialEq, Eq, derive_more::Deref, derive_more::DerefMut)]
112#[non_exhaustive]
113pub struct MicrodescAndHash {
114 #[deref]
116 #[deref_mut]
117 pub md: Microdesc,
118
119 pub sha256: MdDigest,
124}
125
126impl Microdesc {
127 pub fn ntor_key(&self) -> &curve25519::PublicKey {
129 &self.ntor_onion_key.0
130 }
131 pub fn ipv4_policy(&self) -> &Intern<PortPolicy> {
133 &self.ipv4_policy
134 }
135 pub fn ipv6_policy(&self) -> &Intern<PortPolicy> {
137 &self.ipv6_policy
138 }
139 pub fn family(&self) -> &RelayFamily {
141 self.family.as_ref()
142 }
143 pub fn ed25519_id(&self) -> &ed25519::Ed25519Identity {
146 &self.ed25519_id.pk.0
147 }
148 pub fn family_ids(&self) -> &[RelayFamilyId] {
150 self.family_ids.as_ref()
151 }
152}
153
154impl MicrodescAndHash {
155 #[cfg(feature = "build_docs")]
167 pub fn builder() -> MicrodescBuilder {
168 MicrodescBuilder::new()
169 }
170
171 pub fn digest(&self) -> &MdDigest {
173 &self.sha256
174 }
175}
176
177#[derive(Debug, Clone, Default, Deftly, PartialEq, Eq)]
186#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
187pub struct MicrodescIntroItem(#[deftly(netdoc(object))] Option<rsa::PublicKey>);
188
189#[allow(dead_code)]
193#[derive(Clone, Debug)]
194pub struct AnnotatedMicrodesc {
195 md: MicrodescAndHash,
197 ann: MicrodescAnnotation,
199 location: Option<Extent>,
202}
203
204impl AnnotatedMicrodesc {
205 pub fn into_microdesc(self) -> MicrodescAndHash {
207 self.md
208 }
209
210 pub fn md(&self) -> &MicrodescAndHash {
213 &self.md
214 }
215
216 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 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
236static 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 rules.reject_unrecognized();
245 rules.build()
246});
247static 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 #[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 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 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 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 let start_pos = {
321 #[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 #[allow(clippy::unwrap_used)]
332 util::str::str_offset(s, first.kwd_str()).unwrap()
333 };
334
335 {
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 let ntor_onion_key = body
353 .required(NTOR_ONION_KEY)?
354 .parse_arg::<Curve25519Public>(0)?;
355
356 let family = body
361 .maybe(FAMILY)
362 .parse_args_as_str::<RelayFamily>()?
363 .unwrap_or_else(RelayFamily::new)
364 .intern();
365
366 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 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 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 #[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
433fn 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 }
454 None => {
455 return;
456 }
457 };
458 let _ = reader.next();
459 }
460}
461
462#[derive(Debug)]
465pub struct MicrodescReader<'a> {
466 annotated: bool,
468 reader: NetDocReader<'a, MicrodescKwd>,
470}
471
472impl<'a> MicrodescReader<'a> {
473 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 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 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 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 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 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 #![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)] 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 #[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 #[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 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}