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

            
3
use std::num::NonZeroU8;
4
use std::time::SystemTime;
5

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

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

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

            
48
    /// A specified sendme increment and sub protocol capability list, if they were provided.
49
    ///
50
    /// Note that for historical reasons the protocol capabilities here are treated separately
51
    /// from those in `protos`.
52
    pub(super) flow_control: Option<(Protocols, NonZeroU8)>,
53

            
54
    /// A list of subprotocol capabilities advertised by the onion service.
55
    ///
56
    /// Note that for historical reasons.
57
    pub(super) protos: Protocols,
58
}
59

            
60
decl_keyword! {
61
    pub(crate) HsInnerKwd {
62
        "create2-formats" => CREATE2_FORMATS,
63
        "intro-auth-required" => INTRO_AUTH_REQUIRED,
64
        "single-onion-service" => SINGLE_ONION_SERVICE,
65
        "introduction-point" => INTRODUCTION_POINT,
66
        "onion-key" => ONION_KEY,
67
        "auth-key" => AUTH_KEY,
68
        "enc-key" => ENC_KEY,
69
        "enc-key-cert" => ENC_KEY_CERT,
70
        "legacy-key" => LEGACY_KEY,
71
        "legacy-key-cert" => LEGACY_KEY_CERT,
72
        "pow-params" => POW_PARAMS,
73
        "flow-control" => FLOW_CONTROL,
74
        "proto" => PROTO,
75
    }
76
}
77

            
78
/// Rules about how keywords appear in the header part of an onion service
79
/// descriptor.
80
108
static HS_INNER_HEADER_RULES: LazyLock<SectionRules<HsInnerKwd>> = LazyLock::new(|| {
81
    use HsInnerKwd::*;
82

            
83
108
    let mut rules = SectionRules::builder();
84
108
    rules.add(CREATE2_FORMATS.rule().required().args(1..));
85
108
    rules.add(INTRO_AUTH_REQUIRED.rule().args(1..));
86
108
    rules.add(SINGLE_ONION_SERVICE.rule());
87
108
    rules.add(POW_PARAMS.rule().args(1..).may_repeat().obj_optional());
88
108
    rules.add(PROTO.rule().args(0..));
89
108
    rules.add(FLOW_CONTROL.rule().args(2..));
90
108
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
91

            
92
108
    rules.build()
93
108
});
94

            
95
/// Rules about how keywords appear in each introduction-point section of an
96
/// onion service descriptor.
97
108
static HS_INNER_INTRO_RULES: LazyLock<SectionRules<HsInnerKwd>> = LazyLock::new(|| {
98
    use HsInnerKwd::*;
99

            
100
108
    let mut rules = SectionRules::builder();
101
108
    rules.add(INTRODUCTION_POINT.rule().required().args(1..));
102
    // Note: we're labeling ONION_KEY and ENC_KEY as "may_repeat", since even
103
    // though rend-spec labels them as "exactly once", they are allowed to
104
    // appear more than once so long as they appear only once _with an "ntor"_
105
    // key.  torspec!110 tries to document this issue.
106
108
    rules.add(ONION_KEY.rule().required().may_repeat().args(2..));
107
108
    rules.add(AUTH_KEY.rule().required().obj_required());
108
108
    rules.add(ENC_KEY.rule().required().may_repeat().args(2..));
109
108
    rules.add(ENC_KEY_CERT.rule().required().obj_required());
110
108
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
111

            
112
    // NOTE: We never look at the LEGACY_KEY* fields.  This does provide a
113
    // distinguisher for Arti implementations and C tor implementations, but
114
    // that's outside of Arti's threat model.
115
    //
116
    // (In fact, there's an easier distinguisher, since we enforce UTF-8 in
117
    // these documents, and C tor does not.)
118

            
119
108
    rules.build()
120
108
});
121

            
122
/// Helper type returned when we parse an HsDescInner.
123
pub(crate) type UncheckedHsDescInner = TimeRangeBound<SignatureGated<HsDescInner>>;
124

            
125
/// Information about one of the certificates inside an HsDescInner.
126
///
127
/// This is a teporary structure that we use when parsing.
128
struct InnerCertData {
129
    /// The identity of the key that purportedly signs this certificate.
130
    signing_key: Ed25519Identity,
131
    /// The key that is being signed.
132
    subject_key: ed25519::PublicKey,
133
    /// A detached signature object that we must validate before we can conclude
134
    /// that the certificate is valid.
135
    signature: Box<dyn ValidatableSignature>,
136
    /// The time when the certificate expires.
137
    expiry: SystemTime,
138
}
139

            
140
/// Decode a certificate from `tok`, and check that its tag and type are
141
/// expected, that it contains a signing key,  and that both signing and subject
142
/// keys are Ed25519.
143
///
144
/// On success, return an InnerCertData.
145
4386
fn handle_inner_certificate(
146
4386
    tok: &crate::parse::tokenize::Item<HsInnerKwd>,
147
4386
    want_tag: &str,
148
4386
    want_type: tor_cert::CertType,
149
4386
) -> Result<InnerCertData> {
150
4386
    let make_err = |e, msg| {
151
        EK::BadObjectVal
152
            .with_msg(msg)
153
            .with_source(e)
154
            .at_pos(tok.pos())
155
    };
156

            
157
4386
    let cert = tok
158
4386
        .parse_obj::<UnvalidatedEdCert>(want_tag)?
159
4386
        .check_cert_type(want_type)?
160
4386
        .into_unchecked();
161

            
162
    // These certs have to include a signing key.
163
4386
    let cert = cert
164
4386
        .should_have_signing_key()
165
4386
        .map_err(|e| make_err(e, "Certificate was not self-signed"))?;
166

            
167
    // Peel off the signature.
168
4386
    let (cert, signature) = cert
169
4386
        .dangerously_split()
170
4386
        .map_err(|e| make_err(e, "Certificate was not Ed25519-signed"))?;
171
4386
    let signature = Box::new(signature);
172

            
173
    // Peel off the expiration
174
4386
    let cert = cert.dangerously_assume_timely();
175
4386
    let expiry = cert.expiry();
176
4386
    let subject_key = cert
177
4386
        .subject_key()
178
4386
        .as_ed25519()
179
4386
        .ok_or_else(|| {
180
            EK::BadObjectVal
181
                .with_msg("Certified key was not Ed25519")
182
                .at_pos(tok.pos())
183
        })?
184
4386
        .try_into()
185
4386
        .map_err(|_| {
186
            EK::BadObjectVal
187
                .with_msg("Certified key was not valid Ed25519")
188
                .at_pos(tok.pos())
189
        })?;
190

            
191
4386
    let signing_key = *cert.signing_key().ok_or_else(|| {
192
        EK::BadObjectVal
193
            .with_msg("Signing key was not Ed25519")
194
            .at_pos(tok.pos())
195
    })?;
196

            
197
4386
    Ok(InnerCertData {
198
4386
        signing_key,
199
4386
        subject_key,
200
4386
        signature,
201
4386
        expiry,
202
4386
    })
203
4386
}
204

            
205
impl HsDescInner {
206
    /// Attempt to parse the inner document of an onion service descriptor from a
207
    /// provided string.
208
    ///
209
    /// On success, return the signing key that was used for every certificate in the
210
    /// inner document, and the inner document itself.
211
721
    pub fn parse(s: &str) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
212
721
        let mut reader = NetDocReader::new(s)?;
213
725
        let result = Self::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
214
713
        Ok(result)
215
721
    }
