1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
//! Implementation for onion service descriptors.
//!
//! An onion service descriptor is a document generated by an onion service and
//! uploaded to one or more HsDir nodes for clients to later download.  It tells
//! the onion service client where to find the current introduction points for
//! the onion service, and how to connect to them.
//!
//! An onion service descriptor is more complicated than most other
//! documentation types, because it is partially encrypted.

#![allow(dead_code, unused_variables, clippy::missing_panics_doc)] // TODO hs: remove.
mod desc_enc;

#[cfg(feature = "hs-service")]
mod build;
mod inner;
mod middle;
mod outer;

pub use desc_enc::DecryptionError;

use crate::{ParseErrorKind as EK, Result};

use tor_checkable::signed::{self, SignatureGated};
use tor_checkable::timed::{self, TimerangeBound};
use tor_hscrypto::pk::{
    HsBlindId, HsClientDescEncKey, HsClientDescEncSecretKey, HsIntroPtSessionIdKey, HsSvcNtorKey,
};
use tor_hscrypto::{RevisionCounter, Subcredential};
use tor_linkspec::UnparsedLinkSpec;
use tor_llcrypto::pk::curve25519;
use tor_units::IntegerMinutes;

use smallvec::SmallVec;

use std::time::SystemTime;

#[cfg(feature = "hsdesc-inner-docs")]
#[cfg_attr(docsrs, doc(cfg(feature = "hsdesc-inner-docs")))]
pub use {inner::HsDescInner, middle::HsDescMiddle, outer::HsDescOuter};

#[cfg(feature = "hs-service")]
#[cfg_attr(docsrs, doc(cfg(feature = "hs-service")))]
pub use build::HsDescBuilder;

/// Metadata about an onion service descriptor, as stored at an HsDir.
///
/// This object is parsed from the outermost document of an onion service
/// descriptor, and used on the HsDir to maintain its index.  It does not
/// include the inner documents' information about introduction points, since the
/// HsDir cannot decrypt those without knowing the onion service's un-blinded
/// identity.
///
/// The HsDir caches this value, along with the original text of the descriptor.
pub struct StoredHsDescMeta {
    /// The blinded onion identity for this descriptor.  (This is the only
    /// identity that the HsDir knows.)
    blinded_id: HsBlindId,

    /// Information about the expiration and revision counter for this
    /// descriptor.
    idx_info: IndexInfo,
}

/// An unchecked StoredHsDescMeta: parsed, but not checked for liveness or validity.
pub type UncheckedStoredHsDescMeta =
    signed::SignatureGated<timed::TimerangeBound<StoredHsDescMeta>>;

/// Information about how long to hold a given onion service descriptor, and
/// when to replace it.
#[derive(Debug, Clone)]
struct IndexInfo {
    /// The lifetime in minutes that this descriptor should be held after it is
    /// received.
    lifetime: IntegerMinutes<u16>,
    /// The expiration time on the `descriptor-signing-key-cert` included in this
    /// descriptor.
    signing_cert_expires: SystemTime,
    /// The revision counter on this descriptor: higher values should replace
    /// older ones.
    revision: RevisionCounter,
}

/// A decrypted, decoded onion service descriptor.
///
/// This object includes information from both the outer (plaintext) document of
/// the descriptor, and the inner (encrypted) documents.  It tells the client the
/// information it needs to contact the onion service, including necessary
/// introduction points and public keys.
#[derive(Debug, Clone)]
pub struct HsDesc {
    /// Information about the expiration and revision counter for this
    /// descriptor.
    idx_info: IndexInfo,

    /// `KP_hsc_desc_enc`, the public key corresponding to the private key that
    /// we used to decrypt this descriptor.
    ///
    /// This is set to None if we did not have to use a private key to decrypt
    /// the descriptor.
    decrypted_with_id: Option<HsClientDescEncKey>,

    /// A list of recognized CREATE handshakes that this onion service supports.
    // TODO hs: this should probably be a caret enum, not an integer
    // TODO hs: Add this if we actually need it.
    // create2_formats: Vec<u32>,

    /// A list of authentication types that this onion service supports.
    auth_required: Option<SmallVec<[IntroAuthType; 2]>>,

    /// If true, this a "single onion service" and is not trying to keep its own location private.
    is_single_onion_service: bool,

    /// One or more introduction points used to contact the onion service.
    intro_points: Vec<IntroPointDesc>,
}

/// A type of authentication that is required when introducing to an onion
/// service.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Eq, PartialEq, derive_more::Display)]
pub enum IntroAuthType {
    /// Ed25519 authentication is required.
    #[display(fmt = "ed25519")]
    Ed25519,
}

