Skip to main content

tor_netdoc/doc/hsdesc/build/
inner.rs

1//! Functionality for encoding the inner document of an onion service descriptor.
2//!
3//! NOTE: `HsDescInner` is a private helper for building hidden service descriptors, and is
4//! not meant to be used directly. Hidden services will use `HsDescBuilder` to build and encode
5//! hidden service descriptors.
6
7use crate::NetdocBuilder;
8use crate::doc::hsdesc::IntroAuthType;
9use crate::doc::hsdesc::IntroPointDesc;
10use crate::doc::hsdesc::inner::HsInnerKwd;
11use crate::doc::hsdesc::pow::PowParams;
12use crate::doc::hsdesc::pow::v1::PowParamsV1;
13use crate::encode::ItemArgument;
14use crate::encode::NetdocEncoder;
15use crate::types::misc::Iso8601TimeNoSp;
16
17use rand::CryptoRng;
18use rand::RngCore;
19use tor_bytes::{EncodeError, Writer};
20use tor_cell::chancell::msg::HandshakeType;
21use tor_cert::{CertType, CertifiedKey, Ed25519Cert};
22use tor_error::internal;
23use tor_error::{bad_api_usage, into_bad_api_usage};
24use tor_llcrypto::pk::ed25519;
25use tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public;
26
27use base64ct::{Base64, Encoding};
28
29use std::time::SystemTime;
30
31use smallvec::SmallVec;
32
33/// The representation of the inner document of an onion service descriptor.
34///
35/// The plaintext format of this document is described in section 2.5.2.2. of rend-spec-v3.
36#[derive(Debug)]
37pub(super) struct HsDescInner<'a> {
38    /// The descriptor signing key.
39    pub(super) hs_desc_sign: &'a ed25519::Keypair,
40    /// A list of recognized CREATE handshakes that this onion service supports.
41    pub(super) create2_formats: &'a [HandshakeType],
42    /// A list of authentication types that this onion service supports.
43    pub(super) auth_required: Option<&'a SmallVec<[IntroAuthType; 2]>>,
44    /// If true, this a "single onion service" and is not trying to keep its own location private.
45    pub(super) is_single_onion_service: bool,
46    /// One or more introduction points used to contact the onion service.
47    pub(super) intro_points: &'a [IntroPointDesc],
48    /// The expiration time of an introduction point authentication key certificate.
49    pub(super) intro_auth_key_cert_expiry: SystemTime,
50    /// The expiration time of an introduction point encryption key certificate.
51    pub(super) intro_enc_key_cert_expiry: SystemTime,
52    /// Proof-of-work parameters
53    #[cfg(feature = "hs-pow-full")]
54    pub(super) pow_params: Option<&'a PowParams>,
55}
56
57/// Encode the pow-params line.
58#[cfg(feature = "hs-pow-full")]
59fn encode_pow_params(
60    encoder: &mut NetdocEncoder,
61    pow_params: &PowParamsV1,
62) -> Result<(), EncodeError> {
63    let mut pow_params_enc = encoder.item(HsInnerKwd::POW_PARAMS);
64    pow_params_enc.add_arg(&"v1");
65
66    // It's safe to call dangerously_into_parts here, since we encode the
67    // expiration alongside the value.
68    let (seed, (_, expiration)) = pow_params.seed().clone().dangerously_into_parts();
69
70    seed.write_arg_onto(&mut pow_params_enc)?;
71
72    pow_params
73        .suggested_effort()
74        .write_arg_onto(&mut pow_params_enc)?;
75
76    let expiration = if let Some(expiration) = expiration {
77        expiration
78    } else {
79        return Err(internal!("PoW seed should always have expiration").into());
80    };
81
82    Iso8601TimeNoSp::from(expiration).write_arg_onto(&mut pow_params_enc)?;
83
84    Ok(())
85}
86
87impl<'a> NetdocBuilder for HsDescInner<'a> {
88    fn build_sign<R: RngCore + CryptoRng>(self, _: &mut R) -> Result<String, EncodeError> {
89        use HsInnerKwd::*;
90
91        let HsDescInner {
92            hs_desc_sign,
93            create2_formats,
94            auth_required,
95            is_single_onion_service,
96            intro_points,
97            intro_auth_key_cert_expiry,
98            intro_enc_key_cert_expiry,
99            #[cfg(feature = "hs-pow-full")]
100            pow_params,
101        } = self;
102
103        let mut encoder = NetdocEncoder::new();
104
105        {
106            let mut create2_formats_enc = encoder.item(CREATE2_FORMATS);
107            for fmt in create2_formats {
108                let fmt: u16 = (*fmt).into();
109                create2_formats_enc = create2_formats_enc.arg(&fmt);
110            }
111        }
112
113        {
114            if let Some(auth_required) = auth_required {
115                let mut auth_required_enc = encoder.item(INTRO_AUTH_REQUIRED);
116                for auth in auth_required {
117                    auth_required_enc = auth_required_enc.arg(&auth.to_string());
118                }
119            }
120        }
121
122        if is_single_onion_service {
123            encoder.item(SINGLE_ONION_SERVICE);
124        }
125
126        #[cfg(feature = "hs-pow-full")]
127        if let Some(pow_params) = pow_params {
128            match pow_params {
129                #[cfg(feature = "hs-pow-full")]
130                PowParams::V1(pow_params) => encode_pow_params(&mut encoder, pow_params)?,
131                #[cfg(not(feature = "hs-pow-full"))]
132                PowParams::V1(_) => {
133                    return Err(internal!(
134                        "Got a V1 PoW params but support for V1 is disabled."
135                    ));
136                }
137            }
138        }
139
140        // We sort the introduction points here so as not to expose
141        // detail about the order in which they were added, which might
142        // be useful to an attacker somehow.  The choice of ntor
143        // key is arbitrary; we could sort by anything, really.
144        //
145        // TODO SPEC: Either specify that we should sort by ntor key,
146        // or sort by something else and specify that.
147        let mut sorted_ip: Vec<_> = intro_points.iter().collect();
148        sorted_ip.sort_by_key(|key| key.ipt_ntor_key.as_bytes());
149        for intro_point in sorted_ip {
150            // rend-spec-v3 0.4. "Protocol building blocks [BUILDING-BLOCKS]": the number of link
151            // specifiers (NPSEC) must fit in a single byte.
152            let nspec: u8 = intro_point
153                .link_specifiers
154                .len()
155                .try_into()
156                .map_err(into_bad_api_usage!("Too many link specifiers."))?;
157
158            let mut link_specifiers = vec![];
159            link_specifiers.write_u8(nspec);
160
161            for link_spec in &intro_point.link_specifiers {
162                link_specifiers.write(link_spec)?;
163            }
164
165            encoder
166                .item(INTRODUCTION_POINT)
167                .arg(&Base64::encode_string(&link_specifiers));
168            encoder
169                .item(ONION_KEY)
170                .arg(&"ntor")
171                .arg(&Base64::encode_string(&intro_point.ipt_ntor_key.to_bytes()));
172
173            // For compatibility with c-tor, the introduction point authentication key is signed by
174            // the descriptor signing key.
175            let signed_auth_key = Ed25519Cert::builder()
176                .cert_type(CertType::HS_IP_V_SIGNING)
177                .expiration(intro_auth_key_cert_expiry)
178                .signing_key(ed25519::Ed25519Identity::from(hs_desc_sign.verifying_key()))
179                .cert_key(CertifiedKey::Ed25519((*intro_point.ipt_sid_key).into()))
180                .encode_and_sign(hs_desc_sign)
181                .map_err(into_bad_api_usage!("failed to sign the intro auth key"))?;
182
183            encoder
184                .item(AUTH_KEY)
185                .object_bytes("ED25519 CERT", signed_auth_key.as_ref());
186
187            // "The key is a base64 encoded curve25519 public key used to encrypt the introduction
188            // request to service. (`KP_hss_ntor`)"
189            //
190            // TODO: The spec allows for multiple enc-key lines, but we currently only ever encode
191            // a single one.
192            encoder
193                .item(ENC_KEY)
194                .arg(&"ntor")
195                .arg(&Base64::encode_string(
196                    &intro_point.svc_ntor_key.as_bytes()[..],
197                ));
198
199            // The subject key is the ed25519 equivalent of the svc_ntor_key
200            // curve25519 public encryption key, with its sign bit set to 0.
201            //
202            // (Setting the sign bit to zero has a 50% chance of making the
203            // ed25519 public key useless for checking signatures, but that's
204            // okay: since this cert is generated with its signing/subject keys
205            // reversed (for compatibility reasons), we never actually generate
206            // or check any signatures using this key.)
207            let signbit = 0;
208            let ed_svc_ntor_key =
209                convert_curve25519_to_ed25519_public(&intro_point.svc_ntor_key, signbit)
210                    .ok_or_else(|| {
211                        bad_api_usage!("failed to convert curve25519 pk to ed25519 pk")
212                    })?;
213
214            // For compatibility with c-tor, the encryption key is signed with the descriptor
215            // signing key.
216            let signed_enc_key = Ed25519Cert::builder()
217                .cert_type(CertType::HS_IP_CC_SIGNING)
218                .expiration(intro_enc_key_cert_expiry)
219                .signing_key(ed25519::Ed25519Identity::from(hs_desc_sign.verifying_key()))
220                .cert_key(CertifiedKey::Ed25519(ed25519::Ed25519Identity::from(
221                    &ed_svc_ntor_key,
222                )))
223                .encode_and_sign(hs_desc_sign)
224                .map_err(into_bad_api_usage!(
225                    "failed to sign the intro encryption key"
226                ))?;
227
228            encoder
229                .item(ENC_KEY_CERT)
230                .object_bytes("ED25519 CERT", signed_enc_key.as_ref());
231        }
232
233        encoder.finish().map_err(|e| e.into())
234    }
235}
236
237#[cfg(test)]
238mod test {
239    // @@ begin test lint list maintained by maint/add_warning @@
240    #![allow(clippy::bool_assert_comparison)]
241    #![allow(clippy::clone_on_copy)]
242    #![allow(clippy::dbg_macro)]
243    #![allow(clippy::mixed_attributes_style)]
244    #![allow(clippy::print_stderr)]
245    #![allow(clippy::print_stdout)]
246    #![allow(clippy::single_char_pattern)]
247    #![allow(clippy::unwrap_used)]
248    #![allow(clippy::unchecked_time_subtraction)]
249    #![allow(clippy::useless_vec)]
250    #![allow(clippy::needless_pass_by_value)]
251    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
252
253    use super::*;
254    use crate::doc::hsdesc::IntroAuthType;
255    use crate::doc::hsdesc::build::test::{create_intro_point_descriptor, expect_bug};
256    use crate::doc::hsdesc::pow::v1::PowParamsV1;
257
258    use smallvec::SmallVec;
259    use std::net::Ipv4Addr;
260    use std::time::UNIX_EPOCH;
261    use tor_basic_utils::test_rng::Config;
262    use tor_checkable::timed::TimerangeBound;
263    #[cfg(feature = "hs-pow-full")]
264    use tor_hscrypto::pow::v1::{Effort, Seed};
265    use tor_linkspec::LinkSpec;
266
267    /// Build an inner document using the specified parameters.
268    fn create_inner_desc(
269        create2_formats: &[HandshakeType],
270        auth_required: Option<&SmallVec<[IntroAuthType; 2]>>,
271        is_single_onion_service: bool,
272        intro_points: &[IntroPointDesc],
273        pow_params: Option<&PowParams>,
274    ) -> Result<String, EncodeError> {
275        let hs_desc_sign = ed25519::Keypair::generate(&mut Config::Deterministic.into_rng());
276
277        HsDescInner {
278            hs_desc_sign: &hs_desc_sign,
279            create2_formats,
280            auth_required,
281            is_single_onion_service,
282            intro_points,
283            intro_auth_key_cert_expiry: UNIX_EPOCH,
284            intro_enc_key_cert_expiry: UNIX_EPOCH,
285            #[cfg(feature = "hs-pow-full")]
286            pow_params,
287        }
288        .build_sign(&mut rand::rng())
289    }
290
291    #[test]
292    fn inner_hsdesc_no_intro_auth() {
293        // A descriptor for a "single onion service"
294        let hs_desc = create_inner_desc(
295            &[HandshakeType::NTOR], /* create2_formats */
296            None,                   /* auth_required */
297            true,                   /* is_single_onion_service */
298            &[],                    /* intro_points */
299            None,
300        )
301        .unwrap();
302
303        assert_eq!(hs_desc, "create2-formats 2\nsingle-onion-service\n");
304
305        // A descriptor for a location-hidden service
306        let hs_desc = create_inner_desc(
307            &[HandshakeType::NTOR], /* create2_formats */
308            None,                   /* auth_required */
309            false,                  /* is_single_onion_service */
310            &[],                    /* intro_points */
311            None,
312        )
313        .unwrap();
314
315        assert_eq!(hs_desc, "create2-formats 2\n");
316
317        let link_specs1 = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 1234)];
318        let link_specs2 = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 5679)];
319        let link_specs3 = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8901)];
320
321        let mut rng = Config::Deterministic.into_rng();
322        let intros = &[
323            create_intro_point_descriptor(&mut rng, link_specs1),
324            create_intro_point_descriptor(&mut rng, link_specs2),
325            create_intro_point_descriptor(&mut rng, link_specs3),
326        ];
327
328        let hs_desc = create_inner_desc(
329            &[
330                HandshakeType::TAP,
331                HandshakeType::NTOR,
332                HandshakeType::NTOR_V3,
333            ], /* create2_formats */
334            None,   /* auth_required */
335            false,  /* is_single_onion_service */
336            intros, /* intro_points */
337            None,
338        )
339        .unwrap();
340
341        assert_eq!(
342            hs_desc,
343            r#"create2-formats 0 2 3
344introduction-point AQAGfwAAASLF
345onion-key ntor CJi8nDPhIFA7X9Q+oP7+jzxNo044cblmagk/d7oKWGc=
346auth-key
347-----BEGIN ED25519 CERT-----
348AQkAAAAAAU4J4xGrMt9q5eHYZSmbOZTi1iKl59nd3ItYXAa/ASlRAQAgBACQKRtN
349eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61CGkJzc/ECYHzJeeAKIkRFV/6jr9
350zAB5XnEFghZmXdDTQdqcPXAFydyeHWW4uR+Uii0wPI8VokbU0NoLTNYJGAM=
351-----END ED25519 CERT-----
352enc-key ntor TL7GcN+B++pB6eRN/0nBZGmWe125qh7ccQJ/Hhku+x8=
353enc-key-cert
354-----BEGIN ED25519 CERT-----
355AQsAAAAAAabaCv4gv9ddyIztD1J8my9mgotmWnkHX94buLAtt15aAQAgBACQKRtN
356eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61GxlI6caS8iFp2bLmg1+Pkgij47f
357eetKn+yDC5Q3eo/hJLDBGAQNOX7jFMdr9HjotjXIt6/Khfmg58CZC/gKhAw=
358-----END ED25519 CERT-----
359introduction-point AQAGfwAAAQTS
360onion-key ntor HWIigEAdcOgqgHPDFmzhhkeqvYP/GcMT2fKb5JY6ey8=
361auth-key
362-----BEGIN ED25519 CERT-----
363AQkAAAAAAZZVJwNlzVw1ZQGO7MTzC5MsySASd+fswAcjdTJJOifXAQAgBACQKRtN
364eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61IVW0XivcAKhvUvNUsU1CFznk3Mz
365KSsp/mBoKi2iY4f4eN2SXx8U6pmnxnXFxYP6obi+tc5QWj1Jbfl1Aci3TAA=
366-----END ED25519 CERT-----
367enc-key ntor 9Upi9XNWyqx3ZwHeQ5r3+Dh116k+C4yHeE9BcM68HDc=
368enc-key-cert
369-----BEGIN ED25519 CERT-----
370AQsAAAAAAcH+1K5m7pRnMc01mPp5AYVnJK1iZ/fKHwK0tVR/jtBvAQAgBACQKRtN
371eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61Hectpha37ioha85fpNt+/yDfebh
3726BKUUQ0jf3SMXuNgX8SV9NSabn14WCSdKG/8RoYBCTR+yRJX0dy55mjg+go=
373-----END ED25519 CERT-----
374introduction-point AQAGfwAAARYv
375onion-key ntor x/stThC6cVWJJUR7WERZj5VYVPTAOA/UDjHdtprJkiE=
376auth-key
377-----BEGIN ED25519 CERT-----
378AQkAAAAAAVMhalzZJ8txKHuCX8TEhmO3LbCvDgV0zMT4eQ49SDpBAQAgBACQKRtN
379eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61GdVAiMag0dquEx4IywKDLEhxA7N
3802RZFTS2QI+Sk3dyz46WO+epj1YBlgfOYCZlBEx+oFkRlUJdOc0Eu0sDlAw8=
381-----END ED25519 CERT-----
382enc-key ntor XI/a9NGh/7ClaFcKqtdI9DoP8da5ovwPDdgCHUr3xX0=
383enc-key-cert
384-----BEGIN ED25519 CERT-----
385AQsAAAAAAZYGETSx12Og2xqJNMS9kGOHTEFeBkFPi7k0UaFv5HNKAQAgBACQKRtN
386eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61E8vxB5lB83+rQnWmHLzpfuMUZjG
387o7Ct/ZB0j8YRB5lKSd07YAjA6Zo8kMnuZYX2Mb67TxWDQ/zlYJGOwLlj7A8=
388-----END ED25519 CERT-----
389"#
390        );
391    }
392
393    #[test]
394    fn inner_hsdesc_too_many_link_specifiers() {
395        let link_spec = LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 9999);
396        let link_specifiers =
397            std::iter::repeat_n(link_spec, u8::MAX as usize + 1).collect::<Vec<_>>();
398
399        let intros = &[create_intro_point_descriptor(
400            &mut Config::Deterministic.into_rng(),
401            &link_specifiers,
402        )];
403
404        // A descriptor for a location-hidden service with an introduction point with too many link
405        // specifiers
406        let err = create_inner_desc(
407            &[HandshakeType::NTOR], /* create2_formats */
408            None,                   /* auth_required */
409            false,                  /* is_single_onion_service */
410            intros,                 /* intro_points */
411            None,
412        )
413        .unwrap_err();
414
415        assert!(expect_bug(err).contains("Too many link specifiers."));
416    }
417
418    #[test]
419    fn inner_hsdesc_intro_auth() {
420        let mut rng = Config::Deterministic.into_rng();
421        let link_specs = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8080)];
422        let intros = &[create_intro_point_descriptor(&mut rng, link_specs)];
423        let auth = SmallVec::from([IntroAuthType::Ed25519, IntroAuthType::Ed25519]);
424
425        // A descriptor for a location-hidden service with 1 introduction points which requires
426        // auth.
427        let hs_desc = create_inner_desc(
428            &[HandshakeType::NTOR], /* create2_formats */
429            Some(&auth),            /* auth_required */
430            false,                  /* is_single_onion_service */
431            intros,                 /* intro_points */
432            None,
433        )
434        .unwrap();
435
436        assert_eq!(
437            hs_desc,
438            r#"create2-formats 2
439intro-auth-required ed25519 ed25519
440introduction-point AQAGfwAAAR+Q
441onion-key ntor HWIigEAdcOgqgHPDFmzhhkeqvYP/GcMT2fKb5JY6ey8=
442auth-key
443-----BEGIN ED25519 CERT-----
444AQkAAAAAAZZVJwNlzVw1ZQGO7MTzC5MsySASd+fswAcjdTJJOifXAQAgBACQKRtN
445eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61IVW0XivcAKhvUvNUsU1CFznk3Mz
446KSsp/mBoKi2iY4f4eN2SXx8U6pmnxnXFxYP6obi+tc5QWj1Jbfl1Aci3TAA=
447-----END ED25519 CERT-----
448enc-key ntor 9Upi9XNWyqx3ZwHeQ5r3+Dh116k+C4yHeE9BcM68HDc=
449enc-key-cert
450-----BEGIN ED25519 CERT-----
451AQsAAAAAAcH+1K5m7pRnMc01mPp5AYVnJK1iZ/fKHwK0tVR/jtBvAQAgBACQKRtN
452eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61Hectpha37ioha85fpNt+/yDfebh
4536BKUUQ0jf3SMXuNgX8SV9NSabn14WCSdKG/8RoYBCTR+yRJX0dy55mjg+go=
454-----END ED25519 CERT-----
455"#
456        );
457    }
458
459    #[test]
460    #[cfg(feature = "hs-pow-full")]
461    fn inner_hsdesc_pow_params() {
462        use humantime::parse_rfc3339;
463
464        let mut rng = Config::Deterministic.into_rng();
465        let link_specs = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8080)];
466        let intros = &[create_intro_point_descriptor(&mut rng, link_specs)];
467
468        let pow_expiration = parse_rfc3339("1994-04-29T00:00:00Z").unwrap();
469        let pow_params = PowParams::V1(PowParamsV1::new(
470            TimerangeBound::new(Seed::from([0; 32]), ..pow_expiration),
471            Effort::new(64),
472        ));
473
474        let hs_desc = create_inner_desc(
475            &[HandshakeType::NTOR], /* create2_formats */
476            None,                   /* auth_required */
477            false,                  /* is_single_onion_service */
478            intros,                 /* intro_points */
479            Some(&pow_params),
480        )
481        .unwrap();
482
483        assert!(hs_desc.contains(
484            "\npow-params v1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 64 1994-04-29T00:00:00\n"
485        ));
486    }
487}