1
//! Code to handle the inner document of an onion service descriptor.
2

            
3
use std::time::SystemTime;
4

            
5
use super::{IntroAuthType, IntroPointDesc};
6
use crate::batching_split_before::IteratorExt as _;
7
use crate::parse::tokenize::{ItemResult, NetDocReader};
8
use crate::parse::{keyword::Keyword, parser::SectionRules};
9
use crate::types::misc::{UnvalidatedEdCert, B64};
10
use crate::{NetdocErrorKind as EK, Result};
11

            
12
use itertools::Itertools as _;
13
use once_cell::sync::Lazy;
14
use smallvec::SmallVec;
15
use tor_checkable::signed::SignatureGated;
16
use tor_checkable::timed::TimerangeBound;
17
use tor_checkable::Timebound;
18
use tor_hscrypto::pk::{HsIntroPtSessionIdKey, HsSvcNtorKey};
19
use tor_hscrypto::NUM_INTRO_POINT_MAX;
20
use tor_llcrypto::pk::ed25519::Ed25519Identity;
21
use tor_llcrypto::pk::{curve25519, ed25519, ValidatableSignature};
22

            
23
/// The contents of the inner document of an onion service descriptor.
24
#[derive(Debug, Clone)]
25
#[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
26
pub(crate) struct HsDescInner {
27
    /// The authentication types that this onion service accepts when
28
    /// connecting.
29
    //
30
    // TODO: This should probably be a bitfield or enum-set of something.
31
    // Once we know whether the "password" authentication type really exists,
32
    // let's change to a better representation here.
33
    pub(super) intro_auth_types: Option<SmallVec<[IntroAuthType; 2]>>,
34
    /// Is this onion service a "single onion service?"
35
    ///
36
    /// (A "single onion service" is one that is not attempting to anonymize
37
    /// itself.)
38
    pub(super) single_onion_service: bool,
39
    /// A list of advertised introduction points and their contact info.
40
    //
41
    // Always has >= 1 and <= NUM_INTRO_POINT_MAX entries
42
    pub(super) intro_points: Vec<IntroPointDesc>,
43
}
44

            
45
decl_keyword! {
46
    pub(crate) HsInnerKwd {
47
        "create2-formats" => CREATE2_FORMATS,
48
        "intro-auth-required" => INTRO_AUTH_REQUIRED,
49
        "single-onion-service" => SINGLE_ONION_SERVICE,
50
        "introduction-point" => INTRODUCTION_POINT,
51
        "onion-key" => ONION_KEY,
52
        "auth-key" => AUTH_KEY,
53
        "enc-key" => ENC_KEY,
54
        "enc-key-cert" => ENC_KEY_CERT,
55
        "legacy-key" => LEGACY_KEY,
56
        "legacy-key-cert" => LEGACY_KEY_CERT,
57
    }
58
}
59

            
60
/// Rules about how keywords appear in the header part of an onion service
61
/// descriptor.
62
static HS_INNER_HEADER_RULES: Lazy<SectionRules<HsInnerKwd>> = Lazy::new(|| {
63
    use HsInnerKwd::*;
64

            
65
    let mut rules = SectionRules::builder();
66
    rules.add(CREATE2_FORMATS.rule().required().args(1..));
67
    rules.add(INTRO_AUTH_REQUIRED.rule().args(1..));
68
    rules.add(SINGLE_ONION_SERVICE.rule());
69
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
70

            
71
    rules.build()
72
});
73

            
74
/// Rules about how keywords appear in each introduction-point section of an
75
/// onion service descriptor.
76
static HS_INNER_INTRO_RULES: Lazy<SectionRules<HsInnerKwd>> = Lazy::new(|| {
77
    use HsInnerKwd::*;
78

            
79
    let mut rules = SectionRules::builder();
80
    rules.add(INTRODUCTION_POINT.rule().required().args(1..));
81
    // Note: we're labeling ONION_KEY and ENC_KEY as "may_repeat", since even
82
    // though rend-spec labels them as "exactly once", they are allowed to
83
    // appear more than once so long as they appear only once _with an "ntor"_
84
    // key.  torspec!110 tries to document this issue.
85
    rules.add(ONION_KEY.rule().required().may_repeat().args(2..));
86
    rules.add(AUTH_KEY.rule().required().obj_required());
87
    rules.add(ENC_KEY.rule().required().may_repeat().args(2..));
88
    rules.add(ENC_KEY_CERT.rule().required().obj_required());
89
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
90

            
91
    // NOTE: We never look at the LEGACY_KEY* fields.  This does provide a
92
    // distinguisher for Arti implementations and C tor implementations, but
93
    // that's outside of Arti's threat model.
94
    //
95
    // (In fact, there's an easier distinguisher, since we enforce UTF-8 in
96
    // these documents, and C tor does not.)
97

            
98
    rules.build()
99
});
100

            
101
/// Helper type returned when we parse an HsDescInner.
102
pub(crate) type UncheckedHsDescInner = TimerangeBound<SignatureGated<HsDescInner>>;
103

            
104
/// Information about one of the certificates inside an HsDescInner.
105
///
106
/// This is a teporary structure that we use when parsing.
107
struct InnerCertData {
108
    /// The identity of the key that purportedly signs this certificate.
109
    signing_key: Ed25519Identity,
110
    /// The key that is being signed.
111
    subject_key: ed25519::PublicKey,
112
    /// A detached signature object that we must validate before we can conclude
113
    /// that the certificate is valid.
114
    signature: Box<dyn ValidatableSignature>,
115
    /// The time when the certificate expires.
116
    expiry: SystemTime,
117
}
118

            
119
/// Decode a certificate from `tok`, and check that its tag and type are
120
/// expected, that it contains a signing key,  and that both signing and subject
121
/// keys are Ed25519.
122
///
123
/// On success, return an InnerCertData.
124
fn handle_inner_certificate(
125
    tok: &crate::parse::tokenize::Item<HsInnerKwd>,
126
    want_tag: &str,
127
    want_type: tor_cert::CertType,
128
) -> Result<InnerCertData> {
129
    let make_err = |e, msg| {
130
        EK::BadObjectVal
131
            .with_msg(msg)
132
            .with_source(e)
133
            .at_pos(tok.pos())
134
    };
135

            
136
    let cert = tok
137
        .parse_obj::<UnvalidatedEdCert>(want_tag)?
138
        .check_cert_type(want_type)?
139
        .into_unchecked();
140

            
141
    // These certs have to include a signing key.
142
    let cert = cert
143
        .should_have_signing_key()
144
        .map_err(|e| make_err(e, "Certificate was not self-signed"))?;
145

            
146
    // Peel off the signature.
147
    let (cert, signature) = cert
148
        .dangerously_split()
149
        .map_err(|e| make_err(e, "Certificate was not Ed25519-signed"))?;
150
    let signature = Box::new(signature);
151

            
152
    // Peel off the expiration
153
    let cert = cert.dangerously_assume_timely();
154
    let expiry = cert.expiry();
155
    let subject_key = cert
156
        .subject_key()
157
        .as_ed25519()
158
        .ok_or_else(|| {
159
            EK::BadObjectVal
160
                .with_msg("Certified key was not Ed25519")
161
                .at_pos(tok.pos())
162
        })?
163
        .try_into()
164
        .map_err(|_| {
165
            EK::BadObjectVal
166
                .with_msg("Certified key was not valid Ed25519")
167
                .at_pos(tok.pos())
168
        })?;
169

            
170
    let signing_key = *cert.signing_key().ok_or_else(|| {
171
        EK::BadObjectVal
172
            .with_msg("Signing key was not Ed25519")
173
            .at_pos(tok.pos())
174
    })?;
175

            
176
    Ok(InnerCertData {
177
        signing_key,
178
        subject_key,
179
        signature,
180
        expiry,
181
    })
182
}
183

            
184
impl HsDescInner {
185
    /// Attempt to parse the inner document of an onion service descriptor from a
186
    /// provided string.
187
    ///
188
    /// On success, return the signing key that was used for every certificate in the
189
    /// inner document, and the inner document itself.
190
    #[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
191
    pub(super) fn parse(s: &str) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
192
        let mut reader = NetDocReader::new(s);
193
        let result = Self::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
194
        Ok(result)
195
    }
