Skip to main content

tor_netdoc/doc/hsdesc/
build.rs

1//! Hidden service descriptor encoding.
2
3mod inner;
4mod middle;
5mod outer;
6
7use crate::NetdocBuilder;
8use crate::doc::hsdesc::{IntroAuthType, IntroPointDesc};
9use rand::{CryptoRng, RngCore};
10use tor_bytes::EncodeError;
11use tor_cell::chancell::msg::HandshakeType;
12use tor_cert::{CertEncodeError, CertType, CertifiedKey, Ed25519Cert, EncodedEd25519Cert};
13use tor_error::into_bad_api_usage;
14use tor_hscrypto::pk::{HsBlindIdKey, HsBlindIdKeypair, HsSvcDescEncKeypair};
15use tor_hscrypto::{RevisionCounter, Subcredential};
16use tor_llcrypto::pk::curve25519;
17use tor_llcrypto::pk::ed25519;
18use tor_units::IntegerMinutes;
19
20use derive_builder::Builder;
21use smallvec::SmallVec;
22
23use std::borrow::{Borrow, Cow};
24use std::time::SystemTime;
25
26use self::inner::HsDescInner;
27use self::middle::HsDescMiddle;
28use self::outer::HsDescOuter;
29
30use super::desc_enc::{HS_DESC_ENC_NONCE_LEN, HsDescEncNonce, HsDescEncryption};
31use super::pow::PowParams;
32
33/// An intermediary type for encoding hidden service descriptors.
34///
35/// This object is constructed via [`HsDescBuilder`], and then turned into a
36/// signed document using [`HsDescBuilder::build_sign()`].
37///
38/// TODO: Add an example for using this API.
39#[derive(Builder)]
40#[builder(public, derive(Debug, Clone), pattern = "owned", build_fn(vis = ""))]
41struct HsDesc<'a> {
42    /// The blinded hidden service public key used for the first half of the "SECRET_DATA" field.
43    ///
44    /// (See rend-spec v3 2.5.1.1 and 2.5.2.1.)
45    blinded_id: &'a HsBlindIdKey,
46    /// The short-term descriptor signing key (KP_hs_desc_sign, KS_hs_desc_sign).
47    hs_desc_sign: &'a ed25519::Keypair,
48    /// The descriptor signing key certificate.
49    ///
50    /// This certificate can be created using [`create_desc_sign_key_cert`].
51    hs_desc_sign_cert: EncodedEd25519Cert,
52    /// A list of recognized CREATE handshakes that this onion service supports.
53    create2_formats: &'a [HandshakeType],
54    /// A list of authentication types that this onion service supports.
55    auth_required: Option<SmallVec<[IntroAuthType; 2]>>,
56    /// If true, this a "single onion service" and is not trying to keep its own location private.
57    is_single_onion_service: bool,
58    /// One or more introduction points used to contact the onion service.
59    intro_points: &'a [IntroPointDesc],
60    /// The expiration time of an introduction point authentication key certificate.
61    intro_auth_key_cert_expiry: SystemTime,
62    /// The expiration time of an introduction point encryption key certificate.
63    intro_enc_key_cert_expiry: SystemTime,
64    /// Proof-of-work parameters.
65    #[builder(default)]
66    #[cfg(feature = "hs-pow-full")]
67    pow_params: Option<&'a PowParams>,
68
69    /// The list of clients authorized to discover the hidden service.
70    ///
71    /// If `None`, restricted discovery is disabled.
72    /// If `Some(&[])`, restricted discovery is enabled,
73    /// but there will be no authorized clients.
74    ///
75    /// If restricted discovery is disabled, the resulting middle document will contain a single
76    /// `auth-client` line populated with random values.
77    ///
78    /// Restricted discovery is disabled by default.
79    #[builder(default)]
80    auth_clients: Option<&'a [curve25519::PublicKey]>,
81    /// The lifetime of this descriptor, in minutes.
82    ///
83    /// This doesn't actually list the starting time or the end time for the
84    /// descriptor: presumably, because we didn't want to leak the onion
85    /// service's view of the wallclock.
86    lifetime: IntegerMinutes<u16>,
87    /// A revision counter to tell whether this descriptor is more or less recent
88    /// than another one for the same blinded ID.
89    revision_counter: RevisionCounter,
90    /// The "subcredential" of the onion service.
91    subcredential: Subcredential,
92
93    /// Maximum length of generated HsDesc.
94    ///
95    /// If the generated descriptor is larger than this, `build_sign` will return an error.
96    #[builder(field(type = "Option<usize>", build = "()"), setter(strip_option))]
97    #[allow(dead_code)]
98    max_generated_len: (),
99}
100
101/// Restricted discovery parameters.
102#[derive(Debug)]
103pub(super) struct ClientAuth<'a> {
104    /// An ephemeral x25519 keypair generated by the hidden service (`KP_hss_desc_enc`).
105    ///
106    /// A new keypair MUST be generated every time a descriptor is encoded, or the descriptor
107    /// encryption will not be secure.
108    ephemeral_key: HsSvcDescEncKeypair,
109    /// The list of clients authorized to discover this service.
110    auth_clients: &'a [curve25519::PublicKey],
111    /// The `N_hs_desc_enc` descriptor_cookie key generated by the hidden service.
112    ///
113    /// A new descriptor cookie is randomly generated for each descriptor.
114    descriptor_cookie: [u8; HS_DESC_ENC_NONCE_LEN],
115}
116
117impl<'a> ClientAuth<'a> {
118    /// Create a new `ClientAuth` using the specified authorized clients.
119    ///
120    /// If `auth_clients` is empty list, there will be no authorized clients.
121    ///
122    /// This returns `None` if the list of `auth_clients` is `None`.
123    fn new<R: RngCore + CryptoRng>(
124        auth_clients: Option<&'a [curve25519::PublicKey]>,
125        rng: &mut R,
126    ) -> Option<ClientAuth<'a>> {
127        let Some(auth_clients) = auth_clients else {
128            // Restricted discovery is disabled
129            return None;
130        };
131
132        // Generate a new `N_hs_desc_enc` descriptor_cookie key for this descriptor.
133        let descriptor_cookie = rand::Rng::random::<[u8; HS_DESC_ENC_NONCE_LEN]>(rng);
134
135        let secret = curve25519::StaticSecret::random_from_rng(rng);
136        let ephemeral_key = HsSvcDescEncKeypair {
137            public: curve25519::PublicKey::from(&secret).into(),
138            secret: secret.into(),
139        };
140
141        Some(ClientAuth {
142            ephemeral_key,
143            auth_clients,
144            descriptor_cookie,
145        })
146    }
147}
148
149impl<'a> NetdocBuilder for HsDescBuilder<'a> {
150    fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError> {
151        /// The superencrypted field must be padded to the nearest multiple of 10k bytes
152        ///
153        /// rend-spec-v3 2.5.1.1
154        const SUPERENCRYPTED_ALIGN: usize = 10 * (1 << 10);
155
156        let max_generated_len = self.max_generated_len.unwrap_or(usize::MAX);
157
158        let hs_desc = self
159            .build()
160            .map_err(into_bad_api_usage!("the HsDesc could not be built"))?;
161
162        let client_auth = ClientAuth::new(hs_desc.auth_clients, rng);
163
164        // Construct the inner (second layer) plaintext. This is the unencrypted value of the
165        // "encrypted" field.
166        let inner_plaintext = HsDescInner {
167            hs_desc_sign: hs_desc.hs_desc_sign,
168            create2_formats: hs_desc.create2_formats,
169            auth_required: hs_desc.auth_required.as_ref(),
170            is_single_onion_service: hs_desc.is_single_onion_service,
171            intro_points: hs_desc.intro_points,
172            intro_auth_key_cert_expiry: hs_desc.intro_auth_key_cert_expiry,
173            intro_enc_key_cert_expiry: hs_desc.intro_enc_key_cert_expiry,
174            #[cfg(feature = "hs-pow-full")]
175            pow_params: hs_desc.pow_params,
176        }
177        .build_sign(rng)?;
178
179        let desc_enc_nonce = client_auth
180            .as_ref()
181            .map(|client_auth| client_auth.descriptor_cookie.into());
182
183        // Encrypt the inner document. The encrypted blob is the ciphertext contained in the
184        // "encrypted" field described in section 2.5.1.2. of rend-spec-v3.
185        let inner_encrypted = hs_desc.encrypt_field(
186            rng,
187            inner_plaintext.as_bytes(),
188            desc_enc_nonce.as_ref(),
189            b"hsdir-encrypted-data",
190        );
191
192        // Construct the middle (first player) plaintext. This is the unencrypted value of the
193        // "superencrypted" field.
194        let middle_plaintext = HsDescMiddle {
195            client_auth: client_auth.as_ref(),
196            subcredential: hs_desc.subcredential,
197            encrypted: inner_encrypted,
198        }
199        .build_sign(rng)?;
200
201        // Section 2.5.1.1. of rend-spec-v3: before encryption, pad the plaintext to the nearest
202        // multiple of 10k bytes
203        let middle_plaintext =
204            pad_with_zero_to_align(middle_plaintext.as_bytes(), SUPERENCRYPTED_ALIGN);
205
206        // Encrypt the middle document. The encrypted blob is the ciphertext contained in the
207        // "superencrypted" field described in section 2.5.1.1. of rend-spec-v3.
208        let middle_encrypted = hs_desc.encrypt_field(
209            rng,
210            middle_plaintext.borrow(),
211            // desc_enc_nonce is absent when handling the superencryption layer (2.5.1.1).
212            None,
213            b"hsdir-superencrypted-data",
214        );
215
216        // Finally, build the hidden service descriptor.
217        let hsdesc = HsDescOuter {
218            hs_desc_sign: hs_desc.hs_desc_sign,
219            hs_desc_sign_cert: hs_desc.hs_desc_sign_cert,
220            lifetime: hs_desc.lifetime,
221            revision_counter: hs_desc.revision_counter,
222            superencrypted: middle_encrypted,
223        }
224        .build_sign(rng)?;
225
226        if hsdesc.len() > max_generated_len {
227            return Err(EncodeError::BadLengthValue);
228        }
229        Ok(hsdesc)
230    }
231}
232
233/// Create the descriptor signing key certificate.
234///
235/// Returns the encoded representation of the certificate
236/// obtained by signing the descriptor signing key `hs_desc_sign`
237/// with the blinded id key `blind_id`.
238///
239/// This certificate is meant to be passed to [`HsDescBuilder::hs_desc_sign_cert`].
240pub fn create_desc_sign_key_cert(
241    hs_desc_sign: &ed25519::PublicKey,
242    blind_id: &HsBlindIdKeypair,
243    expiry: SystemTime,
244) -> Result<EncodedEd25519Cert, CertEncodeError> {
245    // "The certificate cross-certifies the short-term descriptor signing key with the blinded
246    // public key.  The certificate type must be [08], and the blinded public key must be
247    // present as the signing-key extension."
248    Ed25519Cert::constructor()
249        .cert_type(CertType::HS_BLINDED_ID_V_SIGNING)
250        .expiration(expiry)
251        .signing_key(ed25519::Ed25519Identity::from(blind_id.as_ref().public()))
252        .cert_key(CertifiedKey::Ed25519(hs_desc_sign.into()))
253        .encode_and_sign(blind_id)
254}
255
256impl<'a> HsDesc<'a> {
257    /// Encrypt the specified plaintext using the algorithm described in section
258    /// `[HS-DESC-ENCRYPTION-KEYS]` of rend-spec-v3.txt.
259    fn encrypt_field<R: RngCore + CryptoRng>(
260        &self,
261        rng: &mut R,
262        plaintext: &[u8],
263        desc_enc_nonce: Option<&HsDescEncNonce>,
264        string_const: &[u8],
265    ) -> Vec<u8> {
266        let encrypt = HsDescEncryption {
267            blinded_id: &ed25519::Ed25519Identity::from(self.blinded_id.as_ref()).into(),
268            desc_enc_nonce,
269            subcredential: &self.subcredential,
270            revision: self.revision_counter,
271            string_const,
272        };
273
274        encrypt.encrypt(rng, plaintext)
275    }
276}
277
278/// Pad `v` with zeroes to the next multiple of `alignment`.
279fn pad_with_zero_to_align(v: &[u8], alignment: usize) -> Cow<[u8]> {
280    let padding = (alignment - (v.len() % alignment)) % alignment;
281
282    if padding > 0 {
283        let padded = v
284            .iter()
285            .copied()
286            .chain(std::iter::repeat_n(0, padding))
287            .collect::<Vec<_>>();
288
289        Cow::Owned(padded)
290    } else {
291        // No need to pad.
292        Cow::Borrowed(v)
293    }
294}
295
296#[cfg(test)]
297mod test {
298    // @@ begin test lint list maintained by maint/add_warning @@
299    #![allow(clippy::bool_assert_comparison)]
300    #![allow(clippy::clone_on_copy)]
301    #![allow(clippy::dbg_macro)]
302    #![allow(clippy::mixed_attributes_style)]
303    #![allow(clippy::print_stderr)]
304    #![allow(clippy::print_stdout)]
305    #![allow(clippy::single_char_pattern)]
306    #![allow(clippy::unwrap_used)]
307    #![allow(clippy::unchecked_time_subtraction)]
308    #![allow(clippy::useless_vec)]
309    #![allow(clippy::needless_pass_by_value)]
310    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
311
312    use std::net::Ipv4Addr;
313    use std::time::Duration;
314
315    use super::*;
316    use crate::doc::hsdesc::{EncryptedHsDesc, HsDesc as ParsedHsDesc};
317    use tor_basic_utils::test_rng::Config;
318    use tor_checkable::{SelfSigned, Timebound};
319    use tor_hscrypto::pk::{HsClientDescEncKeypair, HsIdKeypair};
320    use tor_hscrypto::time::TimePeriod;
321    use tor_linkspec::LinkSpec;
322    use tor_llcrypto::pk::{curve25519, ed25519::ExpandedKeypair};
323    use web_time_compat::SystemTimeExt;
324
325    // TODO: move the test helpers to a separate module and make them more broadly available if
326    // necessary.
327
328    /// Expect `err` to be a `Bug`, and return its string representation.
329    ///
330    /// # Panics
331    ///
332    /// Panics if `err` is not a `Bug`.
333    pub(super) fn expect_bug(err: EncodeError) -> String {
334        match err {
335            EncodeError::Bug(b) => b.to_string(),
336            EncodeError::BadLengthValue => panic!("expected Bug, got BadLengthValue"),
337            _ => panic!("expected Bug, got unknown error"),
338        }
339    }
340
341    pub(super) fn create_intro_point_descriptor<R: RngCore + CryptoRng>(
342        rng: &mut R,
343        link_specifiers: &[LinkSpec],
344    ) -> IntroPointDesc {
345        let link_specifiers = link_specifiers
346            .iter()
347            .map(|link_spec| link_spec.encode())
348            .collect::<Result<Vec<_>, _>>()
349            .unwrap();
350
351        IntroPointDesc {
352            link_specifiers,
353            ipt_ntor_key: create_curve25519_pk(rng),
354            ipt_sid_key: ed25519::Keypair::generate(rng).verifying_key().into(),
355            svc_ntor_key: create_curve25519_pk(rng).into(),
356        }
357    }
358
359    /// Create a new curve25519 public key.
360    pub(super) fn create_curve25519_pk<R: RngCore + CryptoRng>(
361        rng: &mut R,
362    ) -> curve25519::PublicKey {
363        let ephemeral_key = curve25519::EphemeralSecret::random_from_rng(rng);
364        (&ephemeral_key).into()
365    }
366
367    /// Parse the specified hidden service descriptor.
368    fn parse_hsdesc(
369        unparsed_desc: &str,
370        blinded_pk: ed25519::PublicKey,
371        subcredential: &Subcredential,
372        hsc_desc_enc: Option<&HsClientDescEncKeypair>,
373    ) -> ParsedHsDesc {
374        const TIMESTAMP: &str = "2023-01-23T15:00:00Z";
375
376        let id = ed25519::Ed25519Identity::from(blinded_pk);
377        let enc_desc: EncryptedHsDesc = ParsedHsDesc::parse(unparsed_desc, &id.into())
378            .unwrap()
379            .check_signature()
380            .unwrap()
381            .check_valid_at(&humantime::parse_rfc3339(TIMESTAMP).unwrap())
382            .unwrap();
383
384        enc_desc
385            .decrypt(subcredential, hsc_desc_enc)
386            .unwrap()
387            .check_valid_at(&humantime::parse_rfc3339(TIMESTAMP).unwrap())
388            .unwrap()
389            .check_signature()
390            .unwrap()
391    }
392
393    #[test]
394    fn encode_decode() {
395        const CREATE2_FORMATS: &[HandshakeType] = &[HandshakeType::TAP, HandshakeType::NTOR];
396        const LIFETIME_MINS: u16 = 100;
397        const REVISION_COUNT: u64 = 2;
398        const CERT_EXPIRY_SECS: u64 = 60 * 60;
399
400        let mut rng = Config::Deterministic.into_rng();
401        // The identity keypair of the hidden service.
402        let hs_id = ed25519::Keypair::generate(&mut rng);
403        let hs_desc_sign = ed25519::Keypair::generate(&mut rng);
404        let period = TimePeriod::new(
405            humantime::parse_duration("24 hours").unwrap(),
406            humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap(),
407            humantime::parse_duration("12 hours").unwrap(),
408        )
409        .unwrap();
410        let (_, blinded_id, subcredential) = HsIdKeypair::from(ExpandedKeypair::from(&hs_id))
411            .compute_blinded_key(period)
412            .unwrap();
413
414        let expiry = SystemTime::get() + Duration::from_secs(CERT_EXPIRY_SECS);
415        let mut rng = Config::Deterministic.into_rng();
416        let intro_points = vec![IntroPointDesc {
417            link_specifiers: vec![
418                LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 9999)
419                    .encode()
420                    .unwrap(),
421            ],
422            ipt_ntor_key: create_curve25519_pk(&mut rng),
423            ipt_sid_key: ed25519::Keypair::generate(&mut rng).verifying_key().into(),
424            svc_ntor_key: create_curve25519_pk(&mut rng).into(),
425        }];
426
427        let hs_desc_sign_cert =
428            create_desc_sign_key_cert(&hs_desc_sign.verifying_key(), &blinded_id, expiry).unwrap();
429        let blinded_pk = (&blinded_id).into();
430        let builder = HsDescBuilder::default()
431            .blinded_id(&blinded_pk)
432            .hs_desc_sign(&hs_desc_sign)
433            .hs_desc_sign_cert(hs_desc_sign_cert)
434            .create2_formats(CREATE2_FORMATS)
435            .auth_required(None)
436            .is_single_onion_service(true)
437            .intro_points(&intro_points)
438            .intro_auth_key_cert_expiry(expiry)
439            .intro_enc_key_cert_expiry(expiry)
440            .lifetime(LIFETIME_MINS.into())
441            .revision_counter(REVISION_COUNT.into())
442            .subcredential(subcredential);
443
444        // Build and encode a new descriptor (cloning `builder` because it's needed later, when we
445        // test if restricted discovery works):
446        let encoded_desc = builder
447            .clone()
448            .build_sign(&mut Config::Deterministic.into_rng())
449            .unwrap();
450
451        // Now decode it...
452        let desc = parse_hsdesc(
453            encoded_desc.as_str(),
454            *blinded_id.as_ref().public(),
455            &subcredential,
456            None, /* No restricted discovery */
457        );
458
459        let hs_desc_sign_cert =
460            create_desc_sign_key_cert(&hs_desc_sign.verifying_key(), &blinded_id, expiry).unwrap();
461        // ...and build a new descriptor using the information from the parsed descriptor,
462        // asserting that the resulting descriptor is identical to the original.
463        let reencoded_desc = HsDescBuilder::default()
464            .blinded_id(&(&blinded_id).into())
465            .hs_desc_sign(&hs_desc_sign)
466            .hs_desc_sign_cert(hs_desc_sign_cert)
467            // create2_formats is hard-coded rather than extracted from desc, because
468            // create2_formats is ignored while parsing
469            .create2_formats(CREATE2_FORMATS)
470            .auth_required(None)
471            .is_single_onion_service(desc.is_single_onion_service)
472            .intro_points(&intro_points)
473            .intro_auth_key_cert_expiry(expiry)
474            .intro_enc_key_cert_expiry(expiry)
475            .lifetime(desc.idx_info.lifetime)
476            .revision_counter(desc.idx_info.revision)
477            .subcredential(subcredential)
478            .build_sign(&mut Config::Deterministic.into_rng())
479            .unwrap();
480
481        assert_eq!(&*encoded_desc, &*reencoded_desc);
482
483        // The same test, this time with restricted discovery enabled
484        // (with a single authorized client):
485        let client_kp: HsClientDescEncKeypair = HsClientDescEncKeypair::generate(&mut rng);
486        let client_pkey = client_kp.public().as_ref();
487        let auth_clients = vec![*client_pkey];
488
489        let encoded_desc = builder
490            .auth_clients(Some(&auth_clients[..]))
491            .build_sign(&mut Config::Deterministic.into_rng())
492            .unwrap();
493
494        // Now decode it...
495        let desc = parse_hsdesc(
496            encoded_desc.as_str(),
497            *blinded_id.as_ref().public(),
498            &subcredential,
499            Some(&client_kp), /* With restricted discovery */
500        );
501
502        let hs_desc_sign_cert =
503            create_desc_sign_key_cert(&hs_desc_sign.verifying_key(), &blinded_id, expiry).unwrap();
504        // ...and build a new descriptor using the information from the parsed descriptor,
505        // asserting that the resulting descriptor is identical to the original.
506        let reencoded_desc = HsDescBuilder::default()
507            .blinded_id(&(&blinded_id).into())
508            .hs_desc_sign(&hs_desc_sign)
509            .hs_desc_sign_cert(hs_desc_sign_cert)
510            // create2_formats is hard-coded rather than extracted from desc, because
511            // create2_formats is ignored while parsing
512            .create2_formats(CREATE2_FORMATS)
513            .auth_required(None)
514            .is_single_onion_service(desc.is_single_onion_service)
515            .intro_points(&intro_points)
516            .intro_auth_key_cert_expiry(expiry)
517            .intro_enc_key_cert_expiry(expiry)
518            .auth_clients(Some(&auth_clients))
519            .lifetime(desc.idx_info.lifetime)
520            .revision_counter(desc.idx_info.revision)
521            .subcredential(subcredential)
522            .build_sign(&mut Config::Deterministic.into_rng())
523            .unwrap();
524
525        assert_eq!(&*encoded_desc, &*reencoded_desc);
526    }
527}