Skip to main content

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)]
75#[allow(dead_code)] // TODO RELAY: Remove this if there turns out to be no need for it.
76struct IndexInfo {
77    /// The lifetime in minutes that this descriptor should be held after it is
78    /// received.
79    lifetime: IntegerMinutes<u16>,
80    /// The expiration time on the `descriptor-signing-key-cert` included in this
81    /// descriptor.
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    #[allow(dead_code)] // TODO RELAY: Remove this if there turns out to be no need for it.
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(&timestamp)?;
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(&timestamp)?
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
351/// An error returned by [`HsDesc::parse_decrypt_validate`], indicating what
352/// kind of failure prevented us from validating an onion service descriptor.
353///
354/// This is distinct from [`tor_netdoc::Error`](crate::Error) so that we can
355/// tell errors that could be the HsDir's fault from those that are definitely
356/// protocol violations by the onion service.
357#[derive(Clone, Debug, thiserror::Error)]
358#[non_exhaustive]
359pub enum HsDescError {
360    /// An outer object failed parsing: the HsDir should probably have
361    /// caught this, and not given us this HsDesc.
362    ///
363    /// (This can be an innocent error if we happen to know about restrictions
364    /// that the HsDir does not).
365    #[error("Parsing failure on outer layer of an onion service descriptor.")]
366    OuterParsing(#[source] crate::Error),
367
368    /// An outer object failed validation: the HsDir should probably have
369    /// caught this, and not given us this HsDesc.
370    ///
371    /// (This can happen erroneously if we think that something is untimely but
372    /// the HSDir's clock is slightly different, or _was_ different when it
373    /// decided to give us this object.)
374    #[error("Validation failure on outer layer of an onion service descriptor.")]
375    OuterValidation(#[source] crate::Error),
376
377    /// Decrypting the inner layer failed because we need to have a decryption key,
378    /// but we didn't provide one.
379    ///
380    /// This is probably our fault.
381    #[error("Decryption failure on onion service descriptor: missing decryption key")]
382    MissingDecryptionKey,
383
384    /// Decrypting the inner layer failed because, although we provided a key,
385    /// we did not provide the key we need to decrypt it.
386    ///
387    /// This is probably our fault.
388    #[error("Decryption failure on onion service descriptor: incorrect decryption key")]
389    WrongDecryptionKey,
390
391    /// Decrypting the inner or middle layer failed because of an issue with the
392    /// decryption itself.
393    ///
394    /// This is the onion service's fault.
395    #[error("Decryption failure on onion service descriptor: could not decrypt")]
396    DecryptionFailed,
397
398    /// We failed to parse something cryptographic in an inner layer of the
399    /// onion service descriptor.
400    ///
401    /// This is definitely the onion service's fault.
402    #[error("Parsing failure on inner layer of an onion service descriptor")]
403    InnerParsing(#[source] crate::Error),
404
405    /// We failed to validate something cryptographic in an inner layer of the
406    /// onion service descriptor.
407    ///
408    /// This is definitely the onion service's fault.
409    #[error("Validation failure on inner layer of an onion service descriptor")]
410    InnerValidation(#[source] crate::Error),
411
412    /// We encountered an internal error.
413    #[error("Internal error: {0}")]
414    Bug(#[from] tor_error::Bug),
415}
416
417impl tor_error::HasKind for HsDescError {
418    fn kind(&self) -> tor_error::ErrorKind {
419        use HsDescError as E;
420        use tor_error::ErrorKind as EK;
421        match self {
422            E::OuterParsing(_) | E::OuterValidation(_) => EK::TorProtocolViolation,
423            E::MissingDecryptionKey => EK::OnionServiceMissingClientAuth,
424            E::WrongDecryptionKey => EK::OnionServiceWrongClientAuth,
425            E::DecryptionFailed | E::InnerParsing(_) | E::InnerValidation(_) => {
426                EK::OnionServiceProtocolViolation
427            }
428            E::Bug(e) => e.kind(),
429        }
430    }
431}
432
433impl HsDescError {
434    /// Return true if this error is one that we should report as a suspicious event.
435    ///
436    /// Note that this is a defense-in-depth check
437    /// for resisting descriptor-length inflation attacks:
438    /// Our limits on total download size and/or total cell counts are the defense
439    /// that really matters.
440    /// (See prop360 for more information.)
441    pub fn should_report_as_suspicious(&self) -> bool {
442        use crate::NetdocErrorKind as EK;
443        use HsDescError as E;
444        #[allow(clippy::match_like_matches_macro)]
445        match self {
446            E::OuterParsing(e) => match e.netdoc_error_kind() {
447                EK::ExtraneousSpace => true,
448                EK::WrongEndingToken => true,
449                EK::MissingKeyword => true,
450                _ => false,
451            },
452            E::OuterValidation(e) => match e.netdoc_error_kind() {
453                EK::BadSignature => true,
454                _ => false,
455            },
456            E::MissingDecryptionKey => false,
457            E::WrongDecryptionKey => false,
458            E::DecryptionFailed => false,
459            E::InnerParsing(_) => false,
460            E::InnerValidation(_) => false,
461            E::Bug(_) => false,
462        }
463    }
464}
465
466impl IntroPointDesc {
467    /// Start building a description of an intro point
468    pub fn builder() -> IntroPointDescBuilder {
469        IntroPointDescBuilder::default()
470    }
471
472    /// The list of link specifiers needed to extend a circuit to the introduction point.
473    ///
474    /// These can include public keys and network addresses.
475    ///
476    /// Accessor function.
477    //
478    // TODO: It would be better to derive this too, but this accessor needs to
479    // return a slice; Getters can only give us a &Vec<> in this case.
480    pub fn link_specifiers(&self) -> &[EncodedLinkSpec] {
481        &self.link_specifiers
482    }
483}
484
485impl EncryptedHsDesc {
486    /// Attempt to decrypt both layers of encryption in this onion service
487    /// descriptor.
488    ///
489    /// If `hsc_desc_enc` is provided, we use it to decrypt the inner encryption layer;
490    /// otherwise, we require that the inner document is encrypted using the "no
491    /// restricted discovery" method.
492    //
493    // TODO: Someday we _might_ want to allow a list of keypairs in place of
494    // `hs_desc_enc`.  For now, though, we always know a single key that we want
495    // to try using, and we don't want to leak any extra information by
496    // providing other keys that _might_ work.  We certainly don't want to
497    // encourage people to provide every key they know.
498    pub fn decrypt(
499        &self,
500        subcredential: &Subcredential,
501        hsc_desc_enc: Option<&HsClientDescEncKeypair>,
502    ) -> StdResult<TimerangeBound<SignatureGated<HsDesc>>, HsDescError> {
503        use HsDescError as E;
504        let blinded_id = self.outer_doc.blinded_id();
505        let revision_counter = self.outer_doc.revision_counter();
506        let kp_desc_sign = self.outer_doc.desc_sign_key_id();
507
508        // Decrypt the superencryption layer; parse the middle document.
509        let middle = self
510            .outer_doc
511            .decrypt_body(subcredential)
512            .map_err(|_| E::DecryptionFailed)?;
513        let middle = std::str::from_utf8(&middle[..]).map_err(|_| {
514            E::InnerParsing(EK::BadObjectVal.with_msg("Bad utf-8 in middle document"))
515        })?;
516        let middle = middle::HsDescMiddle::parse(middle).map_err(E::InnerParsing)?;
517
518        // Decrypt the encryption layer and parse the inner document.
519        let inner = middle.decrypt_inner(
520            &blinded_id,
521            revision_counter,
522            subcredential,
523            hsc_desc_enc.map(|keys| keys.secret()),
524        )?;
525        let inner = std::str::from_utf8(&inner[..]).map_err(|_| {
526            E::InnerParsing(EK::BadObjectVal.with_msg("Bad utf-8 in inner document"))
527        })?;
528        let (cert_signing_key, time_bound) =
529            inner::HsDescInner::parse(inner).map_err(E::InnerParsing)?;
530
531        if cert_signing_key.as_ref() != Some(kp_desc_sign) {
532            return Err(E::InnerValidation(EK::BadObjectVal.with_msg(
533                "Signing keys in inner document did not match those in outer document",
534            )));
535        }
536
537        // Construct the HsDesc!
538        let time_bound = time_bound.dangerously_map(|sig_bound| {
539            sig_bound.dangerously_map(|inner| HsDesc {
540                idx_info: IndexInfo::from_outer_doc(&self.outer_doc),
541                auth_required: inner.intro_auth_types,
542                is_single_onion_service: inner.single_onion_service,
543                intro_points: inner.intro_points,
544                pow_params: inner.pow_params,
545            })
546        });
547        Ok(time_bound)
548    }
549
550    /// Create a new `IndexInfo` from the outer part of an onion service descriptor.
551    fn from_outer_doc(outer_layer: outer::HsDescOuter) -> Self {
552        EncryptedHsDesc {
553            outer_doc: outer_layer,
554        }
555    }
556}
557
558impl IndexInfo {
559    /// Create a new `IndexInfo` from the outer part of an onion service descriptor.
560    fn from_outer_doc(outer: &outer::HsDescOuter) -> Self {
561        IndexInfo {
562            lifetime: outer.lifetime,
563            signing_cert_expires: outer.desc_signing_key_cert.expiry(),
564            revision: outer.revision_counter(),
565        }
566    }
567}
568
569#[cfg(feature = "hs-dir")]
570impl StoredHsDescMeta {
571    /// Create a new `StoredHsDescMeta` from the outer part of an onion service descriptor.
572    fn from_outer_doc(outer: &outer::HsDescOuter) -> Self {
573        let blinded_id = outer.blinded_id();
574        let idx_info = IndexInfo::from_outer_doc(outer);
575        StoredHsDescMeta {
576            blinded_id,
577            idx_info,
578        }
579    }
580}
581
582/// Test data
583#[cfg(any(test, feature = "testing"))]
584#[allow(missing_docs)]
585#[allow(clippy::missing_docs_in_private_items)]
586#[allow(clippy::unwrap_used)]
587pub mod test_data {
588    use super::*;
589    use hex_literal::hex;
590
591    pub const TEST_DATA: &str = include_str!("../../testdata/hsdesc1.txt");
592
593    pub const TEST_SUBCREDENTIAL: [u8; 32] =
594        hex!("78210A0D2C72BB7A0CAF606BCD938B9A3696894FDDDBC3B87D424753A7E3DF37");
595
596    // This HsDesc uses DescEnc authentication.
597    pub const TEST_DATA_2: &str = include_str!("../../testdata/hsdesc2.txt");
598    pub const TEST_DATA_TIMEPERIOD_2: u64 = 19397;
599    // paozpdhgz2okvc6kgbxvh2bnfsmt4xergrtcl4obkhopyvwxkpjzvoad.onion
600    pub const TEST_HSID_2: [u8; 32] =
601        hex!("781D978CE6CE9CAA8BCA306F53E82D2C993E5C91346625F1C151DCFC56D753D3");
602    pub const TEST_SUBCREDENTIAL_2: [u8; 32] =
603        hex!("24A133E905102BDA9A6AFE57F901366A1B8281865A91F1FE0853E4B50CC8B070");
604    // SACGOAEODFGCYY22NYZV45ZESFPFLDGLMBWFACKEO34XGHASSAMQ (base32)
605    pub const TEST_PUBKEY_2: [u8; 32] =
606        hex!("900467008E194C2C635A6E335E7724915E558CCB606C50094476F9731C129019");
607    // SDZNMD4RP4SCH4EYTTUZPFRZINNFWAOPPKZ6BINZAC7LREV24RBQ (base32)
608    pub const TEST_SECKEY_2: [u8; 32] =
609        hex!("90F2D60F917F2423F0989CE9979639435A5B01CF7AB3E0A1B900BEB892BAE443");
610
611    /// K_hs_blind_id that can be used to parse [`TEST_DATA`]
612    ///
613    /// `pub(crate)` mostly because it's difficult to describe what TP it's for.
614    pub(crate) const TEST_DATA_HS_BLIND_ID: [u8; 32] =
615        hex!("43cc0d62fc6252f578705ca645a46109e265290343b1137e90189744b20b3f2d");
616
617    /// Obtain a testing [`HsDesc`]
618    pub fn test_parsed_hsdesc() -> Result<HsDesc> {
619        let blinded_id = TEST_DATA_HS_BLIND_ID.into();
620
621        let desc = HsDesc::parse(TEST_DATA, &blinded_id)?
622            .check_signature()?
623            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
624            .unwrap()
625            .decrypt(&TEST_SUBCREDENTIAL.into(), None)
626            .unwrap();
627        let desc = desc
628            .check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap())
629            .unwrap();
630        let desc = desc.check_signature().unwrap();
631        Ok(desc)
632    }
633}
634
635#[cfg(test)]
636mod test {
637    // @@ begin test lint list maintained by maint/add_warning @@
638    #![allow(clippy::bool_assert_comparison)]
639    #![allow(clippy::clone_on_copy)]
640    #![allow(clippy::dbg_macro)]
641    #![allow(clippy::mixed_attributes_style)]
642    #![allow(clippy::print_stderr)]
643    #![allow(clippy::print_stdout)]
644    #![allow(clippy::single_char_pattern)]
645    #![allow(clippy::unwrap_used)]
646    #![allow(clippy::unchecked_time_subtraction)]
647    #![allow(clippy::useless_vec)]
648    #![allow(clippy::needless_pass_by_value)]
649    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
650    use std::time::Duration;
651
652    use super::test_data::*;
653    use super::*;
654    use hex_literal::hex;
655    use tor_hscrypto::{pk::HsIdKey, time::TimePeriod};
656    use tor_llcrypto::pk::ed25519;
657
658    #[test]
659    #[cfg(feature = "hs-dir")]
660    fn parse_meta_good() -> Result<()> {
661        let meta = StoredHsDescMeta::parse(TEST_DATA)?
662            .check_signature()?
663            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
664            .unwrap();
665
666        assert_eq!(meta.blinded_id.as_ref(), &TEST_DATA_HS_BLIND_ID);
667        assert_eq!(
668            Duration::try_from(meta.idx_info.lifetime).unwrap(),
669            Duration::from_secs(60 * 180)
670        );
671        assert_eq!(
672            meta.idx_info.signing_cert_expires,
673            humantime::parse_rfc3339("2023-01-26T03:00:00Z").unwrap()
674        );
675        assert_eq!(meta.idx_info.revision, RevisionCounter::from(19655750));
676
677        Ok(())
678    }
679
680    #[test]
681    fn parse_desc_good() -> Result<()> {
682        let wrong_blinded_id = [12; 32].into();
683        let desc = HsDesc::parse(TEST_DATA, &wrong_blinded_id);
684        assert!(desc.is_err());
685        let desc = test_parsed_hsdesc()?;
686
687        assert_eq!(
688            Duration::try_from(desc.idx_info.lifetime).unwrap(),
689            Duration::from_secs(60 * 180)
690        );
691        assert_eq!(
692            desc.idx_info.signing_cert_expires,
693            humantime::parse_rfc3339("2023-01-26T03:00:00Z").unwrap()
694        );
695        assert_eq!(desc.idx_info.revision, RevisionCounter::from(19655750));
696        assert!(desc.auth_required.is_none());
697        assert_eq!(desc.is_single_onion_service, false);
698        assert_eq!(desc.intro_points.len(), 3);
699
700        let ipt0 = &desc.intro_points()[0];
701        assert_eq!(
702            ipt0.ipt_ntor_key().as_bytes(),
703            &hex!("553BF9F9E1979D6F5D5D7D20BB3FE7272E32E22B6E86E35C76A7CA8A377E402F")
704        );
705        // TODO TEST: Perhaps add tests for other intro point fields.
706
707        Ok(())
708    }
709
710    /// Get an EncryptedHsDesc corresponding to `TEST_DATA_2`.
711    fn get_test2_encrypted() -> EncryptedHsDesc {
712        let id: HsIdKey = ed25519::PublicKey::from_bytes(&TEST_HSID_2).unwrap().into();
713        let period = TimePeriod::new(
714            humantime::parse_duration("24 hours").unwrap(),
715            humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap(),
716            humantime::parse_duration("12 hours").unwrap(),
717        )
718        .unwrap();
719        assert_eq!(period.interval_num(), TEST_DATA_TIMEPERIOD_2);
720        let (blind_id, subcredential) = id.compute_blinded_key(period).unwrap();
721
722        assert_eq!(
723            blind_id.as_bytes(),
724            &hex!("706628758208395D461AA0F460A5E76E7B828C66B5E794768592B451302E961D")
725        );
726
727        assert_eq!(subcredential.as_ref(), &TEST_SUBCREDENTIAL_2);
728
729        HsDesc::parse(TEST_DATA_2, &blind_id.into())
730            .unwrap()
731            .check_signature()
732            .unwrap()
733            .check_valid_at(&humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap())
734            .unwrap()
735    }
736
737    #[test]
738    fn parse_desc_auth_missing() {
739        // If we try to decrypt TEST_DATA_2 with no ClientDescEncKey, we get a
740        // failure.
741        let encrypted = get_test2_encrypted();
742        let subcredential = TEST_SUBCREDENTIAL_2.into();
743        let with_no_auth = encrypted.decrypt(&subcredential, None);
744        assert!(with_no_auth.is_err());
745    }
746
747    #[test]
748    fn parse_desc_auth_good() {
749        // But if we try to decrypt TEST_DATA_2 with the correct ClientDescEncKey, we get a
750        // the data inside!
751
752        let encrypted = get_test2_encrypted();
753        let subcredential = TEST_SUBCREDENTIAL_2.into();
754        let pk = curve25519::PublicKey::from(TEST_PUBKEY_2).into();
755        let sk = curve25519::StaticSecret::from(TEST_SECKEY_2).into();
756        let desc = encrypted
757            .decrypt(&subcredential, Some(&HsClientDescEncKeypair::new(pk, sk)))
758            .unwrap();
759        let desc = desc
760            .check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap())
761            .unwrap();
762        let desc = desc.check_signature().unwrap();
763        assert_eq!(desc.intro_points.len(), 3);
764    }
765}