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