196

            
197
    /// Attempt to parse the inner document of an onion service descriptor from a
198
    /// provided reader.
199
    ///
200
    /// On success, return the signing key that was used for every certificate in the
201
    /// inner document, and the inner document itself.
202
    fn take_from_reader(
203
        input: &mut NetDocReader<'_, HsInnerKwd>,
204
    ) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
205
        use HsInnerKwd::*;
206

            
207
        // Split up the input at INTRODUCTION_POINT items
208
        let mut sections =
209
            input.batching_split_before_with_header(|item| item.is_ok_with_kwd(INTRODUCTION_POINT));
210
        // Parse the header.
211
        let header = HS_INNER_HEADER_RULES.parse(&mut sections)?;
212

            
213
        // Make sure that the "ntor" handshake is supported in the list of
214
        // `HTYPE`s (handshake types) in `create2-formats`.
215
        {
216
            let tok = header.required(CREATE2_FORMATS)?;
217
            // If we ever want to support a different HTYPE, we'll need to
218
            // store at least the intersection between "their" and "our" supported
219
            // HTYPEs.  For now we only support one, so either this set is empty
220
            // and failing now is fine, or `ntor` (2) is supported, so fine.
221
            if !tok.args().any(|s| s == "2") {
222
                return Err(EK::BadArgument
223
                    .at_pos(tok.pos())
224
                    .with_msg("Onion service descriptor does not support ntor handshake."));
225
            }
226
        }