216

            
217
    /// Attempt to parse the inner document of an onion service descriptor from a
218
    /// provided reader.
219
    ///
220
    /// On success, return the signing key that was used for every certificate in the
221
    /// inner document, and the inner document itself.
222
    //
223
    // TODO: replace Itertools::exactly_one() with a stdlib equivalent when there is one.
224
    //
225
    // See issue #48919 <https://github.com/rust-lang/rust/issues/48919>
226
    #[allow(unstable_name_collisions)]
227
721
    fn take_from_reader(
228
721
        input: &mut NetDocReader<'_, HsInnerKwd>,
229
721
    ) -> Result<(Option<Ed25519Identity>, UncheckedHsDescInner)> {
230
        use HsInnerKwd::*;
231

            
232
        // Split up the input at INTRODUCTION_POINT items
233
721
        let mut sections =
234
15099
            input.batching_split_before_with_header(|item| item.is_ok_with_kwd(INTRODUCTION_POINT));
235
        // Parse the header.
236
721
        let header = HS_INNER_HEADER_RULES.parse(&mut sections)?;
237

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

            
275
            Some(auth_types)
276
        } else {
277
719
            None
278
        };
279

            
280
        // Recognize `single-onion-service` if it's there.
281
719
        let is_single_onion_service = header.get(SINGLE_ONION_SERVICE).is_some();
