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
36use crate::parse2::ItemObjectParseable;
37
38#[cfg(feature = "build_docs")]
39mod build;
40
41#[cfg(feature = "build_docs")]
42pub use build::MicrodescBuilder;
43
44pub const DOC_DIGEST_LEN: usize = 32;
46
47#[allow(dead_code)]
50#[derive(Clone, Debug, Default)]
51pub struct MicrodescAnnotation {
52 last_listed: Option<time::SystemTime>,
55}
56
57pub type MdDigest = [u8; DOC_DIGEST_LEN];
59
60#[derive(Clone, Debug, Deftly, PartialEq, Eq)]
64#[derive_deftly(NetdocParseable)]
65#[non_exhaustive]
66pub struct Microdesc {
67 onion_key: OnionKeyIntro,
74
75 #[deftly(netdoc(single_arg))]
77 pub ntor_onion_key: Curve25519Public,
78
79 #[deftly(netdoc(default))]
81 pub family: Arc<RelayFamily>,
82
83 #[deftly(netdoc(default))]
85 pub family_ids: RelayFamilyIds,
86
87 #[deftly(netdoc(keyword = "p", default))]
89 pub ipv4_policy: Arc<PortPolicy>,
90
91 #[deftly(netdoc(keyword = "p6", default))]
93 pub ipv6_policy: Arc<PortPolicy>,
94
95 #[deftly(netdoc(keyword = "id", with = "Ed25519IdentityLine"))]
98 pub ed25519_id: Ed25519IdentityLine,
99
100 #[deftly(netdoc(skip))]
108 pub sha256: MdDigest,
109}
110
111impl Microdesc {
112 #[cfg(feature = "build_docs")]
124 pub fn builder() -> MicrodescBuilder {
125 MicrodescBuilder::new()
126 }
127
128 pub fn digest(&self) -> &MdDigest {
130 &self.sha256
131 }
132 pub fn ntor_key(&self) -> &curve25519::PublicKey {
134 &self.ntor_onion_key.0
135 }
136 pub fn ipv4_policy(&self) -> &Arc<PortPolicy> {
138 &self.ipv4_policy
139 }
140 pub fn ipv6_policy(&self) -> &Arc<PortPolicy> {
142 &self.ipv6_policy
143 }
144 pub fn family(&self) -> &RelayFamily {
146 self.family.as_ref()
147 }
148 pub fn ed25519_id(&self) -> &ed25519::Ed25519Identity {
151 &self.ed25519_id.pk.0
152 }
153 pub fn family_ids(&self) -> &[RelayFamilyId] {
155 self.family_ids.as_ref()
156 }
157}
158
159#[derive(Debug, Clone, Default, Deftly, PartialEq, Eq)]
164#[derive_deftly(ItemValueParseable)]
165struct OnionKeyIntro(#[deftly(netdoc(object))] Option<rsa::PublicKey>);
166
167#[allow(dead_code)]
171#[derive(Clone, Debug)]
172pub struct AnnotatedMicrodesc {
173 md: Microdesc,
175 ann: MicrodescAnnotation,
177 location: Option<Extent>,
180}
181
182impl AnnotatedMicrodesc {
183 pub fn into_microdesc(self) -> Microdesc {
185 self.md
186 }
187
188 pub fn md(&self) -> &Microdesc {
191 &self.md
192 }
193
194 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 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
214static 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 rules.reject_unrecognized();
223 rules.build()
224});
225static 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 #[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 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 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 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 let start_pos = {
299 #[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 #[allow(clippy::unwrap_used)]
310 util::str::str_offset(s, first.kwd_str()).unwrap()
311 };
312
313 {
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 let ntor_onion_key = body
331 .required(NTOR_ONION_KEY)?
332 .parse_arg::<Curve25519Public>(0)?;
333
334 let family = body
339 .maybe(FAMILY)
340 .parse_args_as_str::<RelayFamily>()?
341 .unwrap_or_else(RelayFamily::new)
342 .intern();
343
344 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 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 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 #[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
410fn 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 }
431 None => {
432 return;
433 }
434 };
435 let _ = reader.next();
436 }
437}
438
439#[derive(Debug)]
442pub struct MicrodescReader<'a> {
443 annotated: bool,
445 reader: NetDocReader<'a, MicrodescKwd>,
447}
448
449impl<'a> MicrodescReader<'a> {
450 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 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 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 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 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 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 #![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 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 #[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 #[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 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}