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::doc::hsdesc::pow::PowParamSet;
8
use crate::parse::tokenize::{ItemResult, NetDocReader};
9
use crate::parse::{keyword::Keyword, parser::SectionRules};
10
use crate::types::misc::{B64, UnvalidatedEdCert};
11
use crate::{NetdocErrorKind as EK, Result};
12

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

            
24
/// The contents of the inner document of an onion service descriptor.
25
#[derive(Debug, Clone)]
26
pub 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
    /// A list of offered proof-of-work parameters, at most one per type.
44
    pub(super) pow_params: PowParamSet,
45
}
46

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

            
63
/// Rules about how keywords appear in the header part of an onion service
64
/// descriptor.
65
110
static HS_INNER_HEADER_RULES: LazyLock<SectionRules<HsInnerKwd>> = LazyLock::new(|| {
66
    use HsInnerKwd::*;
67

            
68
110
    let mut rules = SectionRules::builder();
69
110
    rules.add(CREATE2_FORMATS.rule().required().args(1..));
70
110
    rules.add(INTRO_AUTH_REQUIRED.rule().args(1..));
71
110
    rules.add(SINGLE_ONION_SERVICE.rule());
72
110
    rules.add(POW_PARAMS.rule().args(1..).may_repeat().obj_optional());
73
110
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
74

            
75
110
    rules.build()
76
110
});
77

            
78
/// Rules about how keywords appear in each introduction-point section of an
79
/// onion service descriptor.
80
110
static HS_INNER_INTRO_RULES: LazyLock<SectionRules<HsInnerKwd>> = LazyLock::new(|| {
81
    use HsInnerKwd::*;
82

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

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

            
102
110
    rules.build()
103
110
});
104

            
105
/// Helper type returned when we parse an HsDescInner.
106
pub(crate) type UncheckedHsDescInner = TimerangeBound<SignatureGated<HsDescInner>>;
107

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

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

            
140
4464
    let cert = tok
141
4464
        .parse_obj::<UnvalidatedEdCert>(want_tag)?
142
4464
        .check_cert_type(want_type)?
143
4464
        .into_unchecked();
144

            
145
    // These certs have to include a signing key.
146
4464
    let cert = cert
147
4464
        .should_have_signing_key()
148
4464
        .map_err(|e| make_err(e, "Certificate was not self-signed"))?;
149

            
150
    // Peel off the signature.
151
4464
    let (cert, signature) = cert
152
4464
        .dangerously_split()
153
4464
        .map_err(|e| make_err(e, "Certificate was not Ed25519-signed"))?;
154
4464
    let signature = Box::new(signature);
155

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

            
174
4464
    let signing_key = *cert.signing_key().ok_or_else(|| {
175
        EK::BadObjectVal
176
            .with_msg("Signing key was not Ed25519")
177
            .at_pos(tok.pos())
178
    })?;
179

            
180
4464
    Ok(InnerCertData {
181
4464
        signing_key,
182
4464
        subject_key,
183
4464
        signature,
184
4464
        expiry,
185
4464
    })
186
4464
}
187

            
188
impl HsDescInner {
189
    /// Attempt to parse the inner document of an onion service descriptor from a
190
    /// provided string.
191
    ///
192
    /// On success, return the signing key that was used for every certificate in the
193
    /// inner document, and the inner document itself.
194
734
    pub fn parse(s: &str) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
195
734
        let mut reader = NetDocReader::new(s)?;
196
738
        let result = Self::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
197
726
        Ok(result)
198
734
    }
199

            
200
    /// Attempt to parse the inner document of an onion service descriptor from a
201
    /// provided reader.
202
    ///
203
    /// On success, return the signing key that was used for every certificate in the
204
    /// inner document, and the inner document itself.
205
    //
206
    // TODO: replace Itertools::exactly_one() with a stdlib equivalent when there is one.
207
    //
208
    // See issue #48919 <https://github.com/rust-lang/rust/issues/48919>
209
    #[allow(unstable_name_collisions)]
210
734
    fn take_from_reader(
211
734
        input: &mut NetDocReader<'_, HsInnerKwd>,
212
734
    ) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
213
        use HsInnerKwd::*;
214

            
215
        // Split up the input at INTRODUCTION_POINT items
216
734
        let mut sections =
217
15367
            input.batching_split_before_with_header(|item| item.is_ok_with_kwd(INTRODUCTION_POINT));