/// Information in an onion service descriptor about a single
/// introduction point.
#[derive(Debug, Clone)]
pub struct IntroPointDesc {
    /// A list of link specifiers needed to extend a circuit to the introduction point.
    ///
    /// These can include public keys and network addresses.
    //
    // TODO hs: perhaps we should make certain link specifiers mandatory? That
    // would make it possible for IntroPointDesc to implement CircTarget.
    link_specifiers: Vec<UnparsedLinkSpec>,

    /// The key used to extend a circuit _to the introduction point_, using the
    /// ntor or ntor3 handshakes.  (`KP_ntor`)
    ipt_ntor_key: curve25519::PublicKey,

    /// A key used to identify the onion service at this introduction point.
    /// (`KP_hs_ipt_sid`)
    ipt_sid_key: HsIntroPtSessionIdKey,

    /// `KP_hss_ntor`, the key used to encrypt a handshake _to the onion
    /// service_ when using this introduction point.
    ///
    /// The onion service uses a separate key of this type with each
    /// introduction point as part of its strategy for preventing replay
    /// attacks.
    svc_ntor_key: HsSvcNtorKey,
}

/// An onion service after it has been parsed by the client, but not yet decrypted.
pub struct EncryptedHsDesc {
    /// The un-decoded outer document of our onion service descriptor.
    outer_doc: outer::HsDescOuter,
}

/// An unchecked HsDesc: parsed, but not checked for liveness or validity.
pub type UncheckedEncryptedHsDesc = signed::SignatureGated<timed::TimerangeBound<EncryptedHsDesc>>;

impl StoredHsDescMeta {
    // TODO hs: needs accessor functions too.  (Let's not use public fields; we
    // are likely to want to mess with the repr of these types.)

    /// Parse the outermost layer of the descriptor in `input`, and return the
    /// resulting metadata (if possible).
    pub fn parse(input: &str) -> Result<UncheckedStoredHsDescMeta> {
        let outer = outer::HsDescOuter::parse(input)?;
        Ok(outer.dangerously_map(|timebound| {
            timebound.dangerously_map(|outer| StoredHsDescMeta::from_outer_doc(&outer))
        }))
    }
}

impl HsDesc {
    // TODO hs: needs accessor functions too.  (Let's not use public fields; we
    // are likely to want to mess with the repr of these types.)

    /// Parse the outermost document of the descriptor in `input`, and validate
    /// that its identity is consistent with `blinded_onion_id`.
    ///
    /// On success, the caller will get a wrapped object which they must
    /// validate and then decrypt.
    pub fn parse(
        input: &str,
        // We don't actually need this to parse the HsDesc, but we _do_ need it to prevent
        // a nasty pattern where we forget to check that we got the right one.
        blinded_onion_id: &HsBlindId,
    ) -> Result<UncheckedEncryptedHsDesc> {
        let outer = outer::HsDescOuter::parse(input)?;
        let mut id_matches = false;
        let result = outer.dangerously_map(|timebound| {
            timebound.dangerously_map(|outer| {
                id_matches = blinded_onion_id == &outer.blinded_id();
                EncryptedHsDesc::from_outer_doc(outer)
            })
        });
        if !id_matches {
            // TODO hs: This errorkind is not quite right.
            return Err(
                EK::BadObjectVal.with_msg("onion service descriptor did not have the expected ID")
            );
        }

        Ok(result)
    }
}