227
        // Check whether any kind of introduction-point authentication is
228
        // specified in an `intro-auth-required` line.
229
        let auth_types = if let Some(tok) = header.get(INTRO_AUTH_REQUIRED) {
230
            let mut auth_types: SmallVec<[IntroAuthType; 2]> = SmallVec::new();
231
            let mut push = |at| {
232
                if !auth_types.contains(&at) {
233
                    auth_types.push(at);
234
                }
235
            };
236
            for arg in tok.args() {
237
                #[allow(clippy::single_match)]
238
                match arg {
239
                    "ed25519" => push(IntroAuthType::Ed25519),
240
                    _ => (), // Ignore unrecognized types.
241
                }
242
            }
243
            // .. but if no types are recognized, we can't connect.
244
            if auth_types.is_empty() {
245
                return Err(EK::BadArgument
246
                    .at_pos(tok.pos())
247
                    .with_msg("No recognized introduction authentication methods."));
248
            }
249

            
250
            Some(auth_types)
251
        } else {
252
            None
253
        };
254

            
255
        // Recognize `single-onion-service` if it's there.
256
        let is_single_onion_service = header.get(SINGLE_ONION_SERVICE).is_some();
257

            
258
        let mut signatures = Vec::new();
259
        let mut expirations = Vec::new();
260
        let mut cert_signing_key: Option<Ed25519Identity> = None;
261

            
262
        // Now we parse the introduction points.  Each of these will be a
263
        // section starting with `introduction-point`, ending right before the
264
        // next `introduction-point` (or before the end of the document.)
265
        let mut intro_points = Vec::new();
266
        let mut sections = sections.subsequent();