218
        // Parse the header.
219
734
        let header = HS_INNER_HEADER_RULES.parse(&mut sections)?;
220

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

            
258
            Some(auth_types)
259
        } else {
260
732
            None
261
        };
262

            
263
        // Recognize `single-onion-service` if it's there.
264
732
        let is_single_onion_service = header.get(SINGLE_ONION_SERVICE).is_some();
265

            
266
        // Recognize `pow-params`, parsing each line and rejecting duplicate types
267
732
        let pow_params = PowParamSet::from_items(header.slice(POW_PARAMS))?;
268

            
269
728
        let mut signatures = Vec::new();
270
728
        let mut expirations = Vec::new();
271
728
        let mut cert_signing_key: Option<Ed25519Identity> = None;
272

            
273
        // Now we parse the introduction points.  Each of these will be a
274
        // section starting with `introduction-point`, ending right before the
275
        // next `introduction-point` (or before the end of the document.)
276
728
        let mut intro_points = Vec::new();
277
728
        let mut sections = sections.subsequent();
278
2960
        while let Some(mut ipt_section) = sections.next_batch() {
279
2232
            let ipt_section = HS_INNER_INTRO_RULES.parse(&mut ipt_section)?;
280

            
281
            // Parse link-specifiers
282
2232
            let link_specifiers = {
283
2232
                let tok = ipt_section.required(INTRODUCTION_POINT)?;
284
2232
                let ls = tok.parse_arg::<B64>(0)?;
285
2232
                let mut r = tor_bytes::Reader::from_slice(ls.as_bytes());
286
2232
                let n = r.take_u8()?;
287
2232
                let res = r.extract_n(n.into())?;
288
2232
                r.should_be_exhausted()?;
289
2232
                res
290
            };
291

            
292
            // Parse the ntor "onion-key" (`KP_ntor`) of the introduction point.
293
2232
            let ntor_onion_key = {
294
2232
                let tok = ipt_section
295
2232
                    .slice(ONION_KEY)
296
2232
                    .iter()
297
2334
                    .filter(|item| item.arg(0) == Some("ntor"))
298
2232
                    .exactly_one()
299
2232
                    .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
300
2232
                tok.parse_arg::<B64>(1)?.into_array()?.into()
301
            };
302

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

            
339
2232
                subject_key.into()
340
            };
341

            
342
            // Extract the key `KP_hss_ntor` that we'll use for our
343
            // handshake with the onion service itself.  This comes from the
344
            // "enc-key" item.
345
2232
            let svc_ntor_key: HsSvcNtorKey = {
346
2232
                let tok = ipt_section
347
2232
                    .slice(ENC_KEY)
348
2232
                    .iter()
349
2334
                    .filter(|item| item.arg(0) == Some("ntor"))
350
2232
                    .exactly_one()
351
2232
                    .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
352
2232
                let key = curve25519::PublicKey::from(tok.parse_arg::<B64>(1)?.into_array()?);
353
2232
                key.into()
354
            };
355

            
356
            // Check that the key in the "enc-key-cert" item matches the
357
            // `KP_hss_ntor` we just extracted.
358
            {
359
                // NOTE: As above, this certificate is backwards, and hence
360
                // useless.  Still, we validate it because that is what C tor does.
361
2232
                let tok = ipt_section.required(ENC_KEY_CERT)?;
362
                let InnerCertData {
363
2232
                    signing_key,
364
2232
                    subject_key,
365
2232
                    signature,
366
2232
                    expiry,
367
2232
                } = handle_inner_certificate(
368
2232
                    tok,
369
2232
                    "ED25519 CERT",
370
                    tor_cert::CertType::HS_IP_CC_SIGNING,
371
                )?;
372
2232
                expirations.push(expiry);
373
2232
                signatures.push(signature);
374

            
375
                // Yes, the sign bit is always zero here. This would have a 50%
376
                // chance of making  the key unusable for verification. But since
377
                // the certificate is backwards (see above) we don't actually have
378
                // to check any signatures with it.
379
2232
                let sign_bit = 0;
380
2232
                let expected_ed_key =
381
2232
                    tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public(
382
2232
                        &svc_ntor_key,
383
2232
                        sign_bit,
384
                    );
385
2232
                if expected_ed_key != Some(subject_key) {
386
                    return Err(EK::BadObjectVal
387
                        .at_pos(tok.pos())
388
                        .with_msg("Mismatched subject key"));
389
2232
                }
390

            
391
                // Make sure signing key is as expected.
392
2232
                if cert_signing_key.get_or_insert(signing_key) != &signing_key {
393
                    return Err(EK::BadObjectVal
394
                        .at_pos(tok.pos())
395
                        .with_msg("Mismatched signing key"));
396
2232
                }
397
            };
