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

            
7
use crate::build::ItemArgument;
8
use crate::build::NetdocEncoder;
9
use crate::doc::hsdesc::inner::HsInnerKwd;
10
use crate::doc::hsdesc::pow::v1::PowParamsV1;
11
use crate::doc::hsdesc::pow::PowParams;
12
use crate::doc::hsdesc::IntroAuthType;
13
use crate::doc::hsdesc::IntroPointDesc;
14
use crate::types::misc::Iso8601TimeNoSp;
15
use crate::NetdocBuilder;
16

            
17
use rand::CryptoRng;
18
use rand::RngCore;
19
use tor_bytes::{EncodeError, Writer};
20
use tor_cell::chancell::msg::HandshakeType;
21
use tor_cert::{CertType, CertifiedKey, Ed25519Cert};
22
use tor_error::internal;
23
use tor_error::{bad_api_usage, into_bad_api_usage};
24
use tor_llcrypto::pk::ed25519;
25
use tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public;
26

            
27
use base64ct::{Base64, Encoding};
28

            
29
use std::time::SystemTime;
30

            
31
use 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)]
37
pub(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
#[cfg(feature = "hs-pow-full")]
58
2
fn encode_pow_params(
59
2
    encoder: &mut NetdocEncoder,
60
2
    pow_params: &PowParamsV1,
61
2
) -> Result<(), EncodeError> {
62
2
    let mut pow_params_enc = encoder.item(HsInnerKwd::POW_PARAMS);
63
2
    pow_params_enc.add_arg(&"v1");
64
2

            
65
2
    // It's safe to call dangerously_into_parts here, since we encode the
66
2
    // expiration alongside the value.
67
2
    let (seed, (_, expiration)) = pow_params.seed().clone().dangerously_into_parts();
68
2

            
69
2
    seed.write_onto(&mut pow_params_enc)?;
70

            
71
2
    pow_params
72
2
        .suggested_effort()
73
2
        .write_onto(&mut pow_params_enc)?;
74

            
75
2
    let expiration = if let Some(expiration) = expiration {
76
2
        expiration
77
    } else {
78
        return Err(internal!("PoW seed should always have expiration").into());
79
    };
80

            
81
2
    Iso8601TimeNoSp::from(expiration).write_onto(&mut pow_params_enc)?;
82

            
83
2
    Ok(())
84
2
}
85

            
86
impl<'a> NetdocBuilder for HsDescInner<'a> {
87
164
    fn build_sign<R: RngCore + CryptoRng>(self, _: &mut R) -> Result<String, EncodeError> {
88
        use HsInnerKwd::*;
89

            
90
        let HsDescInner {
91
164
            hs_desc_sign,
92
164
            create2_formats,
93
164
            auth_required,
94
164
            is_single_onion_service,
95
164
            intro_points,
96
164
            intro_auth_key_cert_expiry,
97
164
            intro_enc_key_cert_expiry,
98
164
            #[cfg(feature = "hs-pow-full")]
99
164
            pow_params,
100
164
        } = self;
101
164

            
102
164
        let mut encoder = NetdocEncoder::new();
103
164

            
104
164
        {
105
164
            let mut create2_formats_enc = encoder.item(CREATE2_FORMATS);
106
340
            for fmt in create2_formats {
107
176
                let fmt: u16 = (*fmt).into();
108
176
                create2_formats_enc = create2_formats_enc.arg(&fmt);
109
176
            }
110
        }
111

            
112
        {
113
164
            if let Some(auth_required) = auth_required {
114
2
                let mut auth_required_enc = encoder.item(INTRO_AUTH_REQUIRED);
115
6
                for auth in auth_required {
116
4
                    auth_required_enc = auth_required_enc.arg(&auth.to_string());
117
4
                }
118
162
            }
119
        }
120

            
121
164
        if is_single_onion_service {
122
10
            encoder.item(SINGLE_ONION_SERVICE);
123
154
        }
124

            
125
        #[cfg(feature = "hs-pow-full")]
126
164
        if let Some(pow_params) = pow_params {
127
2
            match pow_params {
128
2
                #[cfg(feature = "hs-pow-full")]
129
2
                PowParams::V1(pow_params) => encode_pow_params(&mut encoder, pow_params)?,
130
                #[cfg(not(feature = "hs-pow-full"))]
131
                PowParams::V1(_) => {
132
                    return Err(internal!(
133
                        "Got a V1 PoW params but support for V1 is disabled."
134
                    ))
135
                }
136
            }
137
162
        }
138

            
139
        // We sort the introduction points here so as not to expose
140
        // detail about the order in which they were added, which might
141
        // be useful to an attacker somehow.  The choice of ntor
142
        // key is arbitrary; we could sort by anything, really.
143
        //
144
        // TODO SPEC: Either specify that we should sort by ntor key,
145
        // or sort by something else and specify that.
146
164
        let mut sorted_ip: Vec<_> = intro_points.iter().collect();
147
596
        sorted_ip.sort_by_key(|key| key.ipt_ntor_key.as_bytes());
148
614
        for intro_point in sorted_ip {
149
            // rend-spec-v3 0.4. "Protocol building blocks [BUILDING-BLOCKS]": the number of link
150
            // specifiers (NPSEC) must fit in a single byte.
151
452
            let nspec: u8 = intro_point
152
452
                .link_specifiers
153
452
                .len()
154
452
                .try_into()
155
452
                .map_err(into_bad_api_usage!("Too many link specifiers."))?;
156

            
157
450
            let mut link_specifiers = vec![];
158
450
            link_specifiers.write_u8(nspec);
159

            
160
1764
            for link_spec in &intro_point.link_specifiers {
161
1314
                link_specifiers.write(link_spec)?;
162
            }
163

            
164
450
            encoder
165
450
                .item(INTRODUCTION_POINT)
166
450
                .arg(&Base64::encode_string(&link_specifiers));
167
450
            encoder
168
450
                .item(ONION_KEY)
169
450
                .arg(&"ntor")
170
450
                .arg(&Base64::encode_string(&intro_point.ipt_ntor_key.to_bytes()));
171

            
172
            // For compatibility with c-tor, the introduction point authentication key is signed by
173
            // the descriptor signing key.
174
450
            let signed_auth_key = Ed25519Cert::constructor()
175
450
                .cert_type(CertType::HS_IP_V_SIGNING)
176
450
                .expiration(intro_auth_key_cert_expiry)
177
450
                .signing_key(ed25519::Ed25519Identity::from(hs_desc_sign.verifying_key()))
178
450
                .cert_key(CertifiedKey::Ed25519((*intro_point.ipt_sid_key).into()))
179
450
                .encode_and_sign(hs_desc_sign)
180
450
                .map_err(into_bad_api_usage!("failed to sign the intro auth key"))?;
181

            
182
450
            encoder
183
450
                .item(AUTH_KEY)
184
450
                .object("ED25519 CERT", signed_auth_key.as_ref());
185
450

            
186
450
            // "The key is a base64 encoded curve25519 public key used to encrypt the introduction
187
450
            // request to service. (`KP_hss_ntor`)"
188
450
            //
189
450
            // TODO: The spec allows for multiple enc-key lines, but we currently only ever encode
190
450
            // a single one.
191
450
            encoder
192
450
                .item(ENC_KEY)
193
450
                .arg(&"ntor")
194
450
                .arg(&Base64::encode_string(
195
450
                    &intro_point.svc_ntor_key.as_bytes()[..],
196
450
                ));
197
450

            
198
450
            // The subject key is the ed25519 equivalent of the svc_ntor_key
199
450
            // curve25519 public encryption key, with its sign bit set to 0.
200
450
            //
201
450
            // (Setting the sign bit to zero has a 50% chance of making the
202
450
            // ed25519 public key useless for checking signatures, but that's
203
450
            // okay: since this cert is generated with its signing/subject keys
204
450
            // reversed (for compatibility reasons), we never actually generate
205
450
            // or check any signatures using this key.)
206
450
            let signbit = 0;
207
450
            let ed_svc_ntor_key =
208
450
                convert_curve25519_to_ed25519_public(&intro_point.svc_ntor_key, signbit)
209
450
                    .ok_or_else(|| {
210
                        bad_api_usage!("failed to convert curve25519 pk to ed25519 pk")
211
450
                    })?;
212

            
213
            // For compatibility with c-tor, the encryption key is signed with the descriptor
214
            // signing key.
215
450
            let signed_enc_key = Ed25519Cert::constructor()
216
450
                .cert_type(CertType::HS_IP_CC_SIGNING)
217
450
                .expiration(intro_enc_key_cert_expiry)
218
450
                .signing_key(ed25519::Ed25519Identity::from(hs_desc_sign.verifying_key()))
219
450
                .cert_key(CertifiedKey::Ed25519(ed25519::Ed25519Identity::from(
220
450
                    &ed_svc_ntor_key,
221
450
                )))
222
450
                .encode_and_sign(hs_desc_sign)
223
450
                .map_err(into_bad_api_usage!(
224
450
                    "failed to sign the intro encryption key"
225
450
                ))?;
226

            
227
450
            encoder
228
450
                .item(ENC_KEY_CERT)
229
450
                .object("ED25519 CERT", signed_enc_key.as_ref());
230
        }
231

            
232
162
        encoder.finish().map_err(|e| e.into())
233
164
    }
234
}
235

            
236
#[cfg(test)]
237
mod test {
238
    // @@ begin test lint list maintained by maint/add_warning @@
239
    #![allow(clippy::bool_assert_comparison)]
240
    #![allow(clippy::clone_on_copy)]
241
    #![allow(clippy::dbg_macro)]
242
    #![allow(clippy::mixed_attributes_style)]
243
    #![allow(clippy::print_stderr)]
244
    #![allow(clippy::print_stdout)]
245
    #![allow(clippy::single_char_pattern)]
246
    #![allow(clippy::unwrap_used)]
247
    #![allow(clippy::unchecked_duration_subtraction)]
248
    #![allow(clippy::useless_vec)]
249
    #![allow(clippy::needless_pass_by_value)]
250
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
251

            
252
    use super::*;
253
    use crate::doc::hsdesc::build::test::{create_intro_point_descriptor, expect_bug};
254
    use crate::doc::hsdesc::pow::v1::PowParamsV1;
255
    use crate::doc::hsdesc::IntroAuthType;
256

            
257
    use rand::thread_rng;
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 thread_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
344
introduction-point AQAGfwAAASLF
345
onion-key ntor CJi8nDPhIFA7X9Q+oP7+jzxNo044cblmagk/d7oKWGc=
346
auth-key
347
-----BEGIN ED25519 CERT-----
348
AQkAAAAAAU4J4xGrMt9q5eHYZSmbOZTi1iKl59nd3ItYXAa/ASlRAQAgBACQKRtN
349
eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61CGkJzc/ECYHzJeeAKIkRFV/6jr9
350
zAB5XnEFghZmXdDTQdqcPXAFydyeHWW4uR+Uii0wPI8VokbU0NoLTNYJGAM=
351
-----END ED25519 CERT-----
352
enc-key ntor TL7GcN+B++pB6eRN/0nBZGmWe125qh7ccQJ/Hhku+x8=
353
enc-key-cert
354
-----BEGIN ED25519 CERT-----
355
AQsAAAAAAabaCv4gv9ddyIztD1J8my9mgotmWnkHX94buLAtt15aAQAgBACQKRtN
356
eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61GxlI6caS8iFp2bLmg1+Pkgij47f
357
eetKn+yDC5Q3eo/hJLDBGAQNOX7jFMdr9HjotjXIt6/Khfmg58CZC/gKhAw=
358
-----END ED25519 CERT-----
359
introduction-point AQAGfwAAAQTS
360
onion-key ntor HWIigEAdcOgqgHPDFmzhhkeqvYP/GcMT2fKb5JY6ey8=
361
auth-key
362
-----BEGIN ED25519 CERT-----
363
AQkAAAAAAZZVJwNlzVw1ZQGO7MTzC5MsySASd+fswAcjdTJJOifXAQAgBACQKRtN
364
eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61IVW0XivcAKhvUvNUsU1CFznk3Mz
365
KSsp/mBoKi2iY4f4eN2SXx8U6pmnxnXFxYP6obi+tc5QWj1Jbfl1Aci3TAA=
366
-----END ED25519 CERT-----
367
enc-key ntor 9Upi9XNWyqx3ZwHeQ5r3+Dh116k+C4yHeE9BcM68HDc=
368
enc-key-cert
369
-----BEGIN ED25519 CERT-----
370
AQsAAAAAAcH+1K5m7pRnMc01mPp5AYVnJK1iZ/fKHwK0tVR/jtBvAQAgBACQKRtN
371
eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61Hectpha37ioha85fpNt+/yDfebh
372
6BKUUQ0jf3SMXuNgX8SV9NSabn14WCSdKG/8RoYBCTR+yRJX0dy55mjg+go=
373
-----END ED25519 CERT-----
374
introduction-point AQAGfwAAARYv
375
onion-key ntor x/stThC6cVWJJUR7WERZj5VYVPTAOA/UDjHdtprJkiE=
376
auth-key
377
-----BEGIN ED25519 CERT-----
378
AQkAAAAAAVMhalzZJ8txKHuCX8TEhmO3LbCvDgV0zMT4eQ49SDpBAQAgBACQKRtN
379
eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61GdVAiMag0dquEx4IywKDLEhxA7N
380
2RZFTS2QI+Sk3dyz46WO+epj1YBlgfOYCZlBEx+oFkRlUJdOc0Eu0sDlAw8=
381
-----END ED25519 CERT-----
382
enc-key ntor XI/a9NGh/7ClaFcKqtdI9DoP8da5ovwPDdgCHUr3xX0=
383
enc-key-cert
384
-----BEGIN ED25519 CERT-----
385
AQsAAAAAAZYGETSx12Og2xqJNMS9kGOHTEFeBkFPi7k0UaFv5HNKAQAgBACQKRtN
386
eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61E8vxB5lB83+rQnWmHLzpfuMUZjG
387
o7Ct/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 = std::iter::repeat(link_spec)
397
            .take(u8::MAX as usize + 1)
398
            .collect::<Vec<_>>();
399

            
400
        let intros = &[create_intro_point_descriptor(
401
            &mut Config::Deterministic.into_rng(),
402
            &link_specifiers,
403
        )];
404

            
405
        // A descriptor for a location-hidden service with an introduction point with too many link
406
        // specifiers
407
        let err = create_inner_desc(
408
            &[HandshakeType::NTOR], /* create2_formats */
409
            None,                   /* auth_required */
410
            false,                  /* is_single_onion_service */
411
            intros,                 /* intro_points */
412
            None,
413
        )
414
        .unwrap_err();
415

            
416
        assert!(expect_bug(err).contains("Too many link specifiers."));
417
    }
418

            
419
    #[test]
420
    fn inner_hsdesc_intro_auth() {
421
        let mut rng = Config::Deterministic.into_rng();
422
        let link_specs = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8080)];
