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
41#[cfg(feature = "hsdesc-inner-docs")]
42#[cfg_attr(docsrs, doc(cfg(feature = "hsdesc-inner-docs")))]
43pub use {inner::HsDescInner, middle::HsDescMiddle, outer::HsDescOuter};
44
45#[cfg(feature = "hs-service")]
46#[cfg_attr(docsrs, doc(cfg(feature = "hs-service")))]
47pub use build::{create_desc_sign_key_cert, HsDescBuilder};
48
49/// Metadata about an onion service descriptor, as stored at an HsDir.
50///
51/// This object is parsed from the outermost document of an onion service
52/// descriptor, and used on the HsDir to maintain its index.  It does not
53/// include the inner documents' information about introduction points, since the
54/// HsDir cannot decrypt those without knowing the onion service's un-blinded
55/// identity.
56///
57/// The HsDir caches this value, along with the original text of the descriptor.
58#[cfg(feature = "hs-dir")]
59#[allow(dead_code)] // TODO RELAY: Remove this.
60pub struct StoredHsDescMeta {
61    /// The blinded onion identity for this descriptor.  (This is the only
62    /// identity that the HsDir knows.)
63    blinded_id: HsBlindId,
64
65    /// Information about the expiration and revision counter for this
66    /// descriptor.
67    idx_info: IndexInfo,
68}
69
70/// An unchecked StoredHsDescMeta: parsed, but not checked for liveness or validity.
71#[cfg(feature = "hs-dir")]
72pub type UncheckedStoredHsDescMeta =
73    signed::SignatureGated<timed::TimerangeBound<StoredHsDescMeta>>;
74
75/// Information about how long to hold a given onion service descriptor, and
76/// when to replace it.
77#[derive(Debug, Clone)]
78#[allow(dead_code)] // TODO RELAY: Remove this if there turns out to be no need for it.
79struct IndexInfo {
80    /// The lifetime in minutes that this descriptor should be held after it is
81    /// received.
82    lifetime: IntegerMinutes<u16>,
83    /// The expiration time on the `descriptor-signing-key-cert` included in this
84    /// descriptor.
85    signing_cert_expires: SystemTime,
86    /// The revision counter on this descriptor: higher values should replace
87    /// older ones.
88    revision: RevisionCounter,
89}
90
91/// A decrypted, decoded onion service descriptor.
92///
93/// This object includes information from both the outer (plaintext) document of
94/// the descriptor, and the inner (encrypted) documents.  It tells the client the
95/// information it needs to contact the onion service, including necessary
96/// introduction points and public keys.
97#[derive(Debug, Clone)]
98pub struct HsDesc {
99    /// Information about the expiration and revision counter for this
100    /// descriptor.
101    #[allow(dead_code)] // TODO RELAY: Remove this if there turns out to be no need for it.
102    idx_info: IndexInfo,
103
104    /// The list of authentication types that this onion service supports.
105    auth_required: Option<SmallVec<[IntroAuthType; 2]>>,
106
107    /// If true, this a "single onion service" and is not trying to keep its own location private.
108    is_single_onion_service: bool,
109
110    /// One or more introduction points used to contact the onion service.
111    intro_points: Vec<IntroPointDesc>,
112
113    /// A list of offered proof-of-work parameters, at most one per type.
114    pow_params: pow::PowParamSet,
115    // /// A list of recognized CREATE handshakes that this onion service supports.
116    //
117    // TODO:  When someday we add a "create2 format" other than "hs-ntor", we
118    // should turn this into a caret enum, record this info, and expose it.
119    // create2_formats: Vec<u32>,
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    ///     .check_valid_at(&timestamp)?;
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    ///     .check_valid_at(&timestamp)?
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    ///   * checks if both layers are valid at the `valid_at` timestamp
264    ///   * validates the signatures on both layers
265    ///
266    /// Returns an error if the descriptor cannot be parsed, or if one of the validation steps
267    /// fails.
268    pub fn parse_decrypt_validate(
269        input: &str,
270        blinded_onion_id: &HsBlindId,
271        valid_at: SystemTime,
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        let (inner_desc, new_bounds) = {
282            // We use is_valid_at and dangerously_into_parts instead of check_valid_at because we
283            // need the time bounds of the outer layer (for computing the intersection with the
284            // time bounds of the inner layer).
285            unchecked_desc
286                .is_valid_at(&valid_at)
287                .map_err(|e| E::OuterValidation(e.into()))?;
288            // It's safe to use dangerously_peek() as we've just checked if unchecked_desc is
289            // valid at the current time
290            let inner_timerangebound = unchecked_desc
291                .dangerously_peek()
292                .decrypt(subcredential, hsc_desc_enc)?;
293
294            let new_bounds = unchecked_desc
295                .intersect(&inner_timerangebound)
296                .map(|(b1, b2)| (b1.cloned(), b2.cloned()));
297
298            (inner_timerangebound, new_bounds)
299        };
300
301        let hsdesc = inner_desc
302            .check_valid_at(&valid_at)
303            .map_err(|e| E::InnerValidation(e.into()))?
304            .check_signature()
305            .map_err(|e| E::InnerValidation(e.into()))?;
306
307        // If we've reached this point, it means the descriptor is valid at specified time. This
308        // means the time bounds of the two layers definitely intersect, so new_bounds **must** be
309        // Some. It is a bug if new_bounds is None.
310        let new_bounds = new_bounds
311            .ok_or_else(|| internal!("failed to compute TimerangeBounds for a valid descriptor"))?;
312
313        Ok(TimerangeBound::new(hsdesc, new_bounds))
314    }
315
316    /// One or more introduction points used to contact the onion service.
317    ///
318    /// Always returns at least one introduction point,
319    /// and never more than [`NUM_INTRO_POINT_MAX`](tor_hscrypto::NUM_INTRO_POINT_MAX).
320    /// (Descriptors which have fewer or more are dealt with during parsing.)
321    ///
322    /// Accessor function.
323    //
324    // TODO: We'd like to derive this, but amplify::Getters  would give us &Vec<>,
325    // not &[].
326    //
327    // Perhaps someday we can use derive_deftly, or add as_ref() support?
328    pub fn intro_points(&self) -> &[IntroPointDesc] {
329        &self.intro_points
330    }
331
332    /// Return true if this onion service claims to be a non-anonymous "single
333    /// onion service".
334    ///
335    /// (We should always anonymize our own connection to an onion service.)
336    pub fn is_single_onion_service(&self) -> bool {
337        self.is_single_onion_service
338    }
339
340    /// Return true if this onion service claims that it needs user authentication
341    /// of some kind in its INTRODUCE messages.
342    ///
343    /// (Arti does not currently support sending this kind of authentication.)
344    pub fn requires_intro_authentication(&self) -> bool {
345        self.auth_required.is_some()
346    }
347
348    /// Get a list of offered proof-of-work parameters, at most one per type.
349    pub fn pow_params(&self) -> &[pow::PowParams] {
350        self.pow_params.slice()
351    }
352}
353
354/// An error returned by [`HsDesc::parse_decrypt_validate`], indicating what
355/// kind of failure prevented us from validating an onion service descriptor.
356///
357/// This is distinct from [`tor_netdoc::Error`](crate::Error) so that we can
358/// tell errors that could be the HsDir's fault from those that are definitely
359/// protocol violations by the onion service.
360#[derive(Clone, Debug, thiserror::Error)]
361#[non_exhaustive]
362pub enum HsDescError {
363    /// An outer object failed parsing: the HsDir should probably have
364    /// caught this, and not given us this HsDesc.
365    ///
366    /// (This can be an innocent error if we happen to know about restrictions
367    /// that the HsDir does not).
368    #[error("Parsing failure on outer layer of an onion service descriptor.")]
369    OuterParsing(#[source] crate::Error),
370
371    /// An outer object failed validation: the HsDir should probably have
372    /// caught this, and not given us this HsDesc.
373    ///
374    /// (This can happen erroneously if we think that something is untimely but
375    /// the HSDir's clock is slightly different, or _was_ different when it
376    /// decided to give us this object.)
377    #[error("Validation failure on outer layer of an onion service descriptor.")]
378    OuterValidation(#[source] crate::Error),
379
380    /// Decrypting the inner layer failed because we need to have a decryption key,
381    /// but we didn't provide one.
382    ///
383    /// This is probably our fault.
384    #[error("Decryption failure on onion service descriptor: missing decryption key")]
385    MissingDecryptionKey,
386
387    /// Decrypting the inner layer failed because, although we provided a key,
388    /// we did not provide the key we need to decrypt it.
389    ///
390    /// This is probably our fault.
391    #[error("Decryption failure on onion service descriptor: incorrect decryption key")]
392    WrongDecryptionKey,
393
394    /// Decrypting the inner or middle layer failed because of an issue with the
395    /// decryption itself.
396    ///
397    /// This is the onion service's fault.
398    #[error("Decryption failure on onion service descriptor: could not decrypt")]
399    DecryptionFailed,
400
401    /// We failed to parse something cryptographic in an inner layer of the
402    /// onion service descriptor.
403    ///
404    /// This is definitely the onion service's fault.
405    #[error("Parsing failure on inner layer of an onion service descriptor")]
406    InnerParsing(#[source] crate::Error),
407
408    /// We failed to validate something cryptographic in an inner layer of the
409    /// onion service descriptor.
410    ///
411    /// This is definitely the onion service's fault.
412    #[error("Validation failure on inner layer of an onion service descriptor")]
413    InnerValidation(#[source] crate::Error),
414
415    /// We encountered an internal error.
416    #[error("Internal error: {0}")]
417    Bug(#[from] tor_error::Bug),
418}
419
420impl tor_error::HasKind for HsDescError {
421    fn kind(&self) -> tor_error::ErrorKind {
422        use tor_error::ErrorKind as EK;
423        use HsDescError as E;
424        match self {
425            E::OuterParsing(_) | E::OuterValidation(_) => EK::TorProtocolViolation,
426            E::MissingDecryptionKey => EK::OnionServiceMissingClientAuth,
427            E::WrongDecryptionKey => EK::OnionServiceWrongClientAuth,
428            E::DecryptionFailed | E::InnerParsing(_) | E::InnerValidation(_) => {
429                EK::OnionServiceProtocolViolation
430            }
431            E::Bug(e) => e.kind(),
432        }
433    }
434}
435
436impl IntroPointDesc {
437    /// Start building a description of an intro point
438    pub fn builder() -> IntroPointDescBuilder {
439        IntroPointDescBuilder::default()
440    }
441
442    /// The list of link specifiers needed to extend a circuit to the introduction point.
443    ///
444    /// These can include public keys and network addresses.
445    ///
446    /// Accessor function.
447    //
448    // TODO: It would be better to derive this too, but this accessor needs to
449    // return a slice; Getters can only give us a &Vec<> in this case.
450    pub fn link_specifiers(&self) -> &[EncodedLinkSpec] {
451        &self.link_specifiers
452    }
453}
454
455impl EncryptedHsDesc {
456    /// Attempt to decrypt both layers of encryption in this onion service
457    /// descriptor.
458    ///
459    /// If `hsc_desc_enc` is provided, we use it to decrypt the inner encryption layer;
460    /// otherwise, we require that the inner document is encrypted using the "no
461    /// restricted discovery" method.
462    //
463    // TODO: Someday we _might_ want to allow a list of keypairs in place of
464    // `hs_desc_enc`.  For now, though, we always know a single key that we want
465    // to try using, and we don't want to leak any extra information by
466    // providing other keys that _might_ work.  We certainly don't want to
467    // encourage people to provide every key they know.
468    pub fn decrypt(
469        &self,
470        subcredential: &Subcredential,
471        hsc_desc_enc: Option<&HsClientDescEncKeypair>,
472    ) -> StdResult<TimerangeBound<SignatureGated<HsDesc>>, HsDescError> {
473        use HsDescError as E;
474        let blinded_id = self.outer_doc.blinded_id();
475        let revision_counter = self.outer_doc.revision_counter();
476        let kp_desc_sign = self.outer_doc.desc_sign_key_id();
477
478        // Decrypt the superencryption layer; parse the middle document.
479        let middle = self
480            .outer_doc
481            .decrypt_body(subcredential)
482            .map_err(|_| E::DecryptionFailed)?;
483        let middle = std::str::from_utf8(&middle[..]).map_err(|_| {
484            E::InnerParsing(EK::BadObjectVal.with_msg("Bad utf-8 in middle document"))
485        })?;
486        let middle = middle::HsDescMiddle::parse(middle).map_err(E::InnerParsing)?;
487
488        // Decrypt the encryption layer and parse the inner document.
489        let inner = middle.decrypt_inner(
490            &blinded_id,
491            revision_counter,
492            subcredential,
493            hsc_desc_enc.map(|keys| keys.secret()),
494        )?;
495        let inner = std::str::from_utf8(&inner[..]).map_err(|_| {
496            E::InnerParsing(EK::BadObjectVal.with_msg("Bad utf-8 in inner document"))
497        })?;
498        let (cert_signing_key, time_bound) =
499            inner::HsDescInner::parse(inner).map_err(E::InnerParsing)?;
500
501        if cert_signing_key.as_ref() != Some(kp_desc_sign) {
502            return Err(E::InnerValidation(EK::BadObjectVal.with_msg(
503                "Signing keys in inner document did not match those in outer document",
504            )));
505        }
506
507        // Construct the HsDesc!
508        let time_bound = time_bound.dangerously_map(|sig_bound| {
509            sig_bound.dangerously_map(|inner| HsDesc {
510                idx_info: IndexInfo::from_outer_doc(&self.outer_doc),
511                auth_required: inner.intro_auth_types,
512                is_single_onion_service: inner.single_onion_service,
513                intro_points: inner.intro_points,
514                pow_params: inner.pow_params,
515            })
516        });
517        Ok(time_bound)
518    }
519
520    /// Create a new `IndexInfo` from the outer part of an onion service descriptor.
521    fn from_outer_doc(outer_layer: outer::HsDescOuter) -> Self {
522        EncryptedHsDesc {
523            outer_doc: outer_layer,
524        }
525    }
526}
527
528impl IndexInfo {
529    /// Create a new `IndexInfo` from the outer part of an onion service descriptor.
530    fn from_outer_doc(outer: &outer::HsDescOuter) -> Self {
531        IndexInfo {
532            lifetime: outer.lifetime,
533            signing_cert_expires: outer.desc_signing_key_cert.expiry(),
534            revision: outer.revision_counter(),
535        }
536    }
537}
538
539#[cfg(feature = "hs-dir")]
540impl StoredHsDescMeta {
541    /// Create a new `StoredHsDescMeta` from the outer part of an onion service descriptor.
542    fn from_outer_doc(outer: &outer::HsDescOuter) -> Self {
543        let blinded_id = outer.blinded_id();
544        let idx_info = IndexInfo::from_outer_doc(outer);
545        StoredHsDescMeta {
546            blinded_id,
547            idx_info,
548        }
549    }
550}
551
552/// Test data
553#[cfg(any(test, feature = "testing"))]
554#[allow(missing_docs)]
555#[allow(clippy::missing_docs_in_private_items)]
556#[allow(clippy::unwrap_used)]
557pub mod test_data {
558    use super::*;
559    use hex_literal::hex;
560
561    pub const TEST_DATA: &str = include_str!("../../testdata/hsdesc1.txt");
562
563    pub const TEST_SUBCREDENTIAL: [u8; 32] =
564        hex!("78210A0D2C72BB7A0CAF606BCD938B9A3696894FDDDBC3B87D424753A7E3DF37");
565
566    // This HsDesc uses DescEnc authentication.
567    pub const TEST_DATA_2: &str = include_str!("../../testdata/hsdesc2.txt");
568    pub const TEST_DATA_TIMEPERIOD_2: u64 = 19397;
569    // paozpdhgz2okvc6kgbxvh2bnfsmt4xergrtcl4obkhopyvwxkpjzvoad.onion
570    pub const TEST_HSID_2: [u8; 32] =
571        hex!("781D978CE6CE9CAA8BCA306F53E82D2C993E5C91346625F1C151DCFC56D753D3");
572    pub const TEST_SUBCREDENTIAL_2: [u8; 32] =
573        hex!("24A133E905102BDA9A6AFE57F901366A1B8281865A91F1FE0853E4B50CC8B070");
574    // SACGOAEODFGCYY22NYZV45ZESFPFLDGLMBWFACKEO34XGHASSAMQ (base32)
575    pub const TEST_PUBKEY_2: [u8; 32] =
576        hex!("900467008E194C2C635A6E335E7724915E558CCB606C50094476F9731C129019");
577    // SDZNMD4RP4SCH4EYTTUZPFRZINNFWAOPPKZ6BINZAC7LREV24RBQ (base32)
578    pub const TEST_SECKEY_2: [u8; 32] =
579        hex!("90F2D60F917F2423F0989CE9979639435A5B01CF7AB3E0A1B900BEB892BAE443");
580
581    /// K_hs_blind_id that can be used to parse [`TEST_DATA`]
582    ///
583    /// `pub(crate)` mostly because it's difficult to describe what TP it's for.
584    pub(crate) const TEST_DATA_HS_BLIND_ID: [u8; 32] =
585        hex!("43cc0d62fc6252f578705ca645a46109e265290343b1137e90189744b20b3f2d");
586
587    /// Obtain a testing [`HsDesc`]
588    pub fn test_parsed_hsdesc() -> Result<HsDesc> {
589        let blinded_id = TEST_DATA_HS_BLIND_ID.into();
590
591        let desc = HsDesc::parse(TEST_DATA, &blinded_id)?
592            .check_signature()?
593            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
594            .unwrap()
595            .decrypt(&TEST_SUBCREDENTIAL.into(), None)
596            .unwrap();
597        let desc = desc
598            .check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap())
599            .unwrap();
600        let desc = desc.check_signature().unwrap();
601        Ok(desc)
602    }
603}
604
605#[cfg(test)]
606mod test {
607    // @@ begin test lint list maintained by maint/add_warning @@
608    #![allow(clippy::bool_assert_comparison)]
609    #![allow(clippy::clone_on_copy)]
610    #![allow(clippy::dbg_macro)]
611    #![allow(clippy::mixed_attributes_style)]
612    #![allow(clippy::print_stderr)]
613    #![allow(clippy::print_stdout)]
614    #![allow(clippy::single_char_pattern)]
615    #![allow(clippy::unwrap_used)]
616    #![allow(clippy::unchecked_duration_subtraction)]
617    #![allow(clippy::useless_vec)]
618    #![allow(clippy::needless_pass_by_value)]
619    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
620    use std::time::Duration;
621
622    use super::test_data::*;
623    use super::*;
624    use hex_literal::hex;
625    use tor_hscrypto::{pk::HsIdKey, time::TimePeriod};
626    use tor_llcrypto::pk::ed25519;
627
628    #[test]
629    #[cfg(feature = "hs-dir")]
630    fn parse_meta_good() -> Result<()> {
631        let meta = StoredHsDescMeta::parse(TEST_DATA)?
632            .check_signature()?
633            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
634            .unwrap();
635
636        assert_eq!(meta.blinded_id.as_ref(), &TEST_DATA_HS_BLIND_ID);
637        assert_eq!(
638            Duration::try_from(meta.idx_info.lifetime).unwrap(),
639            Duration::from_secs(60 * 180)
640        );
641        assert_eq!(
642            meta.idx_info.signing_cert_expires,
643            humantime::parse_rfc3339("2023-01-26T03:00:00Z").unwrap()
644        );
645        assert_eq!(meta.idx_info.revision, RevisionCounter::from(19655750));
646
647        Ok(())
648    }
649
650    #[test]
651    fn parse_desc_good() -> Result<()> {
652        let wrong_blinded_id = [12; 32].into();
653        let desc = HsDesc::parse(TEST_DATA, &wrong_blinded_id);
654        assert!(desc.is_err());
655        let desc = test_parsed_hsdesc()?;
656
657        assert_eq!(
658            Duration::try_from(desc.idx_info.lifetime).unwrap(),
659            Duration::from_secs(60 * 180)
660        );
661        assert_eq!(
662            desc.idx_info.signing_cert_expires,
663            humantime::parse_rfc3339("2023-01-26T03:00:00Z").unwrap()
664        );
665        assert_eq!(desc.idx_info.revision, RevisionCounter::from(19655750));
666        assert!(desc.auth_required.is_none());
667        assert_eq!(desc.is_single_onion_service, false);
668        assert_eq!(desc.intro_points.len(), 3);
669
670        let ipt0 = &desc.intro_points()[0];
671        assert_eq!(
672            ipt0.ipt_ntor_key().as_bytes(),
673            &hex!("553BF9F9E1979D6F5D5D7D20BB3FE7272E32E22B6E86E35C76A7CA8A377E402F")
674        );
675        // TODO TEST: Perhaps add tests for other intro point fields.
676
677        Ok(())
678    }
679
680    /// Get an EncryptedHsDesc corresponding to `TEST_DATA_2`.
681    fn get_test2_encrypted() -> EncryptedHsDesc {
682        let id: HsIdKey = ed25519::PublicKey::from_bytes(&TEST_HSID_2).unwrap().into();
683        let period = TimePeriod::new(
684            humantime::parse_duration("24 hours").unwrap(),
685            humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap(),
686            humantime::parse_duration("12 hours").unwrap(),
687        )
688        .unwrap();
689        assert_eq!(period.interval_num(), TEST_DATA_TIMEPERIOD_2);
690        let (blind_id, subcredential) = id.compute_blinded_key(period).unwrap();
691
692        assert_eq!(
693            blind_id.as_bytes(),
694            &hex!("706628758208395D461AA0F460A5E76E7B828C66B5E794768592B451302E961D")
695        );
696
697        assert_eq!(subcredential.as_ref(), &TEST_SUBCREDENTIAL_2);
698
699        HsDesc::parse(TEST_DATA_2, &blind_id.into())
700            .unwrap()
701            .check_signature()
702            .unwrap()
703            .check_valid_at(&humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap())
704            .unwrap()
705    }
706
707    #[test]
708    fn parse_desc_auth_missing() {
709        // If we try to decrypt TEST_DATA_2 with no ClientDescEncKey, we get a
710        // failure.
711        let encrypted = get_test2_encrypted();
712        let subcredential = TEST_SUBCREDENTIAL_2.into();
713        let with_no_auth = encrypted.decrypt(&subcredential, None);
714        assert!(with_no_auth.is_err());
715    }
716
717    #[test]
718    fn parse_desc_auth_good() {
719        // But if we try to decrypt TEST_DATA_2 with the correct ClientDescEncKey, we get a
720        // the data inside!
721
722        let encrypted = get_test2_encrypted();
723        let subcredential = TEST_SUBCREDENTIAL_2.into();
724        let pk = curve25519::PublicKey::from(TEST_PUBKEY_2).into();
725        let sk = curve25519::StaticSecret::from(TEST_SECKEY_2).into();
726        let desc = encrypted
727            .decrypt(&subcredential, Some(&HsClientDescEncKeypair::new(pk, sk)))
728            .unwrap();
729        let desc = desc
730            .check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap())
731            .unwrap();
732        let desc = desc.check_signature().unwrap();
733        assert_eq!(desc.intro_points.len(), 3);
734    }
735}