398

            
399
            // TODO SPEC: State who enforces NUM_INTRO_POINT_MAX and how (hsdirs, clients?)
400
            //
401
            // Simply discard extraneous IPTs.  The MAX value is hardcoded now, but a future
402
            // protocol evolution might increase it and we should probably still work then.
403
            //
404
            // If the spec intended that hsdirs ought to validate this and reject descriptors
405
            // with more than MAX (when they can), then this code is wrong because it would
406
            // prevent any caller (eg future hsdir code in arti relay) from seeing the violation.
407
2232
            if intro_points.len() < NUM_INTRO_POINT_MAX {
408
2230
                intro_points.push(IntroPointDesc {
409
2230
                    link_specifiers,
410
2230
                    ipt_ntor_key: ntor_onion_key,
411
2230
                    ipt_sid_key: auth_key,
412
2230
                    svc_ntor_key,
413
2230
                });
414
2230
            }
415
        }
416

            
417
        // TODO SPEC: Might a HS publish descriptor with no IPTs to declare itself down?
418
        // If it might, then we should:
419
        //   - accept such descriptors here
420
        //   - check for this situation explicitly in tor-hsclient connect.rs intro_rend_connect
421
        //   - bail with a new `ConnError` (with ErrorKind OnionServiceNotRunning)
422
        // with the consequence that once we obtain such a descriptor,
423
        // we'll be satisfied with it and consider the HS down until the descriptor expires.
424
728
        if intro_points.is_empty() {
425
2
            return Err(EK::MissingEntry.with_msg("no introduction points"));
426
726
        }
427

            
428
726
        let inner = HsDescInner {
429
726
            intro_auth_types: auth_types,
430
726
            single_onion_service: is_single_onion_service,
431
726
            pow_params,
432
726
            intro_points,
433
726
        };
434
726
        let sig_gated = SignatureGated::new(inner, signatures);
435
726
        let time_bound = match expirations.iter().min() {
436
726
            Some(t) => TimerangeBound::new(sig_gated, ..t),
437
            None => TimerangeBound::new(sig_gated, ..),
438
        };
439

            
440
726
        Ok((cert_signing_key, time_bound))
441
734
    }