423
        let intros = &[create_intro_point_descriptor(&mut rng, link_specs)];
424
        let auth = SmallVec::from([IntroAuthType::Ed25519, IntroAuthType::Ed25519]);
425

            
426
        // A descriptor for a location-hidden service with 1 introduction points which requires
427
        // auth.
428
        let hs_desc = create_inner_desc(
429
            &[HandshakeType::NTOR], /* create2_formats */
430
            Some(&auth),            /* auth_required */
431
            false,                  /* is_single_onion_service */
432
            intros,                 /* intro_points */
433
            None,
434
        )
435
        .unwrap();
436

            
437
        assert_eq!(
438
            hs_desc,
439
            r#"create2-formats 2
440
intro-auth-required ed25519 ed25519
441
introduction-point AQAGfwAAAR+Q
442
onion-key ntor HWIigEAdcOgqgHPDFmzhhkeqvYP/GcMT2fKb5JY6ey8=
443
auth-key
444
-----BEGIN ED25519 CERT-----
445
AQkAAAAAAZZVJwNlzVw1ZQGO7MTzC5MsySASd+fswAcjdTJJOifXAQAgBACQKRtN
446
eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61IVW0XivcAKhvUvNUsU1CFznk3Mz
447
KSsp/mBoKi2iY4f4eN2SXx8U6pmnxnXFxYP6obi+tc5QWj1Jbfl1Aci3TAA=
448
-----END ED25519 CERT-----
449
enc-key ntor 9Upi9XNWyqx3ZwHeQ5r3+Dh116k+C4yHeE9BcM68HDc=
450
enc-key-cert
451
-----BEGIN ED25519 CERT-----
452
AQsAAAAAAcH+1K5m7pRnMc01mPp5AYVnJK1iZ/fKHwK0tVR/jtBvAQAgBACQKRtN
453
eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61Hectpha37ioha85fpNt+/yDfebh
454
6BKUUQ0jf3SMXuNgX8SV9NSabn14WCSdKG/8RoYBCTR+yRJX0dy55mjg+go=
455
-----END ED25519 CERT-----
456
"#
457
        );
458
    }
459

            
460
    #[test]
461
    #[cfg(feature = "hs-pow-full")]
462
    fn inner_hsdesc_pow_params() {
463
        use humantime::parse_rfc3339;
464

            
465
        let mut rng = Config::Deterministic.into_rng();
466
        let link_specs = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8080)];
467
        let intros = &[create_intro_point_descriptor(&mut rng, link_specs)];
468

            
469
        let pow_expiration = parse_rfc3339("1994-04-29T00:00:00Z").unwrap();
470
        let pow_params = PowParams::V1(PowParamsV1::new(
471
            TimerangeBound::new(Seed::from([0; 32]), ..pow_expiration),
472
            Effort::new(64),
473
        ));
474

            
475
        let hs_desc = create_inner_desc(
476
            &[HandshakeType::NTOR], /* create2_formats */
477
            None,                   /* auth_required */
478
            false,                  /* is_single_onion_service */
479
            intros,                 /* intro_points */
480
            Some(&pow_params),
481
        )
482
        .unwrap();
483

            
484
        assert!(hs_desc.contains(
485
            "\npow-params v1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 64 1994-04-29T00:00:00\n"
486
        ));
487
    }
488
}