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
106
static HS_INNER_HEADER_RULES: LazyLock<SectionRules<HsInnerKwd>> = LazyLock::new(|| {
66
    use HsInnerKwd::*;
67

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

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

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

            
83
106
    let mut rules = SectionRules::builder();
84
106
    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
106
    rules.add(ONION_KEY.rule().required().may_repeat().args(2..));
90
106
    rules.add(AUTH_KEY.rule().required().obj_required());
91
106
    rules.add(ENC_KEY.rule().required().may_repeat().args(2..));
92
106
    rules.add(ENC_KEY_CERT.rule().required().obj_required());
93
106
    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
106
    rules.build()
103
106
});
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
4308
fn handle_inner_certificate(
129
4308
    tok: &crate::parse::tokenize::Item<HsInnerKwd>,
130
4308
    want_tag: &str,
131
4308
    want_type: tor_cert::CertType,
132
4308
) -> Result<InnerCertData> {
133
4308
    let make_err = |e, msg| {
134
        EK::BadObjectVal
135
            .with_msg(msg)
136
            .with_source(e)
137
            .at_pos(tok.pos())
138
    };
139

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

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

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

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

            
174
4308
    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
4308
    Ok(InnerCertData {
181
4308
        signing_key,
182
4308
        subject_key,
183
4308
        signature,
184
4308
        expiry,
185
4308
    })
186
4308
}
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
708
    pub fn parse(s: &str) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
195
708
        let mut reader = NetDocReader::new(s)?;
196
712
        let result = Self::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
197
700
        Ok(result)
198
708
    }
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
708
    fn take_from_reader(
211
708
        input: &mut NetDocReader<'_, HsInnerKwd>,
212
708
    ) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
213
        use HsInnerKwd::*;
214

            
215
        // Split up the input at INTRODUCTION_POINT items
216
708
        let mut sections =
217
14831
            input.batching_split_before_with_header(|item| item.is_ok_with_kwd(INTRODUCTION_POINT));
218
        // Parse the header.
219
708
        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
706
            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
738
            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
706
            }
234
        }
235
        // Check whether any kind of introduction-point authentication is
236
        // specified in an `intro-auth-required` line.
237
706
        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
706
            None
261
        };
262

            
263
        // Recognize `single-onion-service` if it's there.
264
706
        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
706
        let pow_params = PowParamSet::from_items(header.slice(POW_PARAMS))?;
268

            
269
702
        let mut signatures = Vec::new();
270
702
        let mut expirations = Vec::new();
271
702
        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
702
        let mut intro_points = Vec::new();
277
702
        let mut sections = sections.subsequent();
278
2856
        while let Some(mut ipt_section) = sections.next_batch() {
279
2154
            let ipt_section = HS_INNER_INTRO_RULES.parse(&mut ipt_section)?;
280

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

            
292
            // Parse the ntor "onion-key" (`KP_ntor`) of the introduction point.
293
2154
            let ntor_onion_key = {
294
2154
                let tok = ipt_section
295
2154
                    .slice(ONION_KEY)
296
2154
                    .iter()
297
2256
                    .filter(|item| item.arg(0) == Some("ntor"))
298
2154
                    .exactly_one()
299
2154
                    .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
300
2154
                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
2154
            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
2154
                let tok = ipt_section.required(AUTH_KEY)?;
321
                let InnerCertData {
322
2154
                    signing_key,
323
2154
                    subject_key,
324
2154
                    signature,
325
2154
                    expiry,
326
2154
                } = handle_inner_certificate(
327
2154
                    tok,
328
2154
                    "ED25519 CERT",
329
                    tor_cert::CertType::HS_IP_V_SIGNING,
330
                )?;
331
2154
                expirations.push(expiry);
332
2154
                signatures.push(signature);
333
2154
                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
2154
                }
338

            
339
2154
                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
2154
            let svc_ntor_key: HsSvcNtorKey = {
346
2154
                let tok = ipt_section
347
2154
                    .slice(ENC_KEY)
348
2154
                    .iter()
349
2256
                    .filter(|item| item.arg(0) == Some("ntor"))
350
2154
                    .exactly_one()
351
2154
                    .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
352
2154
                let key = curve25519::PublicKey::from(tok.parse_arg::<B64>(1)?.into_array()?);
353
2154
                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
2154
                let tok = ipt_section.required(ENC_KEY_CERT)?;
362
                let InnerCertData {
363
2154
                    signing_key,
364
2154
                    subject_key,
365
2154
                    signature,
366
2154
                    expiry,
367
2154
                } = handle_inner_certificate(
368
2154
                    tok,
369
2154
                    "ED25519 CERT",
370
                    tor_cert::CertType::HS_IP_CC_SIGNING,
371
                )?;
372
2154
                expirations.push(expiry);
373
2154
                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
2154
                let sign_bit = 0;
380
2154
                let expected_ed_key =
381
2154
                    tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public(
382
2154
                        &svc_ntor_key,
383
2154
                        sign_bit,
384
                    );
385
2154
                if expected_ed_key != Some(subject_key) {
386
                    return Err(EK::BadObjectVal
387
                        .at_pos(tok.pos())
388
                        .with_msg("Mismatched subject key"));
389
2154
                }
390

            
391
                // Make sure signing key is as expected.
392
2154
                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
2154
                }
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
2154
            if intro_points.len() < NUM_INTRO_POINT_MAX {
408
2152
                intro_points.push(IntroPointDesc {
409
2152
                    link_specifiers,
410
2152
                    ipt_ntor_key: ntor_onion_key,
411
2152
                    ipt_sid_key: auth_key,
412
2152
                    svc_ntor_key,
413
2152
                });
414
2152
            }
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
702
        if intro_points.is_empty() {
425
2
            return Err(EK::MissingEntry.with_msg("no introduction points"));
426
700
        }
427

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

            
440
700
        Ok((cert_signing_key, time_bound))
441
708
    }
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
    #![allow(clippy::string_slice)] // See arti#2571
459
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
460

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
689
        Ok(())
690
    }
691
}