282

            
283
        // Recognize `pow-params`, parsing each line and rejecting duplicate types
284
719
        let pow_params = PowParamSet::from_items(header.slice(POW_PARAMS))?;
285

            
286
715
        let protos = if let Some(tok) = header.get(PROTO) {
287
            tok.args_as_str()
288
                .parse()
289
                .map_err(|e| EK::BadArgument.at_pos(tok.pos()).with_source(e))?
290
        } else {
291
715
            Protocols::new()
292
        };
293
715
        let flow_control = if let Some(tok) = header.get(FLOW_CONTROL) {
294
442
            let inc: NonZeroU8 = tok.required_arg(1)?.parse()?;
295
442
            let proto_range = tok.required_arg(0)?;
296
442
            let fc_p =
297
442
                Protocols::from_kind_and_versions(tor_protover::ProtoKind::FlowCtrl, proto_range)
298
442
                    .map_err(|e| EK::BadArgument.at_pos(tok.arg_pos(0)).with_source(e))?;
299
442
            Some((fc_p, inc))
300
        } else {
301
273
            None
302
        };
303

            
304
715
        let mut signatures = Vec::new();
305
715
        let mut expirations = Vec::new();
306
715
        let mut cert_signing_key: Option<Ed25519Identity> = None;
307

            
308
        // Now we parse the introduction points.  Each of these will be a
309
        // section starting with `introduction-point`, ending right before the
310
        // next `introduction-point` (or before the end of the document.)
311
715
        let mut intro_points = Vec::new();
312
715
        let mut sections = sections.subsequent();