impl EncryptedHsDesc {
    /// Attempt to decrypt both layers of encryption in this onion service
    /// descriptor.
    ///
    /// If `hsc_desc_enc` is provided, we use it to decrypt the inner encryption layer;
    /// otherwise, we require that the inner document is encrypted using the "no
    /// client authorization" method.
    ///
    /// Note that `hsc_desc_enc` must be a key *pair* - ie, a KP_hsc_desc_enc
    /// and corresponding KS_hsc_desc_enc. This function **does not check**
    /// this.
    //
    // TODO hs: I'm not sure that taking `hsc_desc_enc` as an argument is correct. Instead, maybe
    // we should take a set of keys?
    pub fn decrypt(
        self,
        subcredential: &Subcredential,
        hsc_desc_enc: Option<(&HsClientDescEncKey, &HsClientDescEncSecretKey)>,
    ) -> Result<TimerangeBound<SignatureGated<HsDesc>>> {
        let blinded_id = self.outer_doc.blinded_id();
        let revision_counter = self.outer_doc.revision_counter;
        let kp_desc_sign = self.outer_doc.desc_sign_key_id();

        // Decrypt the superencryption layer; parse the middle document.
        let middle = self.outer_doc.decrypt_body(subcredential).map_err(|e| {
            EK::BadObjectVal.with_msg("onion service descriptor superencryption failed.")
        })?;
        let middle = std::str::from_utf8(&middle[..])
            .map_err(|e| EK::BadObjectVal.with_msg("Bad utf-8 in middle document"))?;
        let middle = middle::HsDescMiddle::parse(middle)?;

        // Decrypt the encryption layer and parse the inner document.
        let inner = middle
            .decrypt_inner(
                &blinded_id,
                revision_counter,
                subcredential,
                hsc_desc_enc.map(|keys| keys.1),
            )
            .map_err(|e| {
                EK::BadObjectVal.with_msg("onion service descriptor encryption failed.")
            })?;
        let inner = std::str::from_utf8(&inner[..])
            .map_err(|e| EK::BadObjectVal.with_msg("Bad utf-8 in inner document"))?;
        let (cert_signing_key, time_bound) = inner::HsDescInner::parse(inner)?;

        if cert_signing_key.as_ref() != Some(kp_desc_sign) {
            return Err(EK::BadObjectVal
                .with_msg("Signing keys in inner document did not match those in outer document"));
        }

        // Construct the HsDesc!
        let time_bound = time_bound.dangerously_map(|sig_bound| {
            sig_bound.dangerously_map(|inner| HsDesc {
                idx_info: IndexInfo::from_outer_doc(&self.outer_doc),
                decrypted_with_id: hsc_desc_enc.map(|keys| keys.0.clone()),
                auth_required: inner.intro_auth_types,
                is_single_onion_service: inner.single_onion_service,
                intro_points: inner.intro_points,
            })
        });
        Ok(time_bound)
    }

    /// Create a new `IndexInfo` from the outer part of an onion service descriptor.
    fn from_outer_doc(outer_layer: outer::HsDescOuter) -> Self {
        EncryptedHsDesc {
            outer_doc: outer_layer,
        }
    }
}

impl IndexInfo {
    /// Create a new `IndexInfo` from the outer part of an onion service descriptor.
    fn from_outer_doc(outer: &outer::HsDescOuter) -> Self {
        IndexInfo {
            lifetime: outer.lifetime,
            signing_cert_expires: outer.desc_signing_key_cert.expiry(),
            revision: outer.revision_counter,
        }
    }
}

impl StoredHsDescMeta {
    /// Create a new `StoredHsDescMeta` from the outer part of an onion service descriptor.
    fn from_outer_doc(outer: &outer::HsDescOuter) -> Self {
        let blinded_id = outer.blinded_id();
        let idx_info = IndexInfo::from_outer_doc(outer);
        StoredHsDescMeta {
            blinded_id,
            idx_info,
        }
    }
}

/// Test data
#[cfg(any(test, feature = "testing"))]
#[allow(missing_docs)]
#[allow(clippy::missing_docs_in_private_items)]
pub mod test_data {
    use hex_literal::hex;

    pub const TEST_DATA: &str = include_str!("../../testdata/hsdesc1.txt");

    pub const TEST_SUBCREDENTIAL: [u8; 32] =
        hex!("78210A0D2C72BB7A0CAF606BCD938B9A3696894FDDDBC3B87D424753A7E3DF37");

    // This HsDesc uses DescEnc authentication.
    pub const TEST_DATA_2: &str = include_str!("../../testdata/hsdesc2.txt");
    pub const TEST_DATA_TIMEPERIOD_2: u64 = 19397;
    // paozpdhgz2okvc6kgbxvh2bnfsmt4xergrtcl4obkhopyvwxkpjzvoad.onion
    pub const TEST_HSID_2: [u8; 32] =
        hex!("781D978CE6CE9CAA8BCA306F53E82D2C993E5C91346625F1C151DCFC56D753D3");
    pub const TEST_SUBCREDENTIAL_2: [u8; 32] =
        hex!("24A133E905102BDA9A6AFE57F901366A1B8281865A91F1FE0853E4B50CC8B070");
    // SACGOAEODFGCYY22NYZV45ZESFPFLDGLMBWFACKEO34XGHASSAMQ (base32)
    pub const TEST_PUBKEY_2: [u8; 32] =
        hex!("900467008E194C2C635A6E335E7724915E558CCB606C50094476F9731C129019");
    // SDZNMD4RP4SCH4EYTTUZPFRZINNFWAOPPKZ6BINZAC7LREV24RBQ (base32)
    pub const TEST_SECKEY_2: [u8; 32] =
        hex!("90F2D60F917F2423F0989CE9979639435A5B01CF7AB3E0A1B900BEB892BAE443");
}

