1use std::time::SystemTime;
4
5use super::{IntroAuthType, IntroPointDesc};
6use crate::batching_split_before::IteratorExt as _;
7use crate::doc::hsdesc::pow::PowParamSet;
8use crate::parse::tokenize::{ItemResult, NetDocReader};
9use crate::parse::{keyword::Keyword, parser::SectionRules};
10use crate::types::misc::{B64, UnvalidatedEdCert};
11use crate::{NetdocErrorKind as EK, Result};
12
13use itertools::Itertools as _;
14use smallvec::SmallVec;
15use std::sync::LazyLock;
16use tor_checkable::Timebound;
17use tor_checkable::signed::SignatureGated;
18use tor_checkable::timed::TimerangeBound;
19use tor_hscrypto::NUM_INTRO_POINT_MAX;
20use tor_hscrypto::pk::{HsIntroPtSessionIdKey, HsSvcNtorKey};
21use tor_llcrypto::pk::ed25519::Ed25519Identity;
22use tor_llcrypto::pk::{ValidatableSignature, curve25519, ed25519};
23
24#[derive(Debug, Clone)]
26#[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
27pub(crate) struct HsDescInner {
28 pub(super) intro_auth_types: Option<SmallVec<[IntroAuthType; 2]>>,
35 pub(super) single_onion_service: bool,
40 pub(super) intro_points: Vec<IntroPointDesc>,
44 pub(super) pow_params: PowParamSet,
46}
47
48decl_keyword! {
49 pub(crate) HsInnerKwd {
50 "create2-formats" => CREATE2_FORMATS,
51 "intro-auth-required" => INTRO_AUTH_REQUIRED,
52 "single-onion-service" => SINGLE_ONION_SERVICE,
53 "introduction-point" => INTRODUCTION_POINT,
54 "onion-key" => ONION_KEY,
55 "auth-key" => AUTH_KEY,
56 "enc-key" => ENC_KEY,
57 "enc-key-cert" => ENC_KEY_CERT,
58 "legacy-key" => LEGACY_KEY,
59 "legacy-key-cert" => LEGACY_KEY_CERT,
60 "pow-params" => POW_PARAMS,
61 }
62}
63
64static HS_INNER_HEADER_RULES: LazyLock<SectionRules<HsInnerKwd>> = LazyLock::new(|| {
67 use HsInnerKwd::*;
68
69 let mut rules = SectionRules::builder();
70 rules.add(CREATE2_FORMATS.rule().required().args(1..));
71 rules.add(INTRO_AUTH_REQUIRED.rule().args(1..));
72 rules.add(SINGLE_ONION_SERVICE.rule());
73 rules.add(POW_PARAMS.rule().args(1..).may_repeat().obj_optional());
74 rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
75
76 rules.build()
77});
78
79static HS_INNER_INTRO_RULES: LazyLock<SectionRules<HsInnerKwd>> = LazyLock::new(|| {
82 use HsInnerKwd::*;
83
84 let mut rules = SectionRules::builder();
85 rules.add(INTRODUCTION_POINT.rule().required().args(1..));
86 rules.add(ONION_KEY.rule().required().may_repeat().args(2..));
91 rules.add(AUTH_KEY.rule().required().obj_required());
92 rules.add(ENC_KEY.rule().required().may_repeat().args(2..));
93 rules.add(ENC_KEY_CERT.rule().required().obj_required());
94 rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
95
96 rules.build()
104});
105
106pub(crate) type UncheckedHsDescInner = TimerangeBound<SignatureGated<HsDescInner>>;
108
109struct InnerCertData {
113 signing_key: Ed25519Identity,
115 subject_key: ed25519::PublicKey,
117 signature: Box<dyn ValidatableSignature>,
120 expiry: SystemTime,
122}
123
124fn handle_inner_certificate(
130 tok: &crate::parse::tokenize::Item<HsInnerKwd>,
131 want_tag: &str,
132 want_type: tor_cert::CertType,
133) -> Result<InnerCertData> {
134 let make_err = |e, msg| {
135 EK::BadObjectVal
136 .with_msg(msg)
137 .with_source(e)
138 .at_pos(tok.pos())
139 };
140
141 let cert = tok
142 .parse_obj::<UnvalidatedEdCert>(want_tag)?
143 .check_cert_type(want_type)?
144 .into_unchecked();
145
146 let cert = cert
148 .should_have_signing_key()
149 .map_err(|e| make_err(e, "Certificate was not self-signed"))?;
150
151 let (cert, signature) = cert
153 .dangerously_split()
154 .map_err(|e| make_err(e, "Certificate was not Ed25519-signed"))?;
155 let signature = Box::new(signature);
156
157 let cert = cert.dangerously_assume_timely();
159 let expiry = cert.expiry();
160 let subject_key = cert
161 .subject_key()
162 .as_ed25519()
163 .ok_or_else(|| {
164 EK::BadObjectVal
165 .with_msg("Certified key was not Ed25519")
166 .at_pos(tok.pos())
167 })?
168 .try_into()
169 .map_err(|_| {
170 EK::BadObjectVal
171 .with_msg("Certified key was not valid Ed25519")
172 .at_pos(tok.pos())
173 })?;
174
175 let signing_key = *cert.signing_key().ok_or_else(|| {
176 EK::BadObjectVal
177 .with_msg("Signing key was not Ed25519")
178 .at_pos(tok.pos())
179 })?;
180
181 Ok(InnerCertData {
182 signing_key,
183 subject_key,
184 signature,
185 expiry,
186 })
187}
188
189impl HsDescInner {
190 #[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
196 pub(super) fn parse(s: &str) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
197 let mut reader = NetDocReader::new(s)?;
198 let result = Self::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
199 Ok(result)
200 }
201
202 #[allow(unstable_name_collisions)]
212 fn take_from_reader(
213 input: &mut NetDocReader<'_, HsInnerKwd>,
214 ) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
215 use HsInnerKwd::*;
216
217 let mut sections =
219 input.batching_split_before_with_header(|item| item.is_ok_with_kwd(INTRODUCTION_POINT));
220 let header = HS_INNER_HEADER_RULES.parse(&mut sections)?;
222
223 {
226 let tok = header.required(CREATE2_FORMATS)?;
227 if !tok.args().any(|s| s == "2") {
232 return Err(EK::BadArgument
233 .at_pos(tok.pos())
234 .with_msg("Onion service descriptor does not support ntor handshake."));
235 }
236 }
237 let auth_types = if let Some(tok) = header.get(INTRO_AUTH_REQUIRED) {
240 let mut auth_types: SmallVec<[IntroAuthType; 2]> = SmallVec::new();
241 let mut push = |at| {
242 if !auth_types.contains(&at) {
243 auth_types.push(at);
244 }
245 };
246 for arg in tok.args() {
247 #[allow(clippy::single_match)]
248 match arg {
249 "ed25519" => push(IntroAuthType::Ed25519),
250 _ => (), }
252 }
253 if auth_types.is_empty() {
255 return Err(EK::BadArgument
256 .at_pos(tok.pos())
257 .with_msg("No recognized introduction authentication methods."));
258 }
259
260 Some(auth_types)
261 } else {
262 None
263 };
264
265 let is_single_onion_service = header.get(SINGLE_ONION_SERVICE).is_some();
267
268 let pow_params = PowParamSet::from_items(header.slice(POW_PARAMS))?;
270
271 let mut signatures = Vec::new();
272 let mut expirations = Vec::new();
273 let mut cert_signing_key: Option<Ed25519Identity> = None;
274
275 let mut intro_points = Vec::new();
279 let mut sections = sections.subsequent();
280 while let Some(mut ipt_section) = sections.next_batch() {
281 let ipt_section = HS_INNER_INTRO_RULES.parse(&mut ipt_section)?;
282
283 let link_specifiers = {
285 let tok = ipt_section.required(INTRODUCTION_POINT)?;
286 let ls = tok.parse_arg::<B64>(0)?;
287 let mut r = tor_bytes::Reader::from_slice(ls.as_bytes());
288 let n = r.take_u8()?;
289 let res = r.extract_n(n.into())?;
290 r.should_be_exhausted()?;
291 res
292 };
293
294 let ntor_onion_key = {
296 let tok = ipt_section
297 .slice(ONION_KEY)
298 .iter()
299 .filter(|item| item.arg(0) == Some("ntor"))
300 .exactly_one()
301 .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
302 tok.parse_arg::<B64>(1)?.into_array()?.into()
303 };
304
305 let auth_key: HsIntroPtSessionIdKey = {
308 let tok = ipt_section.required(AUTH_KEY)?;
323 let InnerCertData {
324 signing_key,
325 subject_key,
326 signature,
327 expiry,
328 } = handle_inner_certificate(
329 tok,
330 "ED25519 CERT",
331 tor_cert::CertType::HS_IP_V_SIGNING,
332 )?;
333 expirations.push(expiry);
334 signatures.push(signature);
335 if cert_signing_key.get_or_insert(signing_key) != &signing_key {
336 return Err(EK::BadObjectVal
337 .at_pos(tok.pos())
338 .with_msg("Mismatched signing key"));
339 }
340
341 subject_key.into()
342 };
343
344 let svc_ntor_key: HsSvcNtorKey = {
348 let tok = ipt_section
349 .slice(ENC_KEY)
350 .iter()
351 .filter(|item| item.arg(0) == Some("ntor"))
352 .exactly_one()
353 .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
354 let key = curve25519::PublicKey::from(tok.parse_arg::<B64>(1)?.into_array()?);
355 key.into()
356 };
357
358 {
361 let tok = ipt_section.required(ENC_KEY_CERT)?;
364 let InnerCertData {
365 signing_key,
366 subject_key,
367 signature,
368 expiry,
369 } = handle_inner_certificate(
370 tok,
371 "ED25519 CERT",
372 tor_cert::CertType::HS_IP_CC_SIGNING,
373 )?;
374 expirations.push(expiry);
375 signatures.push(signature);
376
377 let sign_bit = 0;
382 let expected_ed_key =
383 tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public(
384 &svc_ntor_key,
385 sign_bit,
386 );
387 if expected_ed_key != Some(subject_key) {
388 return Err(EK::BadObjectVal
389 .at_pos(tok.pos())
390 .with_msg("Mismatched subject key"));
391 }
392
393 if cert_signing_key.get_or_insert(signing_key) != &signing_key {
395 return Err(EK::BadObjectVal
396 .at_pos(tok.pos())
397 .with_msg("Mismatched signing key"));
398 }
399 };
400
401 if intro_points.len() < NUM_INTRO_POINT_MAX {
410 intro_points.push(IntroPointDesc {
411 link_specifiers,
412 ipt_ntor_key: ntor_onion_key,
413 ipt_sid_key: auth_key,
414 svc_ntor_key,
415 });
416 }
417 }
418
419 if intro_points.is_empty() {
427 return Err(EK::MissingEntry.with_msg("no introduction points"));
428 }
429
430 let inner = HsDescInner {
431 intro_auth_types: auth_types,
432 single_onion_service: is_single_onion_service,
433 pow_params,
434 intro_points,
435 };
436 let sig_gated = SignatureGated::new(inner, signatures);
437 let time_bound = match expirations.iter().min() {
438 Some(t) => TimerangeBound::new(sig_gated, ..t),
439 None => TimerangeBound::new(sig_gated, ..),
440 };
441
442 Ok((cert_signing_key, time_bound))
443 }
444}
445
446#[cfg(test)]
447mod test {
448 #![allow(clippy::bool_assert_comparison)]
450 #![allow(clippy::clone_on_copy)]
451 #![allow(clippy::dbg_macro)]
452 #![allow(clippy::mixed_attributes_style)]
453 #![allow(clippy::print_stderr)]
454 #![allow(clippy::print_stdout)]
455 #![allow(clippy::single_char_pattern)]
456 #![allow(clippy::unwrap_used)]
457 #![allow(clippy::unchecked_time_subtraction)]
458 #![allow(clippy::useless_vec)]
459 #![allow(clippy::needless_pass_by_value)]
460 use std::{iter, time::Duration};
463
464 use hex_literal::hex;
465 use itertools::chain;
466 use tor_checkable::{SelfSigned, Timebound};
467
468 use super::*;
469 use crate::doc::hsdesc::{
470 middle::HsDescMiddle,
471 outer::HsDescOuter,
472 pow::PowParams,
473 test_data::{TEST_DATA, TEST_SUBCREDENTIAL},
474 };
475
476 #[test]
479 fn inner_text() {
480 const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner.txt");
482
483 use crate::NetdocErrorKind as NEK;
484 let _desc = HsDescInner::parse(TEST_DATA_INNER).unwrap();
485
486 let none = format!(
487 "{}\n",
488 TEST_DATA_INNER
489 .split_once("\nintroduction-point")
490 .unwrap()
491 .0,
492 );
493 let err = HsDescInner::parse(&none).map(|_| &none).unwrap_err();
494 assert_eq!(err.kind, NEK::MissingEntry);
495
496 let ipt = format!(
497 "introduction-point{}",
498 TEST_DATA_INNER
499 .rsplit_once("\nintroduction-point")
500 .unwrap()
501 .1,
502 );
503 for n in NUM_INTRO_POINT_MAX..NUM_INTRO_POINT_MAX + 2 {
504 let many =
505 chain!(iter::once(&*none), std::iter::repeat_n(&*ipt, n),).collect::<String>();
506 let desc = HsDescInner::parse(&many).unwrap();
507 let desc = desc
508 .1
509 .dangerously_into_parts()
510 .0
511 .dangerously_assume_wellsigned();
512 assert_eq!(desc.intro_points.len(), NUM_INTRO_POINT_MAX);
513 }
514 }
515
516 #[test]
518 #[cfg(feature = "hs-pow-full")]
519 fn inner_c_pow_v1() {
520 const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
521 let desc = HsDescInner::parse(TEST_DATA_INNER).unwrap();
522 let pow_params = desc
523 .1
524 .dangerously_into_parts()
525 .0
526 .dangerously_assume_wellsigned()
527 .pow_params;
528 assert_eq!(pow_params.slice().len(), 1);
529 match &pow_params.slice()[0] {
530 PowParams::V1(v1) => {
531 let expected_effort: tor_hscrypto::pow::v1::Effort = 614.into();
532 let expected_seed: tor_hscrypto::pow::v1::Seed =
533 hex!("144e901df0841833a6e8592190849b4412f307d1565f2f137b2a5bc21a31092a").into();
534 let expected_expiry = Some(SystemTime::UNIX_EPOCH + Duration::new(1712812537, 0));
535 assert_eq!(v1.suggested_effort(), expected_effort);
536 assert_eq!(
537 v1.seed().to_owned().dangerously_assume_timely(),
538 expected_seed
539 );
540 assert_eq!(v1.seed().bounds().1, expected_expiry);
541 }
542 #[allow(unreachable_patterns)]
543 _ => unreachable!(),
544 }
545 }
546
547 #[test]
549 fn inner_c_pow_v1_with_unknown() {
550 const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
551 let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
552 let test_data_inner = format!("{}\npow-params x-example\npow-params{}", parts.0, parts.1);
553 let desc = HsDescInner::parse(&test_data_inner).unwrap();
554 let pow_params = desc
555 .1
556 .dangerously_into_parts()
557 .0
558 .dangerously_assume_wellsigned()
559 .pow_params;
560 assert_eq!(pow_params.slice().len(), 1);
561 }
562
563 #[test]
565 fn inner_pow_empty() {
566 const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
567 let err = HsDescInner::parse(TEST_DATA_INNER).map(|_| ()).unwrap_err();
568 assert_eq!(err.kind, crate::NetdocErrorKind::TooFewArguments);
569 }
570
571 #[test]
573 fn inner_pow_duplicate() {
574 const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
576 let first_split = TEMPLATE.rsplit_once("\npow-params").unwrap();
577 let second_split = first_split.1.split_once("\n").unwrap();
578 let test_data_inner = format!(
579 "{}\npow-params{}\npow-params{}\n{}",
580 first_split.0, second_split.0, second_split.0, second_split.1
581 );
582 let err = HsDescInner::parse(&test_data_inner)
583 .map(|_| ())
584 .unwrap_err();
585 assert_eq!(err.kind, crate::NetdocErrorKind::DuplicateToken);
586 }
587
588 #[test]
590 #[cfg(feature = "hs-pow-full")]
591 fn inner_pow_v1_object() {
592 const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
594 let first_split = TEMPLATE.rsplit_once("\npow-params").unwrap();
595 let second_split = first_split.1.split_once("\n").unwrap();
596 let test_data_inner = format!(
597 "{}\npow-params{}\n-----BEGIN THING-----\n-----END THING-----\n{}",
598 first_split.0, second_split.0, second_split.1
599 );
600 let err = HsDescInner::parse(&test_data_inner)
601 .map(|_| ())
602 .unwrap_err();
603 assert_eq!(err.kind, crate::NetdocErrorKind::UnexpectedObject);
604 }
605
606 #[test]
617 fn inner_pow_unrecognized() {
618 const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
620 let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
621 let test_data_inner = format!(
622 "{}\npow-params x-example\npow-params x-example{}",
623 parts.0, parts.1
624 );
625 let desc = HsDescInner::parse(&test_data_inner).unwrap();
626 let pow_params = desc
627 .1
628 .dangerously_into_parts()
629 .0
630 .dangerously_assume_wellsigned()
631 .pow_params;
632 assert_eq!(pow_params.slice().len(), 0);
633 }
634
635 #[test]
637 fn inner_pow_unrecognized_object() {
638 const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
640 let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
641 let test_data_inner = format!(
642 "{}\npow-params x-something-else with args\n-----BEGIN THING-----\n-----END THING-----{}",
643 parts.0, parts.1
644 );
645 let desc = HsDescInner::parse(&test_data_inner).unwrap();
646 let pow_params = desc
647 .1
648 .dangerously_into_parts()
649 .0
650 .dangerously_assume_wellsigned()
651 .pow_params;
652 assert_eq!(pow_params.slice().len(), 0);
653 }
654
655 #[test]
656 fn parse_good() -> Result<()> {
657 let desc = HsDescOuter::parse(TEST_DATA)?
658 .dangerously_assume_wellsigned()
659 .dangerously_assume_timely();
660 let subcred = TEST_SUBCREDENTIAL.into();
661 let body = desc.decrypt_body(&subcred).unwrap();
662 let body = std::str::from_utf8(&body[..]).unwrap();
663
664 let middle = HsDescMiddle::parse(body)?;
665 let inner_body = middle
666 .decrypt_inner(&desc.blinded_id(), desc.revision_counter(), &subcred, None)
667 .unwrap();
668 let inner_body = std::str::from_utf8(&inner_body).unwrap();
669 let (ed_id, inner) = HsDescInner::parse(inner_body)?;
670 let inner = inner
671 .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
672 .unwrap()
673 .check_signature()
674 .unwrap();
675
676 assert_eq!(ed_id.as_ref(), Some(desc.desc_sign_key_id()));
677
678 assert!(inner.intro_auth_types.is_none());
679 assert_eq!(inner.single_onion_service, false);
680 assert_eq!(inner.intro_points.len(), 3);
681
682 let ipt0 = &inner.intro_points[0];
683 assert_eq!(
684 ipt0.ipt_ntor_key().as_bytes(),
685 &hex!("553BF9F9E1979D6F5D5D7D20BB3FE7272E32E22B6E86E35C76A7CA8A377E402F")
686 );
687
688 assert_ne!(ipt0.link_specifiers, inner.intro_points[1].link_specifiers);
689
690 Ok(())
691 }
692}