313
2908
        while let Some(mut ipt_section) = sections.next_batch() {
314
2193
            let ipt_section = HS_INNER_INTRO_RULES.parse(&mut ipt_section)?;
315

            
316
            // Parse link-specifiers
317
2193
            let link_specifiers = {
318
2193
                let tok = ipt_section.required(INTRODUCTION_POINT)?;
319
2193
                let ls = tok.parse_arg::<B64>(0)?;
320
2193
                let mut r = tor_bytes::Reader::from_slice(ls.as_bytes());
321
2193
                let n = r.take_u8()?;
322
2193
                let res = r.extract_n(n.into())?;
323
2193
                r.should_be_exhausted()?;
324
2193
                res
325
            };
326

            
327
            // Parse the ntor "onion-key" (`KP_ntor`) of the introduction point.
328
2193
            let ntor_onion_key = {
329
2193
                let tok = ipt_section
330
2193
                    .slice(ONION_KEY)
331
2193
                    .iter()
332
2295
                    .filter(|item| item.arg(0) == Some("ntor"))
333
2193
                    .exactly_one()
334
2193
                    .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
335
2193
                tok.parse_arg::<B64>(1)?.into_array()?.into()
336
            };
337

            
338
            // Extract the auth_key (`KP_hs_ipt_sid`) from the (unchecked)
339
            // "auth-key" certificate.
340
2193
            let auth_key: HsIntroPtSessionIdKey = {
341
                // Note that this certificate does not actually serve any
342
                // function _as_ a certificate; it was meant to cross-certify
343
                // the descriptor signing key (`KP_hs_desc_sign`) using the
344
                // authentication key (`KP_hs_ipt_sid`).  But the C tor
345
                // implementation got it backwards.
346
                //
347
                // We have to parse this certificate to extract
348
                // `KP_hs_ipt_sid`, but we don't actually need to validate it:
349
                // it appears inside the inner document, which is already signed
350
                // with `KP_hs_desc_sign`.  Nonetheless, we validate it anyway,
351
                // since that's what C tor does.
352
                //
353
                // See documentation for `CertType::HS_IP_V_SIGNING for more
354
                // info`.
355
2193
                let tok = ipt_section.required(AUTH_KEY)?;
356
                let InnerCertData {
357
2193
                    signing_key,
358
2193
                    subject_key,
359
2193
                    signature,
360
2193
                    expiry,
361
2193
                } = handle_inner_certificate(
362
2193
                    tok,
363
2193
                    "ED25519 CERT",
364
                    tor_cert::CertType::HS_IP_V_SIGNING,
365
                )?;
366
2193
                expirations.push(expiry);
367
2193
                signatures.push(signature);
368
2193
                if cert_signing_key.get_or_insert(signing_key) != &signing_key {
369
                    return Err(EK::BadObjectVal
370
                        .at_pos(tok.pos())
371
                        .with_msg("Mismatched signing key"));
372
2193
                }
373

            
374
2193
                subject_key.into()
375
            };
376

            
377
            // Extract the key `KP_hss_ntor` that we'll use for our
378
            // handshake with the onion service itself.  This comes from the
379
            // "enc-key" item.
380
2193
            let svc_ntor_key: HsSvcNtorKey = {
381
2193
                let tok = ipt_section
382
2193
                    .slice(ENC_KEY)
383
2193
                    .iter()
384
2295
                    .filter(|item| item.arg(0) == Some("ntor"))
385
2193
                    .exactly_one()
386
2193
                    .map_err(|_| EK::MissingToken.with_msg("No unique ntor onion key found."))?;
387
2193
                let key = curve25519::PublicKey::from(tok.parse_arg::<B64>(1)?.into_array()?);
388
2193
                key.into()
389
            };
390

            
391
            // Check that the key in the "enc-key-cert" item matches the
392
            // `KP_hss_ntor` we just extracted.
393
            {
394
                // NOTE: As above, this certificate is backwards, and hence
395
                // useless.  Still, we validate it because that is what C tor does.
396
2193
                let tok = ipt_section.required(ENC_KEY_CERT)?;
397
                let InnerCertData {
398
2193
                    signing_key,
399
2193
                    subject_key,
400
2193
                    signature,
401
2193
                    expiry,
402
2193
                } = handle_inner_certificate(
403
2193
                    tok,
404
2193
                    "ED25519 CERT",
405
                    tor_cert::CertType::HS_IP_CC_SIGNING,
406
                )?;
407
2193
                expirations.push(expiry);
408
2193
                signatures.push(signature);
409

            
410
                // Yes, the sign bit is always zero here. This would have a 50%
411
                // chance of making  the key unusable for verification. But since
412
                // the certificate is backwards (see above) we don't actually have
413
                // to check any signatures with it.
414
2193
                let sign_bit = 0;
415
2193
                let expected_ed_key =
416
2193
                    tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public(
417
2193
                        &svc_ntor_key,
418
2193
                        sign_bit,
419
                    );
420
2193
                if expected_ed_key != Some(subject_key) {
421
                    return Err(EK::BadObjectVal
422
                        .at_pos(tok.pos())
423
                        .with_msg("Mismatched subject key"));
424
2193
                }
425

            
426
                // Make sure signing key is as expected.
427
2193
                if cert_signing_key.get_or_insert(signing_key) != &signing_key {
428
                    return Err(EK::BadObjectVal
429
                        .at_pos(tok.pos())
430
                        .with_msg("Mismatched signing key"));
431
2193
                }
432
            };
433

            
434
            // TODO SPEC: State who enforces NUM_INTRO_POINT_MAX and how (hsdirs, clients?)
435
            //
436
            // Simply discard extraneous IPTs.  The MAX value is hardcoded now, but a future
437
            // protocol evolution might increase it and we should probably still work then.
438
            //
439
            // If the spec intended that hsdirs ought to validate this and reject descriptors
440
            // with more than MAX (when they can), then this code is wrong because it would
441
            // prevent any caller (eg future hsdir code in arti relay) from seeing the violation.
442
2193
            if intro_points.len() < NUM_INTRO_POINT_MAX {
443
2191
                intro_points.push(IntroPointDesc {
444
2191
                    link_specifiers,
445
2191
                    ipt_ntor_key: ntor_onion_key,
446
2191
                    ipt_sid_key: auth_key,
447
2191
                    svc_ntor_key,
448
2191
                });
449
2191
            }
450
        }
