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