#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_duration_subtraction)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    use std::time::Duration;

    use super::test_data::*;
    use super::*;
    use hex_literal::hex;
    use tor_checkable::{SelfSigned, Timebound};
    use tor_hscrypto::{pk::HsIdKey, time::TimePeriod};
    use tor_llcrypto::pk::ed25519;

    #[test]
    fn parse_meta_good() -> Result<()> {
        let meta = StoredHsDescMeta::parse(TEST_DATA)?
            .check_signature()?
            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
            .unwrap();

        assert_eq!(
            meta.blinded_id.as_ref(),
            &hex!("43cc0d62fc6252f578705ca645a46109e265290343b1137e90189744b20b3f2d")
        );
        assert_eq!(
            Duration::try_from(meta.idx_info.lifetime).unwrap(),
            Duration::from_secs(60 * 180)
        );
        assert_eq!(
            meta.idx_info.signing_cert_expires,
            humantime::parse_rfc3339("2023-01-26T03:00:00Z").unwrap()
        );
        assert_eq!(meta.idx_info.revision, RevisionCounter::from(19655750));

        Ok(())
    }

    #[test]
    fn parse_desc_good() -> Result<()> {
        let wrong_blinded_id = [12; 32].into();
        let desc = HsDesc::parse(TEST_DATA, &wrong_blinded_id);
        assert!(desc.is_err());
        let blinded_id =
            hex!("43cc0d62fc6252f578705ca645a46109e265290343b1137e90189744b20b3f2d").into();
        let desc = HsDesc::parse(TEST_DATA, &blinded_id)?
            .check_signature()?
            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
            .unwrap()
            .decrypt(&TEST_SUBCREDENTIAL.into(), None)?;
        let desc = desc
            .check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap())
            .unwrap();
        let desc = desc.check_signature().unwrap();

        assert_eq!(
            Duration::try_from(desc.idx_info.lifetime).unwrap(),
            Duration::from_secs(60 * 180)
        );
        assert_eq!(
            desc.idx_info.signing_cert_expires,
            humantime::parse_rfc3339("2023-01-26T03:00:00Z").unwrap()
        );
        assert_eq!(desc.idx_info.revision, RevisionCounter::from(19655750));
        assert!(desc.decrypted_with_id.is_none());
        assert!(desc.auth_required.is_none());
        assert_eq!(desc.is_single_onion_service, false);
        assert_eq!(desc.intro_points.len(), 3);

        // TODO hs: add checks that the intro point fields are as expected.

        Ok(())
    }

    /// Get an EncryptedHsDesc corresponding to `TEST_DATA_2`.
    fn get_test2_encrypted() -> EncryptedHsDesc {
        let id: HsIdKey = ed25519::PublicKey::from_bytes(&TEST_HSID_2).unwrap().into();
        let period = TimePeriod::new(
            humantime::parse_duration("24 hours").unwrap(),
            humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap(),
            humantime::parse_duration("12 hours").unwrap(),
        )
        .unwrap();
        assert_eq!(period.interval_num(), TEST_DATA_TIMEPERIOD_2);
        let (blind_id, subcredential) = id.compute_blinded_key(period).unwrap();

        assert_eq!(
            blind_id.as_bytes(),
            &hex!("706628758208395D461AA0F460A5E76E7B828C66B5E794768592B451302E961D")
        );

        assert_eq!(subcredential.as_ref(), &TEST_SUBCREDENTIAL_2);

        HsDesc::parse(TEST_DATA_2, &blind_id.into())
            .unwrap()
            .check_signature()
            .unwrap()
            .check_valid_at(&humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap())
            .unwrap()
    }

    #[test]
    fn parse_desc_auth_missing() {
        // If we try to decrypt TEST_DATA_2 with no ClientDescEncKey, we get a
        // failure.
        let encrypted = get_test2_encrypted();
        let subcredential = TEST_SUBCREDENTIAL_2.into();
        let with_no_auth = encrypted.decrypt(&subcredential, None);
        assert!(with_no_auth.is_err());
    }

    #[test]
    fn parse_desc_auth_good() {
        // But if we try to decrypt TEST_DATA_2 with the correct ClientDescEncKey, we get a
        // the data inside!

        let encrypted = get_test2_encrypted();
        let subcredential = TEST_SUBCREDENTIAL_2.into();
        let pk = curve25519::PublicKey::from(TEST_PUBKEY_2).into();
        let sk = curve25519::StaticSecret::from(TEST_SECKEY_2).into();
        let desc = encrypted.decrypt(&subcredential, Some((&pk, &sk))).unwrap();
        let desc = desc
            .check_valid_at(&humantime::parse_rfc3339("2023-01-24T03:00:00Z").unwrap())
            .unwrap();
        let desc = desc.check_signature().unwrap();
        assert_eq!(desc.intro_points.len(), 3);
    }
}