451

            
452
        // TODO SPEC: Might a HS publish descriptor with no IPTs to declare itself down?
453
        // If it might, then we should:
454
        //   - accept such descriptors here
455
        //   - check for this situation explicitly in tor-hsclient connect.rs intro_rend_connect
456
        //   - bail with a new `ConnError` (with ErrorKind OnionServiceNotRunning)
457
        // with the consequence that once we obtain such a descriptor,
458
        // we'll be satisfied with it and consider the HS down until the descriptor expires.
459
715
        if intro_points.is_empty() {
460
2
            return Err(EK::MissingEntry.with_msg("no introduction points"));
461
713
        }
462

            
463
713
        let inner = HsDescInner {
464
713
            intro_auth_types: auth_types,
465
713
            single_onion_service: is_single_onion_service,
466
713
            pow_params,
467
713
            intro_points,
468
713
            flow_control,
469
713
            protos,
470
713
        };
471
713
        let sig_gated = SignatureGated::new(inner, signatures);
472
713
        let time_bound = match expirations.iter().min() {
473
713
            Some(t) => TimeRangeBound::new(sig_gated, ..t),
474
            None => TimeRangeBound::new(sig_gated, ..),
475
        };
476

            
477
713
        Ok((cert_signing_key, time_bound))
478
721
    }
