tor_netdoc/doc/hsdesc/
build.rs

1//! Hidden service descriptor encoding.
2
3mod inner;
4mod middle;
5mod outer;
6
7use crate::doc::hsdesc::{IntroAuthType, IntroPointDesc};
8use crate::NetdocBuilder;
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::{HsDescEncNonce, HsDescEncryption, HS_DESC_ENC_NONCE_LEN};
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_duration_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
324    // TODO: move the test helpers to a separate module and make them more broadly available if
325    // necessary.
326
327    /// Expect `err` to be a `Bug`, and return its string representation.
328    ///
329    /// # Panics
330    ///
331    /// Panics if `err` is not a `Bug`.
332    pub(super) fn expect_bug(err: EncodeError) -> String {
333        match err {
334            EncodeError::Bug(b) => b.to_string(),
335            EncodeError::BadLengthValue => panic!("expected Bug, got BadLengthValue"),
336            _ => panic!("expected Bug, got unknown error"),
337        }
338    }
339
340    pub(super) fn create_intro_point_descriptor<R: RngCore + CryptoRng>(
341        rng: &mut R,
342        link_specifiers: &[LinkSpec],
343    ) -> IntroPointDesc {
344        let link_specifiers = link_specifiers
345            .iter()
346            .map(|link_spec| link_spec.encode())
347            .collect::<Result<Vec<_>, _>>()
348            .unwrap();
349
350        IntroPointDesc {
351            link_specifiers,
352            ipt_ntor_key: create_curve25519_pk(rng),
353            ipt_sid_key: ed25519::Keypair::generate(rng).verifying_key().into(),
354            svc_ntor_key: create_curve25519_pk(rng).into(),
355        }
356    }
357
358    /// Create a new curve25519 public key.
359    pub(super) fn create_curve25519_pk<R: RngCore + CryptoRng>(
360        rng: &mut R,
361    ) -> curve25519::PublicKey {
362        let ephemeral_key = curve25519::EphemeralSecret::random_from_rng(rng);
363        (&ephemeral_key).into()
364    }
365
366    /// Parse the specified hidden service descriptor.
367    fn parse_hsdesc(
368        unparsed_desc: &str,
369        blinded_pk: ed25519::PublicKey,
370        subcredential: &Subcredential,
371        hsc_desc_enc: Option<&HsClientDescEncKeypair>,
372    ) -> ParsedHsDesc {
373        const TIMESTAMP: &str = "2023-01-23T15:00:00Z";
374
375        let id = ed25519::Ed25519Identity::from(blinded_pk);
376        let enc_desc: EncryptedHsDesc = ParsedHsDesc::parse(unparsed_desc, &id.into())
377            .unwrap()
378            .check_signature()
379            .unwrap()
380            .check_valid_at(&humantime::parse_rfc3339(TIMESTAMP).unwrap())
381            .unwrap();
382
383        enc_desc
384            .decrypt(subcredential, hsc_desc_enc)
385            .unwrap()
386            .check_valid_at(&humantime::parse_rfc3339(TIMESTAMP).unwrap())
387            .unwrap()
388            .check_signature()
389            .unwrap()
390    }
391
392    #[test]
393    fn encode_decode() {
394        const CREATE2_FORMATS: &[HandshakeType] = &[HandshakeType::TAP, HandshakeType::NTOR];
395        const LIFETIME_MINS: u16 = 100;
396        const REVISION_COUNT: u64 = 2;
397        const CERT_EXPIRY_SECS: u64 = 60 * 60;
398
399        let mut rng = Config::Deterministic.into_rng();
400        // The identity keypair of the hidden service.
401        let hs_id = ed25519::Keypair::generate(&mut rng);
402        let hs_desc_sign = ed25519::Keypair::generate(&mut rng);
403        let period = TimePeriod::new(
404            humantime::parse_duration("24 hours").unwrap(),
405            humantime::parse_rfc3339("2023-02-09T12:00:00Z").unwrap(),
406            humantime::parse_duration("12 hours").unwrap(),
407        )
408        .unwrap();
409        let (_, blinded_id, subcredential) = HsIdKeypair::from(ExpandedKeypair::from(&hs_id))
410            .compute_blinded_key(period)
411            .unwrap();
412
413        let expiry = SystemTime::now() + Duration::from_secs(CERT_EXPIRY_SECS);
414        let mut rng = Config::Deterministic.into_rng();
415        let intro_points = vec![IntroPointDesc {
416            link_specifiers: vec![LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 9999)
417                .encode()
418                .unwrap()],
419            ipt_ntor_key: create_curve25519_pk(&mut rng),
420            ipt_sid_key: ed25519::Keypair::generate(&mut rng).verifying_key().into(),
421            svc_ntor_key: create_curve25519_pk(&mut rng).into(),
422        }];
423
424        let hs_desc_sign_cert =
425            create_desc_sign_key_cert(&hs_desc_sign.verifying_key(), &blinded_id, expiry).unwrap();
426        let blinded_pk = (&blinded_id).into();
427        let builder = HsDescBuilder::default()
428            .blinded_id(&blinded_pk)
429            .hs_desc_sign(&hs_desc_sign)
430            .hs_desc_sign_cert(hs_desc_sign_cert)
431            .create2_formats(CREATE2_FORMATS)
432            .auth_required(None)
433            .is_single_onion_service(true)
434            .intro_points(&intro_points)
435            .intro_auth_key_cert_expiry(expiry)
436            .intro_enc_key_cert_expiry(expiry)
437            .lifetime(LIFETIME_MINS.into())
438            .revision_counter(REVISION_COUNT.into())
439            .subcredential(subcredential);
440
441        // Build and encode a new descriptor (cloning `builder` because it's needed later, when we
442        // test if restricted discovery works):
443        let encoded_desc = builder
444            .clone()
445            .build_sign(&mut Config::Deterministic.into_rng())
446            .unwrap();
447
448        // Now decode it...
449        let desc = parse_hsdesc(
450            encoded_desc.as_str(),
451            *blinded_id.as_ref().public(),
452            &subcredential,
453            None, /* No restricted discovery */
454        );
455
456        let hs_desc_sign_cert =
457            create_desc_sign_key_cert(&hs_desc_sign.verifying_key(), &blinded_id, expiry).unwrap();
458        // ...and build a new descriptor using the information from the parsed descriptor,
459        // asserting that the resulting descriptor is identical to the original.
460        let reencoded_desc = HsDescBuilder::default()
461            .blinded_id(&(&blinded_id).into())
462            .hs_desc_sign(&hs_desc_sign)
463            .hs_desc_sign_cert(hs_desc_sign_cert)
464            // create2_formats is hard-coded rather than extracted from desc, because
465            // create2_formats is ignored while parsing
466            .create2_formats(CREATE2_FORMATS)
467            .auth_required(None)
468            .is_single_onion_service(desc.is_single_onion_service)
469            .intro_points(&intro_points)
470            .intro_auth_key_cert_expiry(expiry)
471            .intro_enc_key_cert_expiry(expiry)
472            .lifetime(desc.idx_info.lifetime)
473            .revision_counter(desc.idx_info.revision)
474            .subcredential(subcredential)
475            .build_sign(&mut Config::Deterministic.into_rng())
476            .unwrap();
477
478        assert_eq!(&*encoded_desc, &*reencoded_desc);
479
480        // The same test, this time with restricted discovery enabled
481        // (with a single authorized client):
482        let client_kp: HsClientDescEncKeypair = HsClientDescEncKeypair::generate(&mut rng);
483        let client_pkey = client_kp.public().as_ref();
484        let auth_clients = vec![*client_pkey];
485
486        let encoded_desc = builder
487            .auth_clients(Some(&auth_clients[..]))
488            .build_sign(&mut Config::Deterministic.into_rng())
489            .unwrap();
490
491        // Now decode it...
492        let desc = parse_hsdesc(
493            encoded_desc.as_str(),
494            *blinded_id.as_ref().public(),
495            &subcredential,
496            Some(&client_kp), /* With restricted discovery */
497        );
498
499        let hs_desc_sign_cert =
500            create_desc_sign_key_cert(&hs_desc_sign.verifying_key(), &blinded_id, expiry).unwrap();
501        // ...and build a new descriptor using the information from the parsed descriptor,
502        // asserting that the resulting descriptor is identical to the original.
503        let reencoded_desc = HsDescBuilder::default()
504            .blinded_id(&(&blinded_id).into())
505            .hs_desc_sign(&hs_desc_sign)
506            .hs_desc_sign_cert(hs_desc_sign_cert)
507            // create2_formats is hard-coded rather than extracted from desc, because
508            // create2_formats is ignored while parsing
509            .create2_formats(CREATE2_FORMATS)
510            .auth_required(None)
511            .is_single_onion_service(desc.is_single_onion_service)
512            .intro_points(&intro_points)
513            .intro_auth_key_cert_expiry(expiry)
514            .intro_enc_key_cert_expiry(expiry)
515            .auth_clients(Some(&auth_clients))
516            .lifetime(desc.idx_info.lifetime)
517            .revision_counter(desc.idx_info.revision)
518            .subcredential(subcredential)
519            .build_sign(&mut Config::Deterministic.into_rng())
520            .unwrap();
521
522        assert_eq!(&*encoded_desc, &*reencoded_desc);
523    }
524}