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