267
        while let Some(mut ipt_section) = sections.next_batch() {
268
            let ipt_section = HS_INNER_INTRO_RULES.parse(&mut ipt_section)?;
269

            
270
            // Parse link-specifiers
271
            let link_specifiers = {
272
                let tok = ipt_section.required(INTRODUCTION_POINT)?;
273
                let ls = tok.parse_arg::<B64>(0)?;
274
                let mut r = tor_bytes::Reader::from_slice(ls.as_bytes());
275
                let n = r.take_u8()?;
276
                let res = r.extract_n(n.into())?;
277
                r.should_be_exhausted()?;
278
                res
279
            };
280

            
281
            // Parse the ntor "onion-key" (`KP_ntor`) of the introduction point.
282
            let ntor_onion_key = {
283
                let tok = ipt_section
284
                    .slice(ONION_KEY)
285
                    .iter()
286
                    .filter(|item| item.arg(0) == Some("ntor"))
287
                    .exactly_one()
288
                    .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
289
                tok.parse_arg::<B64>(1)?.into_array()?.into()
290
            };
291

            
292
            // Extract the auth_key (`KP_hs_ipt_sid`) from the (unchecked)
293
            // "auth-key" certificate.
294
            let auth_key: HsIntroPtSessionIdKey = {
295
                // Note that this certificate does not actually serve any
296
                // function _as_ a certificate; it was meant to cross-certify
297
                // the descriptor signing key (`KP_hs_desc_sign`) using the
298
                // authentication key (`KP_hs_ipt_sid`).  But the C tor
299
                // implementation got it backwards.
300
                //
301
                // We have to parse this certificate to extract
302
                // `KP_hs_ipt_sid`, but we don't actually need to validate it:
303
                // it appears inside the inner document, which is already signed
304
                // with `KP_hs_desc_sign`.  Nonetheless, we validate it anyway,
305
                // since that's what C tor does.
306
                //
307
                // See documentation for `CertType::HS_IP_V_SIGNING for more
308
                // info`.
309
                let tok = ipt_section.required(AUTH_KEY)?;
310
                let InnerCertData {
311
                    signing_key,
312
                    subject_key,
313
                    signature,
314
                    expiry,
315
                } = handle_inner_certificate(
316
                    tok,
317
                    "ED25519 CERT",
318
                    tor_cert::CertType::HS_IP_V_SIGNING,
319
                )?;
320
                expirations.push(expiry);
321
                signatures.push(signature);
322
                if cert_signing_key.get_or_insert(signing_key) != &signing_key {
323
                    return Err(EK::BadObjectVal
324
                        .at_pos(tok.pos())
325
                        .with_msg("Mismatched signing key"));
326
                }
327

            
328
                subject_key.into()
329
            };
330

            
331
            // Extract the key `KP_hss_ntor` that we'll use for our
332
            // handshake with the onion service itself.  This comes from the
333
            // "enc-key" item.
334
            let svc_ntor_key: HsSvcNtorKey = {
335
                let tok = ipt_section
336
                    .slice(ENC_KEY)
337
                    .iter()
338
                    .filter(|item| item.arg(0) == Some("ntor"))
339
                    .exactly_one()
340
                    .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
341
                let key = curve25519::PublicKey::from(tok.parse_arg::<B64>(1)?.into_array()?);
342
                key.into()
343
            };
344

            
345
            // Check that the key in the "enc-key-cert" item matches the
346
            // `KP_hss_ntor` we just extracted.
347
            {
348
                // NOTE: As above, this certificate is backwards, and hence
349
                // useless.  Still, we validate it because that is what C tor does.
350
                let tok = ipt_section.required(ENC_KEY_CERT)?;
351
                let InnerCertData {
352
                    signing_key,
353
                    subject_key,
354
                    signature,
355
                    expiry,
356
                } = handle_inner_certificate(
357
                    tok,
358
                    "ED25519 CERT",
359
                    tor_cert::CertType::HS_IP_CC_SIGNING,
360
                )?;
361
                expirations.push(expiry);
362
                signatures.push(signature);
363

            
364
                // Yes, the sign bit is always zero here. This would have a 50%
365
                // chance of making  the key unusable for verification. But since
366
                // the certificate is backwards (see above) we don't actually have
367
                // to check any signatures with it.
368
                let sign_bit = 0;
369
                let expected_ed_key =
370
                    tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public(
371
                        &svc_ntor_key,
372
                        sign_bit,
373
                    );
374
                if expected_ed_key != Some(subject_key) {
375
                    return Err(EK::BadObjectVal
376
                        .at_pos(tok.pos())
377
                        .with_msg("Mismatched subject key"));
378
                }
379

            
380
                // Make sure signing key is as expected.
381
                if cert_signing_key.get_or_insert(signing_key) != &signing_key {
382
                    return Err(EK::BadObjectVal
383
                        .at_pos(tok.pos())
384
                        .with_msg("Mismatched signing key"));
385
                }
386
            };
387

            
388
            // TODO SPEC: State who enforces NUM_INTRO_POINT_MAX and how (hsdirs, clients?)
389
            //
390
            // Simply discard extraneous IPTs.  The MAX value is hardcoded now, but a future
391
            // protocol evolution might increase it and we should probably still work then.
392
            //
393
            // If the spec intended that hsdirs ought to validate this and reject descriptors
394
            // with more than MAX (when they can), then this code is wrong because it would
395
            // prevent any caller (eg future hsdir code in arti relay) from seeing the violation.
396
            if intro_points.len() < NUM_INTRO_POINT_MAX {
397
                intro_points.push(IntroPointDesc {
398
                    link_specifiers,
399
                    ipt_ntor_key: ntor_onion_key,
400
                    ipt_sid_key: auth_key,
401
                    svc_ntor_key,
402
                });
403
            }
404
        }
405

            
406
        // TODO SPEC: Might a HS publish descriptor with no IPTs to declare itself down?
407
        // If it might, then we should:
408
        //   - accept such descriptors here
409
        //   - check for this situation explicitly in tor-hsclient connect.rs intro_rend_connect
410
        //   - bail with a new `ConnError` (with ErrorKind OnionServiceNotRunning)
411
        // with the consequence that once we obtain such a descriptor,
412
        // we'll be satisfied with it and consider the HS down until the descriptor expires.
413
        if intro_points.is_empty() {
414
            return Err(EK::MissingEntry.with_msg("no introduction points"));
415
        }
416

            
417
        let inner = HsDescInner {
418
            intro_auth_types: auth_types,
419
            single_onion_service: is_single_onion_service,
420
            intro_points,
421
        };
422
        let sig_gated = SignatureGated::new(inner, signatures);
