tor_netdoc/doc/hsdesc.rs
1//! Implementation for onion service descriptors.
2//!
3//! An onion service descriptor is a document generated by an onion service and
4//! uploaded to one or more HsDir nodes for clients to later download. It tells
5//! the onion service client where to find the current introduction points for
6//! the onion service, and how to connect to them.
7//!
8//! An onion service descriptor is more complicated than most other
9//! documentation types, because it is partially encrypted.
10
11mod desc_enc;
12
13#[cfg(feature = "hs-service")]
14mod build;
15mod inner;
16mod middle;
17mod outer;
18pub mod pow;
19
20pub use desc_enc::DecryptionError;
21use tor_basic_utils::rangebounds::RangeBoundsExt;
22use tor_error::internal;
23
24use crate::{NetdocErrorKind as EK, Result};
25
26use tor_checkable::signed::{self, SignatureGated};
27use tor_checkable::timed::{self, TimerangeBound};
28use tor_checkable::{SelfSigned, Timebound};
29use tor_hscrypto::pk::{HsBlindId, HsClientDescEncKeypair, HsIntroPtSessionIdKey, HsSvcNtorKey};
30use tor_hscrypto::{RevisionCounter, Subcredential};
31use tor_linkspec::EncodedLinkSpec;
32use tor_llcrypto::pk::curve25519;
33use tor_units::IntegerMinutes;
34
35use derive_builder::Builder;
36use smallvec::SmallVec;
37
38use std::result::Result as StdResult;
39use std::time::SystemTime;
40
41pub use {inner::HsDescInner, middle::HsDescMiddle, outer::HsDescOuter};
42
43#[cfg(feature = "hs-service")]
44pub use build::{HsDescBuilder, create_desc_sign_key_cert};
45
46/// Metadata about an onion service descriptor, as stored at an HsDir.
47///
48/// This object is parsed from the outermost document of an onion service
49/// descriptor, and used on the HsDir to maintain its index. It does not
50/// include the inner documents' information about introduction points, since the
51/// HsDir cannot decrypt those without knowing the onion service's un-blinded
52/// identity.
53///
54/// The HsDir caches this value, along with the original text of the descriptor.
55#[cfg(feature = "hs-dir")]
56#[allow(dead_code)] // TODO RELAY: Remove this.
57pub struct StoredHsDescMeta {
58 /// The blinded onion identity for this descriptor. (This is the only
59 /// identity that the HsDir knows.)
60 blinded_id: HsBlindId,
61
62 /// Information about the expiration and revision counter for this
63 /// descriptor.
64 idx_info: IndexInfo,
65}
66
67/// An unchecked StoredHsDescMeta: parsed, but not checked for liveness or validity.
68#[cfg(feature = "hs-dir")]
69pub type UncheckedStoredHsDescMeta =
70 signed::SignatureGated<timed::TimerangeBound<StoredHsDescMeta>>;
71
72/// Information about how long to hold a given onion service descriptor, and
73/// when to replace it.
74#[derive(Debug, Clone)]
75struct IndexInfo {
76 /// The lifetime in minutes that this descriptor should be held after it is
77 /// received.
78 #[allow(dead_code)] // TODO RELAY: Remove this if there turns out to be no need for it.
79 lifetime: IntegerMinutes<u16>,
80 /// The expiration time on the `descriptor-signing-key-cert` included in this
81 /// descriptor.
82 #[allow(dead_code)] // TODO RELAY: Remove this if there turns out to be no need for it.
83 signing_cert_expires: SystemTime,
84 /// The revision counter on this descriptor: higher values should replace
85 /// older ones.
86 revision: RevisionCounter,
87}
88
89/// A decrypted, decoded onion service descriptor.
90///
91/// This object includes information from both the outer (plaintext) document of
92/// the descriptor, and the inner (encrypted) documents. It tells the client the
93/// information it needs to contact the onion service, including necessary
94/// introduction points and public keys.
95#[derive(Debug, Clone)]
96pub struct HsDesc {
97 /// Information about the expiration and revision counter for this
98 /// descriptor.
99 idx_info: IndexInfo,
100
101 /// The list of authentication types that this onion service supports.
102 auth_required: Option<SmallVec<[IntroAuthType; 2]>>,
103
104 /// If true, this a "single onion service" and is not trying to keep its own location private.
105 is_single_onion_service: bool,
106
107 /// One or more introduction points used to contact the onion service.
108 intro_points: Vec<IntroPointDesc>,
109
110 /// A list of offered proof-of-work parameters, at most one per type.
111 pow_params: pow::PowParamSet,
112 // /// A list of recognized CREATE handshakes that this onion service supports.
113 //
114 // TODO: When someday we add a "create2 format" other than "hs-ntor", we
115 // should turn this into a caret enum, record this info, and expose it.
116 // create2_formats: Vec<u32>,
117}
118
119/// A type of authentication that is required when introducing to an onion
120/// service.
121#[non_exhaustive]
122#[derive(Debug, Clone, Copy, Eq, PartialEq, derive_more::Display)]
123pub enum IntroAuthType {
124 /// Ed25519 authentication is required.
125 #[display("ed25519")]
126 Ed25519,
127}
128
129/// Information in an onion service descriptor about a single
130/// introduction point.
131#[derive(Debug, Clone, amplify::Getters, Builder)]
132#[builder(pattern = "owned")] // mirrors HsDescBuilder
133pub struct IntroPointDesc {
134 /// The list of link specifiers needed to extend a circuit to the introduction point.
135 ///
136 /// These can include public keys and network addresses.
137 ///
138 /// Note that we do not enforce the presence of any link specifiers here;
139 /// this means that you can't assume that an `IntroPointDesc` is a meaningful
140 /// `ChanTarget` without some processing.
141 //
142 // The builder setter takes a `Vec` directly. This seems fine.
143 #[getter(skip)]
144 link_specifiers: Vec<EncodedLinkSpec>,
145
146 /// The key to be used to extend a circuit _to the introduction point_, using the
147 /// ntor or ntor3 handshakes. (`KP_ntor`)
148 #[builder(setter(name = "ipt_kp_ntor"))] // TODO rename the internal variable too
149 ipt_ntor_key: curve25519::PublicKey,
150
151 /// The key to be used to identify the onion service at this introduction point.
152 /// (`KP_hs_ipt_sid`)
153 #[builder(setter(name = "kp_hs_ipt_sid"))] // TODO rename the internal variable too
154 ipt_sid_key: HsIntroPtSessionIdKey,
155
156 /// `KP_hss_ntor`, the key used to encrypt a handshake _to the onion
157 /// service_ when using this introduction point.
158 ///
159 /// The onion service uses a separate key of this type with each
160 /// introduction point as part of its strategy for preventing replay
161 /// attacks.
162 #[builder(setter(name = "kp_hss_ntor"))] // TODO rename the internal variable too
163 svc_ntor_key: HsSvcNtorKey,
164}
165
166/// An onion service after it has been parsed by the client, but not yet decrypted.
167pub struct EncryptedHsDesc {
168 /// The un-decoded outer document of our onion service descriptor.
169 outer_doc: outer::HsDescOuter,
170}
171
172/// An unchecked HsDesc: parsed, but not checked for liveness or validity.
173pub type UncheckedEncryptedHsDesc = signed::SignatureGated<timed::TimerangeBound<EncryptedHsDesc>>;
174
175#[cfg(feature = "hs-dir")]
176impl StoredHsDescMeta {
177 // TODO relay: needs accessor functions too. (Let's not use public fields; we
178 // are likely to want to mess with the repr of these types.)
179
180 /// Parse the outermost layer of the descriptor in `input`, and return the
181 /// resulting metadata (if possible).
182 pub fn parse(input: &str) -> Result<UncheckedStoredHsDescMeta> {
183 let outer = outer::HsDescOuter::parse(input)?;
184 Ok(outer.dangerously_map(|timebound| {
185 timebound.dangerously_map(|outer| StoredHsDescMeta::from_outer_doc(&outer))
186 }))
187 }
188}
189
190impl HsDesc {
191 /// Parse the outermost document of the descriptor in `input`, and validate
192 /// that its identity is consistent with `blinded_onion_id`.
193 ///
194 /// On success, the caller will get a wrapped object which they must
195 /// validate and then decrypt.
196 ///
197 /// Use [`HsDesc::parse_decrypt_validate`] if you just need an [`HsDesc`] and don't want to
198 /// handle the validation/decryption of the wrapped object yourself.
199 ///
200 /// # Example
201 /// ```
202 /// # use hex_literal::hex;
203 /// # use tor_checkable::{SelfSigned, Timebound};
204 /// # use tor_netdoc::doc::hsdesc::HsDesc;
205 /// # use tor_netdoc::Error;
206 /// #
207 /// # let unparsed_desc: &str = include_str!("../../testdata/hsdesc1.txt");
208 /// # let blinded_id =
209 /// # hex!("43cc0d62fc6252f578705ca645a46109e265290343b1137e90189744b20b3f2d").into();
210 /// # let subcredential =
211 /// # hex!("78210A0D2C72BB7A0CAF606BCD938B9A3696894FDDDBC3B87D424753A7E3DF37").into();
212 /// # let timestamp = humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap();
213 /// #
214 /// // Parse the descriptor
215 /// let unchecked_desc = HsDesc::parse(unparsed_desc, &blinded_id)?;
216 /// // Validate the signature and timeliness of the outer document
217 /// let checked_desc = unchecked_desc
218 /// .check_signature()?
219 /// .check_valid_at(×tamp)?;
220 /// // Decrypt the outer and inner layers of the descriptor
221 /// let unchecked_decrypted_desc = checked_desc.decrypt(&subcredential, None)?;
222 /// // Validate the signature and timeliness of the inner document
223 /// let hsdesc = unchecked_decrypted_desc
224 /// .check_valid_at(×tamp)?
225 /// .check_signature()?;
226 /// # Ok::<(), anyhow::Error>(())
227 /// ```
228 pub fn parse(
229 input: &str,
230 // We don't actually need this to parse the HsDesc, but we _do_ need it to prevent
231 // a nasty pattern where we forget to check that we got the right one.
232 blinded_onion_id: &HsBlindId,
233 ) -> Result<UncheckedEncryptedHsDesc> {
234 let outer = outer::HsDescOuter::parse(input)?;
235 let mut id_matches = false;
236 let result = outer.dangerously_map(|timebound| {
237 timebound.dangerously_map(|outer| {
238 id_matches = blinded_onion_id == &outer.blinded_id();
239 EncryptedHsDesc::from_outer_doc(outer)
240 })
241 });
242 if !id_matches {
243 return Err(
244 EK::BadObjectVal.with_msg("onion service descriptor did not have the expected ID")
245 );
246 }
247
248 Ok(result)
249 }
250
251 /// A convenience function for parsing, decrypting and validating HS descriptors.
252 ///
253 /// This function:
254 /// * parses the outermost document of the descriptor in `input`, and validates that its
255 /// identity is consistent with `blinded_onion_id`.
256 /// * decrypts both layers of encryption in the onion service descriptor. If `hsc_desc_enc`
257 /// is provided, we use it to decrypt the inner encryption layer;
258 /// otherwise, we require that
259 /// the inner document is encrypted using the "no restricted discovery" method.
260 /// * checks if both layers are valid at the `valid_at` timestamp
261 /// * validates the signatures on both layers
262 ///
263 /// Returns an error if the descriptor cannot be parsed, or if one of the validation steps
264 /// fails.
265 pub fn parse_decrypt_validate(
266 input: &str,
267 blinded_onion_id: &HsBlindId,
268 valid_at: SystemTime,
269 subcredential: &Subcredential,
270 hsc_desc_enc: Option<&HsClientDescEncKeypair>,
271 ) -> StdResult<TimerangeBound<Self>, HsDescError> {
272 use HsDescError as E;
273 let unchecked_desc = Self::parse(input, blinded_onion_id)
274 .map_err(E::OuterParsing)?
275 .check_signature()
276 .map_err(|e| E::OuterValidation(e.into()))?;
277
278 let (inner_desc, new_bounds) = {
279 // We use is_valid_at and dangerously_into_parts instead of check_valid_at because we
280 // need the time bounds of the outer layer (for computing the intersection with the
281 // time bounds of the inner layer).
282 unchecked_desc
283 .is_valid_at(&valid_at)
284 .map_err(|e| E::OuterValidation(e.into()))?;
285 // It's safe to use dangerously_peek() as we've just checked if unchecked_desc is
286 // valid at the current time
287 let inner_timerangebound = unchecked_desc
288 .dangerously_peek()
289 .decrypt(subcredential, hsc_desc_enc)?;
290
291 let new_bounds = unchecked_desc
292 .intersect(&inner_timerangebound)
293 .map(|(b1, b2)| (b1.cloned(), b2.cloned()));
294
295 (inner_timerangebound, new_bounds)
296 };
297
298 let hsdesc = inner_desc
299 .check_valid_at(&valid_at)
300 .map_err(|e| E::InnerValidation(e.into()))?
301 .check_signature()
302 .map_err(|e| E::InnerValidation(e.into()))?;
303
304 // If we've reached this point, it means the descriptor is valid at specified time. This
305 // means the time bounds of the two layers definitely intersect, so new_bounds **must** be
306 // Some. It is a bug if new_bounds is None.
307 let new_bounds = new_bounds
308 .ok_or_else(|| internal!("failed to compute TimerangeBounds for a valid descriptor"))?;
309
310 Ok(TimerangeBound::new(hsdesc, new_bounds))
311 }
312
313 /// One or more introduction points used to contact the onion service.
314 ///
315 /// Always returns at least one introduction point,
316 /// and never more than [`NUM_INTRO_POINT_MAX`](tor_hscrypto::NUM_INTRO_POINT_MAX).
317 /// (Descriptors which have fewer or more are dealt with during parsing.)
318 ///
319 /// Accessor function.
320 //
321 // TODO: We'd like to derive this, but amplify::Getters would give us &Vec<>,
322 // not &[].
323 //
324 // Perhaps someday we can use derive_deftly, or add as_ref() support?
325 pub fn intro_points(&self) -> &[IntroPointDesc] {
326 &self.intro_points
327 }
328
329 /// Return true if this onion service claims to be a non-anonymous "single
330 /// onion service".
331 ///
332 /// (We should always anonymize our own connection to an onion service.)
333 pub fn is_single_onion_service(&self) -> bool {
334 self.is_single_onion_service
335 }
336
337 /// Return true if this onion service claims that it needs user authentication
338 /// of some kind in its INTRODUCE messages.
339 ///
340 /// (Arti does not currently support sending this kind of authentication.)
341 pub fn requires_intro_authentication(&self) -> bool {
342 self.auth_required.is_some()
343 }
344
345 /// Get a list of offered proof-of-work parameters, at most one per type.
346 pub fn pow_params(&self) -> &[pow::PowParams] {
347 self.pow_params.slice()
348 }
349
350 /// Return the revision counter of this descriptor
351 pub fn revision(&self) -> RevisionCounter {
352 self.idx_info.revision
353 }
354}
355
356/// An error returned by [`HsDesc::parse_decrypt_validate`], indicating what
357/// kind of failure prevented us from validating an onion service descriptor.
358///
359/// This is distinct from [`tor_netdoc::Error`](crate::Error) so that we can
360/// tell errors that could be the HsDir's fault from those that are definitely
361/// protocol violations by the onion service.
362#[derive(Clone, Debug, thiserror::Error)]
363#[non_exhaustive]
364pub enum HsDescError {
365 /// An outer object failed parsing: the HsDir should probably have
366 /// caught this, and not given us this HsDesc.
367 ///
368 /// (This can be an innocent error if we happen to know about restrictions
369 /// that the HsDir does not).
370 #[error("Parsing failure on outer layer of an onion service descriptor.")]
371 OuterParsing(#[source] crate::Error),
372
373 /// An outer object failed validation: the HsDir should probably have
374 /// caught this, and not given us this HsDesc.
375 ///
376 /// (This can happen erroneously if we think that something is untimely but
377 /// the HSDir's clock is slightly different, or _was_ different when it
378 /// decided to give us this object.)
379 #[error("Validation failure on outer layer of an onion service descriptor.")]
380 OuterValidation(#[source] crate::Error),
381
382 /// Decrypting the inner layer failed because we need to have a decryption key,
383 /// but we didn't provide one.
384 ///
385 /// This is probably our fault.
386 #[error("Decryption failure on onion service descriptor: missing decryption key")]
387 MissingDecryptionKey,
388
389 /// Decrypting the inner layer failed because, although we provided a key,
390 /// we did not provide the key we need to decrypt it.
391 ///
392 /// This is probably our fault.
393 #[error("Decryption failure on onion service descriptor: incorrect decryption key")]
394 WrongDecryptionKey,
395
396 /// Decrypting the inner or middle layer failed because of an issue with the
397 /// decryption itself.
398 ///
399 /// This is the onion service's fault.
400 #[error("Decryption failure on onion service descriptor: could not decrypt")]
401 DecryptionFailed,
402
403 /// We failed to parse something cryptographic in an inner layer of the
404 /// onion service descriptor.
405 ///
406 /// This is definitely the onion service's fault.
407 #[error("Parsing failure on inner layer of an onion service descriptor")]
408 InnerParsing(#[source] crate::Error),
409
410 /// We failed to validate something cryptographic in an inner layer of the
411 /// onion service descriptor.
412 ///
413 /// This is definitely the onion service's fault.
414 #[error("Validation failure on inner layer of an onion service descriptor")]
415 InnerValidation(#[source] crate::Error),
416
417 /// We encountered an internal error.
418 #[error("Internal error: {0}")]
419 Bug(#[from] tor_error::Bug),
420}
421
422impl tor_error::HasKind for HsDescError {
423 fn kind(&self) -> tor_error::ErrorKind {
424 use HsDescError as E;
425 use tor_error::ErrorKind as EK;
426 match self {
427 E::OuterParsing(_) | E::OuterValidation(_) => EK::TorProtocolViolation,
428 E::MissingDecryptionKey => EK::OnionServiceMissingClientAuth,
429 E::WrongDecryptionKey => EK::OnionServiceWrongClientAuth,
430 E::DecryptionFailed | E::InnerParsing(_) | E::InnerValidation(_) => {
431 EK::OnionServiceProtocolViolation
432 }
433 E::Bug(e) => e.kind(),
434 }
435 }
436}
437
438impl HsDescError {
439 /// Return true if this error is one that we should report as a suspicious event.
440 ///
441 /// Note that this is a defense-in-depth check
442 /// for resisting descriptor-length inflation attacks:
443 /// Our limits on total download size and/or total cell counts are the defense
444 /// that really matters.
445 /// (See prop360 for more information.)
446 pub fn should_report_as_suspicious(&self) -> bool {
447 use crate::NetdocErrorKind as EK;
448 use HsDescError as E;
449 #[allow(clippy::match_like_matches_macro)]
450 match self {
451 E::OuterParsing(e) => match e.netdoc_error_kind() {
452 EK::ExtraneousSpace => true,
453 EK::WrongEndingToken => true,
454 EK::MissingKeyword => true,
455 _ => false,
456 },
457 E::OuterValidation(e) => match e.netdoc_error_kind() {
458 EK::BadSignature => true,
459 _ => false,
460 },
461 E::MissingDecryptionKey => false,
462 E::WrongDecryptionKey => false,
463 E::DecryptionFailed => false,
464 E::InnerParsing(_) => false,
465 E::InnerValidation(_) => false,
466 E::Bug(_) => false,
467 }
468 }
469}
470
471impl IntroPointDesc {
472 /// Start building a description of an intro point
473 pub fn builder() -> IntroPointDescBuilder {
474 IntroPointDescBuilder::default()
475 }
476
477 /// The list of link specifiers needed to extend a circuit to the introduction point.
478 ///
479 /// These can include public keys and network addresses.
480 ///
481 /// Accessor function.
482 //
483 // TODO: It would be better to derive this too, but this accessor needs to
484 // return a slice; Getters can only give us a &Vec<> in this case.
485 pub fn link_specifiers(&self) -> &[EncodedLinkSpec] {
486 &self.link_specifiers
487 }
488}
489
490impl EncryptedHsDesc {
491 /// Attempt to decrypt both layers of encryption in this onion service
492 /// descriptor.
493 ///
494 /// If `hsc_desc_enc` is provided, we use it to decrypt the inner encryption layer;
495 /// otherwise, we require that the inner document is encrypted using the "no
496 /// restricted discovery" method.
497 //
498 // TODO: Someday we _might_ want to allow a list of keypairs in place of
499 // `hs_desc_enc`. For now, though, we always know a single key that we want
500 // to try using, and we don't want to leak any extra information by
501 // providing other keys that _might_ work. We certainly don't want to
502 // encourage people to provide every key they know.
503 pub fn decrypt(
504 &self,
505 subcredential: &Subcredential,
506 hsc_desc_enc: Option<&HsClientDescEncKeypair>,
507 ) -> StdResult<TimerangeBound<SignatureGated<HsDesc>>, HsDescError> {
508 use HsDescError as E;
509 let blinded_id = self.outer_doc.blinded_id();
510 let revision_counter = self.outer_doc.revision_counter();
511 let kp_desc_sign = self.outer_doc.desc_sign_key_id();
512
513 // Decrypt the superencryption layer; parse the middle document.
514 let middle = self
515 .outer_doc
516 .decrypt_body(subcredential)
517 .map_err(|_| E::DecryptionFailed)?;
518 let middle = std::str::from_utf8(&middle[..]).map_err(|_| {
519 E::InnerParsing(EK::BadObjectVal.with_msg("Bad utf-8 in middle document"))
520 })?;
521 let middle = middle::HsDescMiddle::parse(middle).map_err(E::InnerParsing)?;
522
523 // Decrypt the encryption layer and parse the inner document.
524 let inner = middle.decrypt_inner(
525 &blinded_id,
526 revision_counter,
527 subcredential,
528 hsc_desc_enc.map(|keys| keys.secret()),
529 )?;
530 let inner = std::str::from_utf8(&inner[..]).map_err(|_| {
531 E::InnerParsing(EK::BadObjectVal.with_msg("Bad utf-8 in inner document"))
532 })?;
533 let (cert_signing_key, time_bound) =
534 inner::HsDescInner::parse(inner).map_err(E::InnerParsing)?;
535
536 if cert_signing_key.as_ref() != Some(kp_desc_sign) {
537 return Err(E::InnerValidation(EK::BadObjectVal.with_msg(
538 "Signing keys in inner document did not match those in outer document",
539 )));
540 }
541
542 // Construct the HsDesc!
543 let time_bound = time_bound.dangerously_map(|sig_bound| {
544 sig_bound.dangerously_map(|inner| HsDesc {
545 idx_info: IndexInfo::from_outer_doc(&self.outer_doc),
546 auth_required: inner.intro_auth_types,
547 is_single_onion_service: inner.single_onion_service,
548 intro_points: inner.intro_points,
549 pow_params: inner.pow_params,
550 })
551 });
552 Ok(time_bound)
553 }
554
555 /// Create a new `IndexInfo` from the outer part of an onion service descriptor.
556 fn from_outer_doc(outer_layer: outer::HsDescOuter) -> Self {
557 EncryptedHsDesc {
558 outer_doc: outer_layer,
559 }
560 }
561}
562
563impl IndexInfo {
564 /// Create a new `IndexInfo` from the outer part of an onion service descriptor.
565 fn from_outer_doc(outer: &outer::HsDescOuter) -> Self {
566 IndexInfo {
567 lifetime: outer.lifetime,
568 signing_cert_expires: outer.desc_signing_key_cert.expiry(),
569 revision: outer.revision_counter(),
570 }
571 }
572}
573
574#[cfg(feature = "hs-dir")]
575impl StoredHsDescMeta {
576 /// Create a new `StoredHsDescMeta` from the outer part of an onion service descriptor.
577 fn from_outer_doc(outer: &outer::HsDescOuter) -> Self {
578 let blinded_id = outer.blinded_id();
579 let idx_info = IndexInfo::from_outer_doc(outer);
580 StoredHsDescMeta {
581 blinded_id,
582 idx_info,
583 }
584 }
585}
586
587/// Test data
588#[cfg(any(test, feature = "testing"))]
589#[allow(missing_docs)]
590#[allow(clippy::missing_docs_in_private_items)]
591#[allow(clippy::unwrap_used)]
592pub mod test_data {
593 use super::*;
594 use hex_literal::hex;
595
596 pub const TEST_DATA: &str = include_str!("../../testdata/hsdesc1.txt");
597
598 pub const TEST_SUBCREDENTIAL: [u8; 32] =
599 hex!("78210A0D2C72BB7A0CAF606BCD938B9A3696894FDDDBC3B87D424753A7E3DF37");
600
601 // This HsDesc uses DescEnc authentication.
602 pub const TEST_DATA_2: &str = include_str!("../../testdata/hsdesc2.txt");
603 pub const TEST_DATA_TIMEPERIOD_2: u64 = 19397;
604 // paozpdhgz2okvc6kgbxvh2bnfsmt4xergrtcl4obkhopyvwxkpjzvoad.onion
605 pub const TEST_HSID_2: [u8; 32] =
606 hex!("781D978CE6CE9CAA8BCA306F53E82D2C993E5C91346625F1C151DCFC56D753D3");
607 pub const TEST_SUBCREDENTIAL_2: [u8; 32] =
608 hex!("24A133E905102BDA9A6AFE57F901366A1B8281865A91F1FE0853E4B50CC8B070");
609 // SACGOAEODFGCYY22NYZV45ZESFPFLDGLMBWFACKEO34XGHASSAMQ (base32)
610 pub const TEST_PUBKEY_2: [u8; 32] =
611 hex!("900467008E194C2C635A6E335E7724915E558CCB606C50094476F9731C129019");
612 // SDZNMD4RP4SCH4EYTTUZPFRZINNFWAOPPKZ6BINZAC7LREV24RBQ (base32)
613 pub const TEST_SECKEY_2: [u8; 32] =
614 hex!("90F2D60F917F2423F0989CE9979639435A5B01CF7AB3E0A1B900BEB892BAE443");
615
616 /// K_hs_blind_id that can be used to parse [`TEST_DATA`]
617 ///
618 /// `pub(crate)` mostly because it's difficult to describe what TP it's for.
619 pub(crate) const TEST_DATA_HS_BLIND_ID: [u8; 32] =
620 hex!("43cc0d62fc6252f578705ca645a46109e265290343b1137e90189744b20b3f2d");
621
622 /// Obtain a testing [`HsDesc`]
623 pub fn test_parsed_hsdesc() -> Result<HsDesc> {
624 let blinded_id = TEST_DATA_HS_BLIND_ID.into();
625
626 let desc = HsDesc::parse(TEST_DATA, &blinded_id)?
627 .check_signature()?
628 .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
629 .unwrap()
630 .decrypt(&TEST_SUBCREDENTIAL.into(), None)
631 .unwrap();
632 let desc = desc
633 .check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap())
634 .unwrap();
635 let desc = desc.check_signature().unwrap();
636 Ok(desc)
637 }
638}
639
640#[cfg(test)]
641mod test {
642 // @@ begin test lint list maintained by maint/add_warning @@
643 #![allow(clippy::bool_assert_comparison)]
644 #![allow(clippy::clone_on_copy)]
645 #![allow(clippy::dbg_macro)]
646 #![allow(clippy::mixed_attributes_style)]
647 #![allow(clippy::print_stderr)]
648 #![allow(clippy::print_stdout)]
649 #![allow(clippy::single_char_pattern)]
650 #![allow(clippy::unwrap_used)]
651 #![allow(clippy::unchecked_time_subtraction)]
652 #![allow(clippy::useless_vec)]
653 #![allow(clippy::needless_pass_by_value)]
654 //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
655 use std::time::Duration;
656
657 use super::test_data::*;
658 use super::*;
659 use hex_literal::hex;
660 use tor_hscrypto::{pk::HsIdKey, time::TimePeriod};
661 use tor_llcrypto::pk::ed25519;
662
663 #[test]
664 #[cfg(feature = "hs-dir")]
665 fn parse_meta_good() -> Result<()> {
666 let meta = StoredHsDescMeta::parse(TEST_DATA)?
667 .check_signature()?
668 .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
669 .unwrap();
670
671 assert_eq!(meta.blinded_id.as_ref(), &TEST_DATA_HS_BLIND_ID);
672 assert_eq!(
673 Duration::try_from(meta.idx_info.lifetime).unwrap(),
674 Duration::from_secs(60 * 180)
675 );
676 assert_eq!(
677 meta.idx_info.signing_cert_expires,
678 humantime::parse_rfc3339("2023-01-26T03:00:00Z").unwrap()
679 );
680 assert_eq!(meta.idx_info.revision, RevisionCounter::from(19655750));
681
682 Ok(())
683 }
684
685 #[test]
686 fn parse_desc_good() -> Result<()> {
687 let wrong_blinded_id = [12; 32].into();
688 let desc = HsDesc::parse(TEST_DATA, &wrong_blinded_id);
689 assert!(desc.is_err());
690 let desc = test_parsed_hsdesc()?;
691
692 assert_eq!(
693 Duration::try_from(desc.idx_info.lifetime).unwrap(),
694 Duration::from_secs(60 * 180)
695 );
696 assert_eq!(
697 desc.idx_info.signing_cert_expires,
698 humantime::parse_rfc3339("2023-01-26T03:00:00Z").unwrap()
699 );
700 assert_eq!(desc.idx_info.revision, RevisionCounter::from(19655750));
701 assert!(desc.auth_required.is_none());
702 assert_eq!(desc.is_single_onion_service, false);
703 assert_eq!(desc.intro_points.len(), 3);
704
705 let ipt0 = &desc.intro_points()[0];
706 assert_eq!(
707 ipt0.ipt_ntor_key().as_bytes(),
708 &hex!("553BF9F9E1979D6F5D5D7D20BB3FE7272E32E22B6E86E35C76A7CA8A377E402F")
709 );
710 // TODO TEST: Perhaps add tests for other intro point fields.
711
712 Ok(())
713 }
714
715 /// Get an EncryptedHsDesc corresponding to `TEST_DATA_2`.
716 fn get_test2_encrypted() -> EncryptedHsDesc {
717 let id: HsIdKey = ed25519::PublicKey::from_bytes(&TEST_HSID_2).unwrap().into();
718 let period = TimePeriod::new(
719 humantime::parse_duration("24 hours").unwrap(),
720 humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap(),
721 humantime::parse_duration("12 hours").unwrap(),
722 )
723 .unwrap();
724 assert_eq!(period.interval_num(), TEST_DATA_TIMEPERIOD_2);
725 let (blind_id, subcredential) = id.compute_blinded_key(period).unwrap();
726
727 assert_eq!(
728 blind_id.as_bytes(),
729 &hex!("706628758208395D461AA0F460A5E76E7B828C66B5E794768592B451302E961D")
730 );
731
732 assert_eq!(subcredential.as_ref(), &TEST_SUBCREDENTIAL_2);
733
734 HsDesc::parse(TEST_DATA_2, &blind_id.into())
735 .unwrap()
736 .check_signature()
737 .unwrap()
738 .check_valid_at(&humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap())
739 .unwrap()
740 }
741
742 #[test]
743 fn parse_desc_auth_missing() {
744 // If we try to decrypt TEST_DATA_2 with no ClientDescEncKey, we get a
745 // failure.
746 let encrypted = get_test2_encrypted();
747 let subcredential = TEST_SUBCREDENTIAL_2.into();
748 let with_no_auth = encrypted.decrypt(&subcredential, None);
749 assert!(with_no_auth.is_err());
750 }
751
752 #[test]
753 fn parse_desc_auth_good() {
754 // But if we try to decrypt TEST_DATA_2 with the correct ClientDescEncKey, we get a
755 // the data inside!
756
757 let encrypted = get_test2_encrypted();
758 let subcredential = TEST_SUBCREDENTIAL_2.into();
759 let pk = curve25519::PublicKey::from(TEST_PUBKEY_2).into();
760 let sk = curve25519::StaticSecret::from(TEST_SECKEY_2).into();
761 let desc = encrypted
762 .decrypt(&subcredential, Some(&HsClientDescEncKeypair::new(pk, sk)))
763 .unwrap();
764 let desc = desc
765 .check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap())
766 .unwrap();
767 let desc = desc.check_signature().unwrap();
768 assert_eq!(desc.intro_points.len(), 3);
769 }
770}