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::{UnvalidatedEdCert, B64};
11use crate::{NetdocErrorKind as EK, Result};
12
13use itertools::Itertools as _;
14use once_cell::sync::Lazy;
15use smallvec::SmallVec;
16use tor_checkable::signed::SignatureGated;
17use tor_checkable::timed::TimerangeBound;
18use tor_checkable::Timebound;
19use tor_hscrypto::pk::{HsIntroPtSessionIdKey, HsSvcNtorKey};
20use tor_hscrypto::NUM_INTRO_POINT_MAX;
21use tor_llcrypto::pk::ed25519::Ed25519Identity;
22use tor_llcrypto::pk::{curve25519, ed25519, ValidatableSignature};
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: Lazy<SectionRules<HsInnerKwd>> = Lazy::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: Lazy<SectionRules<HsInnerKwd>> = Lazy::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 fn take_from_reader(
208 input: &mut NetDocReader<'_, HsInnerKwd>,
209 ) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
210 use HsInnerKwd::*;
211
212 let mut sections =
214 input.batching_split_before_with_header(|item| item.is_ok_with_kwd(INTRODUCTION_POINT));
215 let header = HS_INNER_HEADER_RULES.parse(&mut sections)?;
217
218 {
221 let tok = header.required(CREATE2_FORMATS)?;
222 if !tok.args().any(|s| s == "2") {
227 return Err(EK::BadArgument
228 .at_pos(tok.pos())
229 .with_msg("Onion service descriptor does not support ntor handshake."));
230 }
231 }
232 let auth_types = if let Some(tok) = header.get(INTRO_AUTH_REQUIRED) {
235 let mut auth_types: SmallVec<[IntroAuthType; 2]> = SmallVec::new();
236 let mut push = |at| {
237 if !auth_types.contains(&at) {
238 auth_types.push(at);
239 }
240 };
241 for arg in tok.args() {
242 #[allow(clippy::single_match)]
243 match arg {
244 "ed25519" => push(IntroAuthType::Ed25519),
245 _ => (), }
247 }
248 if auth_types.is_empty() {
250 return Err(EK::BadArgument
251 .at_pos(tok.pos())
252 .with_msg("No recognized introduction authentication methods."));
253 }
254
255 Some(auth_types)
256 } else {
257 None
258 };
259
260 let is_single_onion_service = header.get(SINGLE_ONION_SERVICE).is_some();
262
263 let pow_params = PowParamSet::from_items(header.slice(POW_PARAMS))?;
265
266 let mut signatures = Vec::new();
267 let mut expirations = Vec::new();
268 let mut cert_signing_key: Option<Ed25519Identity> = None;
269
270 let mut intro_points = Vec::new();
274 let mut sections = sections.subsequent();
275 while let Some(mut ipt_section) = sections.next_batch() {
276 let ipt_section = HS_INNER_INTRO_RULES.parse(&mut ipt_section)?;
277
278 let link_specifiers = {
280 let tok = ipt_section.required(INTRODUCTION_POINT)?;
281 let ls = tok.parse_arg::<B64>(0)?;
282 let mut r = tor_bytes::Reader::from_slice(ls.as_bytes());
283 let n = r.take_u8()?;
284 let res = r.extract_n(n.into())?;
285 r.should_be_exhausted()?;
286 res
287 };
288
289 let ntor_onion_key = {
291 let tok = ipt_section
292 .slice(ONION_KEY)
293 .iter()
294 .filter(|item| item.arg(0) == Some("ntor"))
295 .exactly_one()
296 .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
297 tok.parse_arg::<B64>(1)?.into_array()?.into()
298 };
299
300 let auth_key: HsIntroPtSessionIdKey = {
303 let tok = ipt_section.required(AUTH_KEY)?;
318 let InnerCertData {
319 signing_key,
320 subject_key,
321 signature,
322 expiry,
323 } = handle_inner_certificate(
324 tok,
325 "ED25519 CERT",
326 tor_cert::CertType::HS_IP_V_SIGNING,
327 )?;
328 expirations.push(expiry);
329 signatures.push(signature);
330 if cert_signing_key.get_or_insert(signing_key) != &signing_key {
331 return Err(EK::BadObjectVal
332 .at_pos(tok.pos())
333 .with_msg("Mismatched signing key"));
334 }
335
336 subject_key.into()
337 };
338
339 let svc_ntor_key: HsSvcNtorKey = {
343 let tok = ipt_section
344 .slice(ENC_KEY)
345 .iter()
346 .filter(|item| item.arg(0) == Some("ntor"))
347 .exactly_one()
348 .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
349 let key = curve25519::PublicKey::from(tok.parse_arg::<B64>(1)?.into_array()?);
350 key.into()
351 };
352
353 {
356 let tok = ipt_section.required(ENC_KEY_CERT)?;
359 let InnerCertData {
360 signing_key,
361 subject_key,
362 signature,
363 expiry,
364 } = handle_inner_certificate(
365 tok,
366 "ED25519 CERT",
367 tor_cert::CertType::HS_IP_CC_SIGNING,
368 )?;
369 expirations.push(expiry);
370 signatures.push(signature);
371
372 let sign_bit = 0;
377 let expected_ed_key =
378 tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public(
379 &svc_ntor_key,
380 sign_bit,
381 );
382 if expected_ed_key != Some(subject_key) {
383 return Err(EK::BadObjectVal
384 .at_pos(tok.pos())
385 .with_msg("Mismatched subject key"));
386 }
387
388 if cert_signing_key.get_or_insert(signing_key) != &signing_key {
390 return Err(EK::BadObjectVal
391 .at_pos(tok.pos())
392 .with_msg("Mismatched signing key"));
393 }
394 };
395
396 if intro_points.len() < NUM_INTRO_POINT_MAX {
405 intro_points.push(IntroPointDesc {
406 link_specifiers,
407 ipt_ntor_key: ntor_onion_key,
408 ipt_sid_key: auth_key,
409 svc_ntor_key,
410 });
411 }
412 }
413
414 if intro_points.is_empty() {
422 return Err(EK::MissingEntry.with_msg("no introduction points"));
423 }
424
425 let inner = HsDescInner {
426 intro_auth_types: auth_types,
427 single_onion_service: is_single_onion_service,
428 pow_params,
429 intro_points,
430 };
431 let sig_gated = SignatureGated::new(inner, signatures);
432 let time_bound = match expirations.iter().min() {
433 Some(t) => TimerangeBound::new(sig_gated, ..t),
434 None => TimerangeBound::new(sig_gated, ..),
435 };
436
437 Ok((cert_signing_key, time_bound))
438 }
439}
440
441#[cfg(test)]
442mod test {
443 #![allow(clippy::bool_assert_comparison)]
445 #![allow(clippy::clone_on_copy)]
446 #![allow(clippy::dbg_macro)]
447 #![allow(clippy::mixed_attributes_style)]
448 #![allow(clippy::print_stderr)]
449 #![allow(clippy::print_stdout)]
450 #![allow(clippy::single_char_pattern)]
451 #![allow(clippy::unwrap_used)]
452 #![allow(clippy::unchecked_duration_subtraction)]
453 #![allow(clippy::useless_vec)]
454 #![allow(clippy::needless_pass_by_value)]
455 use std::{iter, time::Duration};
458
459 use hex_literal::hex;
460 use itertools::chain;
461 use tor_checkable::{SelfSigned, Timebound};
462
463 use super::*;
464 use crate::doc::hsdesc::{
465 middle::HsDescMiddle,
466 outer::HsDescOuter,
467 pow::PowParams,
468 test_data::{TEST_DATA, TEST_SUBCREDENTIAL},
469 };
470
471 #[test]
474 fn inner_text() {
475 const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner.txt");
477
478 use crate::NetdocErrorKind as NEK;
479 let _desc = HsDescInner::parse(TEST_DATA_INNER).unwrap();
480
481 let none = format!(
482 "{}\n",
483 TEST_DATA_INNER
484 .split_once("\nintroduction-point")
485 .unwrap()
486 .0,
487 );
488 let err = HsDescInner::parse(&none).map(|_| &none).unwrap_err();
489 assert_eq!(err.kind, NEK::MissingEntry);
490
491 let ipt = format!(
492 "introduction-point{}",
493 TEST_DATA_INNER
494 .rsplit_once("\nintroduction-point")
495 .unwrap()
496 .1,
497 );
498 for n in NUM_INTRO_POINT_MAX..NUM_INTRO_POINT_MAX + 2 {
499 let many =
500 chain!(iter::once(&*none), std::iter::repeat_n(&*ipt, n),).collect::<String>();
501 let desc = HsDescInner::parse(&many).unwrap();
502 let desc = desc
503 .1
504 .dangerously_into_parts()
505 .0
506 .dangerously_assume_wellsigned();
507 assert_eq!(desc.intro_points.len(), NUM_INTRO_POINT_MAX);
508 }
509 }
510
511 #[test]
513 #[cfg(feature = "hs-pow-full")]
514 fn inner_c_pow_v1() {
515 const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
516 let desc = HsDescInner::parse(TEST_DATA_INNER).unwrap();
517 let pow_params = desc
518 .1
519 .dangerously_into_parts()
520 .0
521 .dangerously_assume_wellsigned()
522 .pow_params;
523 assert_eq!(pow_params.slice().len(), 1);
524 match &pow_params.slice()[0] {
525 PowParams::V1(v1) => {
526 let expected_effort: tor_hscrypto::pow::v1::Effort = 614.into();
527 let expected_seed: tor_hscrypto::pow::v1::Seed =
528 hex!("144e901df0841833a6e8592190849b4412f307d1565f2f137b2a5bc21a31092a").into();
529 let expected_expiry = Some(SystemTime::UNIX_EPOCH + Duration::new(1712812537, 0));
530 assert_eq!(v1.suggested_effort(), expected_effort);
531 assert_eq!(
532 v1.seed().to_owned().dangerously_assume_timely(),
533 expected_seed
534 );
535 assert_eq!(v1.seed().bounds().1, expected_expiry);
536 }
537 #[allow(unreachable_patterns)]
538 _ => unreachable!(),
539 }
540 }
541
542 #[test]
544 fn inner_c_pow_v1_with_unknown() {
545 const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
546 let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
547 let test_data_inner = format!("{}\npow-params x-example\npow-params{}", parts.0, parts.1);
548 let desc = HsDescInner::parse(&test_data_inner).unwrap();
549 let pow_params = desc
550 .1
551 .dangerously_into_parts()
552 .0
553 .dangerously_assume_wellsigned()
554 .pow_params;
555 assert_eq!(pow_params.slice().len(), 1);
556 }
557
558 #[test]
560 fn inner_pow_empty() {
561 const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
562 let err = HsDescInner::parse(TEST_DATA_INNER).map(|_| ()).unwrap_err();
563 assert_eq!(err.kind, crate::NetdocErrorKind::TooFewArguments);
564 }
565
566 #[test]
568 fn inner_pow_duplicate() {
569 const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
571 let first_split = TEMPLATE.rsplit_once("\npow-params").unwrap();
572 let second_split = first_split.1.split_once("\n").unwrap();
573 let test_data_inner = format!(
574 "{}\npow-params{}\npow-params{}\n{}",
575 first_split.0, second_split.0, second_split.0, second_split.1
576 );
577 let err = HsDescInner::parse(&test_data_inner)
578 .map(|_| ())
579 .unwrap_err();
580 assert_eq!(err.kind, crate::NetdocErrorKind::DuplicateToken);
581 }
582
583 #[test]
585 #[cfg(feature = "hs-pow-full")]
586 fn inner_pow_v1_object() {
587 const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
589 let first_split = TEMPLATE.rsplit_once("\npow-params").unwrap();
590 let second_split = first_split.1.split_once("\n").unwrap();
591 let test_data_inner = format!(
592 "{}\npow-params{}\n-----BEGIN THING-----\n-----END THING-----\n{}",
593 first_split.0, second_split.0, second_split.1
594 );
595 let err = HsDescInner::parse(&test_data_inner)
596 .map(|_| ())
597 .unwrap_err();
598 assert_eq!(err.kind, crate::NetdocErrorKind::UnexpectedObject);
599 }
600
601 #[test]
612 fn inner_pow_unrecognized() {
613 const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
615 let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
616 let test_data_inner = format!(
617 "{}\npow-params x-example\npow-params x-example{}",
618 parts.0, parts.1
619 );
620 let desc = HsDescInner::parse(&test_data_inner).unwrap();
621 let pow_params = desc
622 .1
623 .dangerously_into_parts()
624 .0
625 .dangerously_assume_wellsigned()
626 .pow_params;
627 assert_eq!(pow_params.slice().len(), 0);
628 }
629
630 #[test]
632 fn inner_pow_unrecognized_object() {
633 const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
635 let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
636 let test_data_inner = format!(
637 "{}\npow-params x-something-else with args\n-----BEGIN THING-----\n-----END THING-----{}",
638 parts.0, parts.1
639 );
640 let desc = HsDescInner::parse(&test_data_inner).unwrap();
641 let pow_params = desc
642 .1
643 .dangerously_into_parts()
644 .0
645 .dangerously_assume_wellsigned()
646 .pow_params;
647 assert_eq!(pow_params.slice().len(), 0);
648 }
649
650 #[test]
651 fn parse_good() -> Result<()> {
652 let desc = HsDescOuter::parse(TEST_DATA)?
653 .dangerously_assume_wellsigned()
654 .dangerously_assume_timely();
655 let subcred = TEST_SUBCREDENTIAL.into();
656 let body = desc.decrypt_body(&subcred).unwrap();
657 let body = std::str::from_utf8(&body[..]).unwrap();
658
659 let middle = HsDescMiddle::parse(body)?;
660 let inner_body = middle
661 .decrypt_inner(&desc.blinded_id(), desc.revision_counter(), &subcred, None)
662 .unwrap();
663 let inner_body = std::str::from_utf8(&inner_body).unwrap();
664 let (ed_id, inner) = HsDescInner::parse(inner_body)?;
665 let inner = inner
666 .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
667 .unwrap()
668 .check_signature()
669 .unwrap();
670
671 assert_eq!(ed_id.as_ref(), Some(desc.desc_sign_key_id()));
672
673 assert!(inner.intro_auth_types.is_none());
674 assert_eq!(inner.single_onion_service, false);
675 assert_eq!(inner.intro_points.len(), 3);
676
677 let ipt0 = &inner.intro_points[0];
678 assert_eq!(
679 ipt0.ipt_ntor_key().as_bytes(),
680 &hex!("553BF9F9E1979D6F5D5D7D20BB3FE7272E32E22B6E86E35C76A7CA8A377E402F")
681 );
682
683 assert_ne!(ipt0.link_specifiers, inner.intro_points[1].link_specifiers);
684
685 Ok(())
686 }
687}