442
}
443

            
444
#[cfg(test)]
445
mod test {
446
    // @@ begin test lint list maintained by maint/add_warning @@
447
    #![allow(clippy::bool_assert_comparison)]
448
    #![allow(clippy::clone_on_copy)]
449
    #![allow(clippy::dbg_macro)]
450
    #![allow(clippy::mixed_attributes_style)]
451
    #![allow(clippy::print_stderr)]
452
    #![allow(clippy::print_stdout)]
453
    #![allow(clippy::single_char_pattern)]
454
    #![allow(clippy::unwrap_used)]
455
    #![allow(clippy::unchecked_time_subtraction)]
456
    #![allow(clippy::useless_vec)]
457
    #![allow(clippy::needless_pass_by_value)]
458
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
459

            
460
    use std::{iter, time::Duration};
461

            
462
    use hex_literal::hex;
463
    use itertools::chain;
464
    use tor_checkable::{SelfSigned, Timebound};
465

            
466
    use super::*;
467
    use crate::doc::hsdesc::{
468
        middle::HsDescMiddle,
469
        outer::HsDescOuter,
470
        pow::PowParams,
471
        test_data::{TEST_DATA, TEST_SUBCREDENTIAL},
472
    };
473

            
474
    /// Test one particular canned 'inner' document, checking
475
    /// edge cases for zero intro points and too many intro points
476
    #[test]
477
    fn inner_text() {
478
        // This is the inner document from hsdesc1.txt aka TEST_DATA
479
        const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner.txt");
480

            
481
        use crate::NetdocErrorKind as NEK;
482
        let _desc = HsDescInner::parse(TEST_DATA_INNER).unwrap();
483

            
484
        let none = format!(
485
            "{}\n",
486
            TEST_DATA_INNER
487
                .split_once("\nintroduction-point")
488
                .unwrap()
489
                .0,
490
        );
491
        let err = HsDescInner::parse(&none).map(|_| &none).unwrap_err();
492
        assert_eq!(err.kind, NEK::MissingEntry);
493

            
494
        let ipt = format!(
495
            "introduction-point{}",
496
            TEST_DATA_INNER
497
                .rsplit_once("\nintroduction-point")
498
                .unwrap()
499
                .1,
500
        );
501
        for n in NUM_INTRO_POINT_MAX..NUM_INTRO_POINT_MAX + 2 {
502
            let many =
503
                chain!(iter::once(&*none), std::iter::repeat_n(&*ipt, n),).collect::<String>();
504
            let desc = HsDescInner::parse(&many).unwrap();
505
            let desc = desc
506
                .1
507
                .dangerously_into_parts()
508
                .0
509
                .dangerously_assume_wellsigned();
510
            assert_eq!(desc.intro_points.len(), NUM_INTRO_POINT_MAX);
511
        }
512
    }
513

            
514
    /// Test parseability of an inner document generated by C tor with PoW v1
515
    #[test]
516
    #[cfg(feature = "hs-pow-full")]
517
    fn inner_c_pow_v1() {
518
        const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
519
        let desc = HsDescInner::parse(TEST_DATA_INNER).unwrap();
520
        let pow_params = desc
521
            .1
522
            .dangerously_into_parts()
523
            .0
524
            .dangerously_assume_wellsigned()
525
            .pow_params;
526
        assert_eq!(pow_params.slice().len(), 1);
527
        match &pow_params.slice()[0] {
528
            PowParams::V1(v1) => {
529
                let expected_effort: tor_hscrypto::pow::v1::Effort = 614.into();
530
                let expected_seed: tor_hscrypto::pow::v1::Seed =
531
                    hex!("144e901df0841833a6e8592190849b4412f307d1565f2f137b2a5bc21a31092a").into();
532
                let expected_expiry = Some(SystemTime::UNIX_EPOCH + Duration::new(1712812537, 0));
533
                assert_eq!(v1.suggested_effort(), expected_effort);
534
                assert_eq!(
535
                    v1.seed().to_owned().dangerously_assume_timely(),
536
                    expected_seed
537
                );
538
                assert_eq!(v1.seed().bounds().1, expected_expiry);
539
            }
540
            #[allow(unreachable_patterns)]
541
            _ => unreachable!(),
542
        }
543
    }
544

            
545
    /// Ensure the same valid v1 pow document parses with the addition of unknown schemes
546
    #[test]
547
    fn inner_c_pow_v1_with_unknown() {
548
        const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
549
        let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
550
        let test_data_inner = format!("{}\npow-params x-example\npow-params{}", parts.0, parts.1);
551
        let desc = HsDescInner::parse(&test_data_inner).unwrap();
552
        let pow_params = desc
553
            .1
554
            .dangerously_into_parts()
555
            .0
556
            .dangerously_assume_wellsigned()
557
            .pow_params;
558
        assert_eq!(pow_params.slice().len(), 1);
559
    }
560

            
561
    /// Incorrect reduced document with a pow-params line that has no scheme parameter
562
    #[test]
563
    fn inner_pow_empty() {
564
        const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
565
        let err = HsDescInner::parse(TEST_DATA_INNER).map(|_| ()).unwrap_err();
566
        assert_eq!(err.kind, crate::NetdocErrorKind::TooFewArguments);
567
    }
568

            
569
    /// Incorrect document with duplicated pow-params lines of the same known type
570
    #[test]
571
    fn inner_pow_duplicate() {
572
        // Modify the canned v1 pow example from c tor, by duplicating the entire pow-params line
573
        const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
574
        let first_split = TEMPLATE.rsplit_once("\npow-params").unwrap();
575
        let second_split = first_split.1.split_once("\n").unwrap();
576
        let test_data_inner = format!(
577
            "{}\npow-params{}\npow-params{}\n{}",
578
            first_split.0, second_split.0, second_split.0, second_split.1
579
        );
580
        let err = HsDescInner::parse(&test_data_inner)
581
            .map(|_| ())
582
            .unwrap_err();
583
        assert_eq!(err.kind, crate::NetdocErrorKind::DuplicateToken);
584
    }
585

            
586
    /// Incorrect document with an unexpected object encoded after the pow v1 scheme's pow-params
587
    #[test]
588
    #[cfg(feature = "hs-pow-full")]
589
    fn inner_pow_v1_object() {
590
        // Modify the canned v1 pow example
591
        const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
592
        let first_split = TEMPLATE.rsplit_once("\npow-params").unwrap();
593
        let second_split = first_split.1.split_once("\n").unwrap();
594
        let test_data_inner = format!(
595
            "{}\npow-params{}\n-----BEGIN THING-----\n-----END THING-----\n{}",
596
            first_split.0, second_split.0, second_split.1
597
        );
598
        let err = HsDescInner::parse(&test_data_inner)
599
            .map(|_| ())
600
            .unwrap_err();
601
        assert_eq!(err.kind, crate::NetdocErrorKind::UnexpectedObject);
602
    }
603

            
604
    /// Document including an unrecognized pow-params line, ignored without error and not
605
    /// represented in the output at all.
606
    ///
607
    /// Also tests that unrecognized schemes are not subject to a restriction against
608
    /// duplicate appearances. (The spec allows that implementations do not need to
609
    /// implement this prohibition for arbitrary scheme strings)
610
    ///
611
    /// TODO: We may want PowParamSet to provide a representation for arbitrary unknown PoW
612
    ///       schemes, to the extent that this information may be useful for error reporting
613
    ///       purposes after an onion service rendezvous fails.
614
    #[test]
615
    fn inner_pow_unrecognized() {
616
        // Use the reduced document from inner_pow_empty() as a template
617
        const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
618
        let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
619
        let test_data_inner = format!(
620
            "{}\npow-params x-example\npow-params x-example{}",
621
            parts.0, parts.1
622
        );
623
        let desc = HsDescInner::parse(&test_data_inner).unwrap();
624
        let pow_params = desc
625
            .1
626
            .dangerously_into_parts()
627
            .0
628
            .dangerously_assume_wellsigned()
629
            .pow_params;
630
        assert_eq!(pow_params.slice().len(), 0);
631
    }
632

            
633
    /// Document with an unrecognized pow-params line including an object
634
    #[test]
635
    fn inner_pow_unrecognized_object() {
636
        // Use the reduced document from inner_pow_empty() as a template
637
        const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
638
        let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
639
        let test_data_inner = format!(
640
            "{}\npow-params x-something-else with args\n-----BEGIN THING-----\n-----END THING-----{}",
641
            parts.0, parts.1
642
        );
643
        let desc = HsDescInner::parse(&test_data_inner).unwrap();
644
        let pow_params = desc
645
            .1
646
            .dangerously_into_parts()
647
            .0
648
            .dangerously_assume_wellsigned()
649
            .pow_params;
650
        assert_eq!(pow_params.slice().len(), 0);
651
    }
652

            
653
    #[test]
654
    fn parse_good() -> Result<()> {
655
        let desc = HsDescOuter::parse(TEST_DATA)?
656
            .dangerously_assume_wellsigned()
657
            .dangerously_assume_timely();
658
        let subcred = TEST_SUBCREDENTIAL.into();
659
        let body = desc.decrypt_body(&subcred).unwrap();
660
        let body = std::str::from_utf8(&body[..]).unwrap();
661

            
662
        let middle = HsDescMiddle::parse(body)?;
663
        let inner_body = middle
664
            .decrypt_inner(&desc.blinded_id(), desc.revision_counter(), &subcred, None)
665
            .unwrap();
666
        let inner_body = std::str::from_utf8(&inner_body).unwrap();
667
        let (ed_id, inner) = HsDescInner::parse(inner_body)?;
668
        let inner = inner
669
            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
670
            .unwrap()
671
            .check_signature()
672
            .unwrap();
673

            
674
        assert_eq!(ed_id.as_ref(), Some(desc.desc_sign_key_id()));
675

            
676
        assert!(inner.intro_auth_types.is_none());
677
        assert_eq!(inner.single_onion_service, false);
678
        assert_eq!(inner.intro_points.len(), 3);
679

            
680
        let ipt0 = &inner.intro_points[0];
681
        assert_eq!(
682
            ipt0.ipt_ntor_key().as_bytes(),
683
            &hex!("553BF9F9E1979D6F5D5D7D20BB3FE7272E32E22B6E86E35C76A7CA8A377E402F")
684
        );
685

            
686
        assert_ne!(ipt0.link_specifiers, inner.intro_points[1].link_specifiers);
687

            
688
        Ok(())
689
    }
690
}