423
        let time_bound = match expirations.iter().min() {
424
            Some(t) => TimerangeBound::new(sig_gated, ..t),
425
            None => TimerangeBound::new(sig_gated, ..),
426
        };
427

            
428
        Ok((cert_signing_key, time_bound))
429
    }
430
}
431

            
432
#[cfg(test)]
433
mod test {
434
    // @@ begin test lint list maintained by maint/add_warning @@
435
    #![allow(clippy::bool_assert_comparison)]
436
    #![allow(clippy::clone_on_copy)]
437
    #![allow(clippy::dbg_macro)]
438
    #![allow(clippy::print_stderr)]
439
    #![allow(clippy::print_stdout)]
440
    #![allow(clippy::single_char_pattern)]
441
    #![allow(clippy::unwrap_used)]
442
    #![allow(clippy::unchecked_duration_subtraction)]
443
    #![allow(clippy::useless_vec)]
444
    #![allow(clippy::needless_pass_by_value)]
445
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
446

            
447
    use std::iter;
448

            
449
    use hex_literal::hex;
450
    use itertools::chain;
451
    use tor_checkable::{SelfSigned, Timebound};
452

            
453
    use super::*;
454
    use crate::doc::hsdesc::{
455
        middle::HsDescMiddle,
456
        outer::HsDescOuter,
457
        test_data::{TEST_DATA, TEST_SUBCREDENTIAL},
458
    };
459

            
460
    // This is the inner document from hsdesc1.txt aka TEST_DATA
461
    const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner.txt");
462

            
463
    #[test]
464
    fn inner_text() {
465
        use crate::NetdocErrorKind as NEK;
466
        let _desc = HsDescInner::parse(TEST_DATA_INNER).unwrap();
467

            
468
        let none = format!(
469
            "{}\n",
470
            TEST_DATA_INNER
471
                .split_once("\nintroduction-point")
472
                .unwrap()
473
                .0,
474
        );
475
        let err = HsDescInner::parse(&none).map(|_| &none).unwrap_err();
476
        assert_eq!(err.kind, NEK::MissingEntry);
477

            
478
        let ipt = format!(
479
            "introduction-point{}",
480
            TEST_DATA_INNER
481
                .rsplit_once("\nintroduction-point")
482
                .unwrap()
483
                .1,
484
        );
485
        for n in NUM_INTRO_POINT_MAX..NUM_INTRO_POINT_MAX + 2 {
486
            let many = chain!(iter::once(&*none), iter::repeat(&*ipt).take(n),).collect::<String>();
487
            let desc = HsDescInner::parse(&many).unwrap();
488
            let desc = desc
489
                .1
490
                .dangerously_into_parts()
491
                .0
492
                .dangerously_assume_wellsigned();
493
            assert_eq!(desc.intro_points.len(), NUM_INTRO_POINT_MAX);
494
        }
495
    }
496

            
497
    #[test]
498
    fn parse_good() -> Result<()> {
499
        let desc = HsDescOuter::parse(TEST_DATA)?
500
            .dangerously_assume_wellsigned()
501
            .dangerously_assume_timely();
502
        let subcred = TEST_SUBCREDENTIAL.into();
503
        let body = desc.decrypt_body(&subcred).unwrap();
504
        let body = std::str::from_utf8(&body[..]).unwrap();
505

            
506
        let middle = HsDescMiddle::parse(body)?;
507
        let inner_body = middle
508
            .decrypt_inner(&desc.blinded_id(), desc.revision_counter(), &subcred, None)
509
            .unwrap();
510
        let inner_body = std::str::from_utf8(&inner_body).unwrap();
511
        let (ed_id, inner) = HsDescInner::parse(inner_body)?;
512
        let inner = inner
513
            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
514
            .unwrap()
515
            .check_signature()
516
            .unwrap();
517

            
518
        assert_eq!(ed_id.as_ref(), Some(desc.desc_sign_key_id()));
519

            
520
        assert!(inner.intro_auth_types.is_none());
521
        assert_eq!(inner.single_onion_service, false);
522
        assert_eq!(inner.intro_points.len(), 3);
523

            
524
        let ipt0 = &inner.intro_points[0];
525
        assert_eq!(
526
            ipt0.ipt_ntor_key().as_bytes(),
527
            &hex!("553BF9F9E1979D6F5D5D7D20BB3FE7272E32E22B6E86E35C76A7CA8A377E402F")
528
        );
529

            
530
        assert_ne!(ipt0.link_specifiers, inner.intro_points[1].link_specifiers);
531

            
532
        Ok(())
533
    }
534
}