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::{UnvalidatedEdCert, B64};
11
use crate::{NetdocErrorKind as EK, Result};
12

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

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

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

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

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

            
76
84
    rules.build()
77
84
});
78

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

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

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

            
103
84
    rules.build()
104
84
});
105

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

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

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

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

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

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

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

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

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

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

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

            
212
        // Split up the input at INTRODUCTION_POINT items
213
319
        let mut sections =
214
6711
            input.batching_split_before_with_header(|item| item.is_ok_with_kwd(INTRODUCTION_POINT));
215
        // Parse the header.
216
319
        let header = HS_INNER_HEADER_RULES.parse(&mut sections)?;
217

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

            
255
            Some(auth_types)
256
        } else {
257
317
            None
258
        };
259

            
260
        // Recognize `single-onion-service` if it's there.
261
317
        let is_single_onion_service = header.get(SINGLE_ONION_SERVICE).is_some();
262

            
263
        // Recognize `pow-params`, parsing each line and rejecting duplicate types
264
317
        let pow_params = PowParamSet::from_items(header.slice(POW_PARAMS))?;
265

            
266
313
        let mut signatures = Vec::new();
267
313
        let mut expirations = Vec::new();
268
313
        let mut cert_signing_key: Option<Ed25519Identity> = None;
269
313

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

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

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

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

            
336
987
                subject_key.into()
337
            };
338

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

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

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

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

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

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

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

            
437
311
        Ok((cert_signing_key, time_bound))
438
319
    }
439
}
440

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

            
457
    use std::{iter, time::Duration};
458

            
459
    use hex_literal::hex;
460
    use itertools::chain;
461
    use tor_checkable::{SelfSigned, Timebound};
462

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

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

            
478
        use crate::NetdocErrorKind as NEK;
479
        let _desc = HsDescInner::parse(TEST_DATA_INNER).unwrap();
480

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

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

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

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

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

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

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

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

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

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

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

            
670
        assert_eq!(ed_id.as_ref(), Some(desc.desc_sign_key_id()));
671

            
672
        assert!(inner.intro_auth_types.is_none());
673
        assert_eq!(inner.single_onion_service, false);
674
        assert_eq!(inner.intro_points.len(), 3);
675

            
676
        let ipt0 = &inner.intro_points[0];
677
        assert_eq!(
678
            ipt0.ipt_ntor_key().as_bytes(),
679
            &hex!("553BF9F9E1979D6F5D5D7D20BB3FE7272E32E22B6E86E35C76A7CA8A377E402F")
680
        );
681

            
682
        assert_ne!(ipt0.link_specifiers, inner.intro_points[1].link_specifiers);
683

            
684
        Ok(())
685
    }
686
}