479
}
480

            
481
#[cfg(test)]
482
mod test {
483
    // @@ begin test lint list maintained by maint/add_warning @@
484
    #![allow(clippy::bool_assert_comparison)]
485
    #![allow(clippy::clone_on_copy)]
486
    #![allow(clippy::dbg_macro)]
487
    #![allow(clippy::mixed_attributes_style)]
488
    #![allow(clippy::print_stderr)]
489
    #![allow(clippy::print_stdout)]
490
    #![allow(clippy::single_char_pattern)]
491
    #![allow(clippy::unwrap_used)]
492
    #![allow(clippy::unchecked_time_subtraction)]
493
    #![allow(clippy::useless_vec)]
494
    #![allow(clippy::needless_pass_by_value)]
495
    #![allow(clippy::string_slice)] // See arti#2571
496
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
497

            
498
    use std::{iter, time::Duration};
499

            
500
    use hex_literal::hex;
501
    use itertools::chain;
502
    use tor_checkable::{SelfSigned, TimeBound};
503

            
504
    use super::*;
505
    use crate::doc::hsdesc::{
506
        middle::HsDescMiddle,
507
        outer::HsDescOuter,
508
        pow::PowParams,
509
        test_data::{TEST_DATA, TEST_SUBCREDENTIAL},
510
    };
511

            
512
    /// Test one particular canned 'inner' document, checking
513
    /// edge cases for zero intro points and too many intro points
514
    #[test]
515
    fn inner_text() {
516
        // This is the inner document from hsdesc1.txt aka TEST_DATA
517
        const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner.txt");
518

            
519
        use crate::NetdocErrorKind as NEK;
520
        let _desc = HsDescInner::parse(TEST_DATA_INNER).unwrap();
521

            
522
        let none = format!(
523
            "{}\n",
524
            TEST_DATA_INNER
525
                .split_once("\nintroduction-point")
526
                .unwrap()
527
                .0,
528
        );
529
        let err = HsDescInner::parse(&none).map(|_| &none).unwrap_err();
530
        assert_eq!(err.kind, NEK::MissingEntry);
531

            
532
        let ipt = format!(
533
            "introduction-point{}",
534
            TEST_DATA_INNER
535
                .rsplit_once("\nintroduction-point")
536
                .unwrap()
537
                .1,
538
        );
539
        for n in NUM_INTRO_POINT_MAX..NUM_INTRO_POINT_MAX + 2 {
540
            let many =
541
                chain!(iter::once(&*none), std::iter::repeat_n(&*ipt, n),).collect::<String>();
542
            let desc = HsDescInner::parse(&many).unwrap();
543
            let desc = desc
544
                .1
545
                .dangerously_into_parts()
546
                .0
547
                .dangerously_assume_wellsigned();
548
            assert_eq!(desc.intro_points.len(), NUM_INTRO_POINT_MAX);
549
        }
550
    }
551

            
552
    /// Test parseability of an inner document generated by C tor with PoW v1
553
    #[test]
554
    #[cfg(feature = "hs-pow-full")]
555
    fn inner_c_pow_v1() {
556
        const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
557
        let desc = HsDescInner::parse(TEST_DATA_INNER).unwrap();
558
        let pow_params = desc
559
            .1
560
            .dangerously_into_parts()
561
            .0
562
            .dangerously_assume_wellsigned()
563
            .pow_params;
564
        assert_eq!(pow_params.slice().len(), 1);
565
        match &pow_params.slice()[0] {
566
            PowParams::V1(v1) => {
567
                let expected_effort: tor_hscrypto::pow::v1::Effort = 614.into();
568
                let expected_seed: tor_hscrypto::pow::v1::Seed =
569
                    hex!("144e901df0841833a6e8592190849b4412f307d1565f2f137b2a5bc21a31092a").into();
570
                let expected_expiry = Some(SystemTime::UNIX_EPOCH + Duration::new(1712812537, 0));
571
                assert_eq!(v1.suggested_effort(), expected_effort);
572
                assert_eq!(
573
                    v1.seed().to_owned().dangerously_assume_timely(),
574
                    expected_seed
575
                );
576
                assert_eq!(v1.seed().bounds_start_end().1, expected_expiry);
577
            }
578
            #[allow(unreachable_patterns)]
579
            _ => unreachable!(),
580
        }
581
    }
582

            
583
    /// Ensure the same valid v1 pow document parses with the addition of unknown schemes
584
    #[test]
585
    fn inner_c_pow_v1_with_unknown() {
586
        const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
587
        let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
588
        let test_data_inner = format!("{}\npow-params x-example\npow-params{}", parts.0, parts.1);
589
        let desc = HsDescInner::parse(&test_data_inner).unwrap();
590
        let pow_params = desc
591
            .1
592
            .dangerously_into_parts()
593
            .0
594
            .dangerously_assume_wellsigned()
595
            .pow_params;
596
        assert_eq!(pow_params.slice().len(), 1);
597
    }
598

            
599
    /// Incorrect reduced document with a pow-params line that has no scheme parameter
600
    #[test]
601
    fn inner_pow_empty() {
602
        const TEST_DATA_INNER: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
603
        let err = HsDescInner::parse(TEST_DATA_INNER).map(|_| ()).unwrap_err();
604
        assert_eq!(err.kind, crate::NetdocErrorKind::TooFewArguments);
605
    }
606

            
607
    /// Incorrect document with duplicated pow-params lines of the same known type
608
    #[test]
609
    fn inner_pow_duplicate() {
610
        // Modify the canned v1 pow example from c tor, by duplicating the entire pow-params line
611
        const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
612
        let first_split = TEMPLATE.rsplit_once("\npow-params").unwrap();
613
        let second_split = first_split.1.split_once("\n").unwrap();
614
        let test_data_inner = format!(
615
            "{}\npow-params{}\npow-params{}\n{}",
616
            first_split.0, second_split.0, second_split.0, second_split.1
617
        );
618
        let err = HsDescInner::parse(&test_data_inner)
619
            .map(|_| ())
620
            .unwrap_err();
621
        assert_eq!(err.kind, crate::NetdocErrorKind::DuplicateToken);
622
    }
623

            
624
    /// Incorrect document with an unexpected object encoded after the pow v1 scheme's pow-params
625
    #[test]
626
    #[cfg(feature = "hs-pow-full")]
627
    fn inner_pow_v1_object() {
628
        // Modify the canned v1 pow example
629
        const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-v1.txt");
630
        let first_split = TEMPLATE.rsplit_once("\npow-params").unwrap();
631
        let second_split = first_split.1.split_once("\n").unwrap();
632
        let test_data_inner = format!(
633
            "{}\npow-params{}\n-----BEGIN THING-----\n-----END THING-----\n{}",
634
            first_split.0, second_split.0, second_split.1
635
        );
636
        let err = HsDescInner::parse(&test_data_inner)
637
            .map(|_| ())
638
            .unwrap_err();
639
        assert_eq!(err.kind, crate::NetdocErrorKind::UnexpectedObject);
640
    }
641

            
642
    /// Document including an unrecognized pow-params line, ignored without error and not
643
    /// represented in the output at all.
644
    ///
645
    /// Also tests that unrecognized schemes are not subject to a restriction against
646
    /// duplicate appearances. (The spec allows that implementations do not need to
647
    /// implement this prohibition for arbitrary scheme strings)
648
    ///
649
    /// TODO: We may want PowParamSet to provide a representation for arbitrary unknown PoW
650
    ///       schemes, to the extent that this information may be useful for error reporting
651
    ///       purposes after an onion service rendezvous fails.
652
    #[test]
653
    fn inner_pow_unrecognized() {
654
        // Use the reduced document from inner_pow_empty() as a template
655
        const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
656
        let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
657
        let test_data_inner = format!(
658
            "{}\npow-params x-example\npow-params x-example{}",
659
            parts.0, parts.1
660
        );
661
        let desc = HsDescInner::parse(&test_data_inner).unwrap();
662
        let pow_params = desc
663
            .1
664
            .dangerously_into_parts()
665
            .0
666
            .dangerously_assume_wellsigned()
667
            .pow_params;
668
        assert_eq!(pow_params.slice().len(), 0);
669
    }
670

            
671
    /// Document with an unrecognized pow-params line including an object
672
    #[test]
673
    fn inner_pow_unrecognized_object() {
674
        // Use the reduced document from inner_pow_empty() as a template
675
        const TEMPLATE: &str = include_str!("../../../testdata/hsdesc-inner-pow-empty.txt");
676
        let parts = TEMPLATE.rsplit_once("\npow-params").unwrap();
677
        let test_data_inner = format!(
678
            "{}\npow-params x-something-else with args\n-----BEGIN THING-----\n-----END THING-----{}",
679
            parts.0, parts.1
680
        );
681
        let desc = HsDescInner::parse(&test_data_inner).unwrap();
682
        let pow_params = desc
683
            .1
684
            .dangerously_into_parts()
685
            .0
686
            .dangerously_assume_wellsigned()
687
            .pow_params;
688
        assert_eq!(pow_params.slice().len(), 0);
689
    }
690

            
691
    #[test]
692
    fn parse_good() -> Result<()> {
693
        let desc = HsDescOuter::parse(TEST_DATA)?
694
            .dangerously_assume_wellsigned()
695
            .dangerously_assume_timely();
696
        let subcred = TEST_SUBCREDENTIAL.into();
697
        let body = desc.decrypt_body(&subcred).unwrap();
698
        let body = std::str::from_utf8(&body[..]).unwrap();
699

            
700
        let middle = HsDescMiddle::parse(body)?;
701
        let inner_body = middle
702
            .decrypt_inner(&desc.blinded_id(), desc.revision_counter(), &subcred, None)
703
            .unwrap();
704
        let inner_body = std::str::from_utf8(&inner_body).unwrap();
705
        let (ed_id, inner) = HsDescInner::parse(inner_body)?;
706
        let inner = inner
707
            .if_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
708
            .unwrap()
709
            .check_signature()
710
            .unwrap();
711

            
712
        assert_eq!(ed_id.as_ref(), Some(desc.desc_sign_key_id()));
713

            
714
        assert!(inner.intro_auth_types.is_none());
715
        assert_eq!(inner.single_onion_service, false);
716
        assert_eq!(inner.intro_points.len(), 3);
717

            
718
        let ipt0 = &inner.intro_points[0];
719
        assert_eq!(
720
            ipt0.ipt_ntor_key().as_bytes(),
721
            &hex!("553BF9F9E1979D6F5D5D7D20BB3FE7272E32E22B6E86E35C76A7CA8A377E402F")
722
        );
723

            
724
        assert_ne!(ipt0.link_specifiers, inner.intro_points[1].link_specifiers);
725

            
726
        Ok(())
727
    }
728
}