1
//! Implement parsing for the outer document of an onion service descriptor.
2

            
3
use itertools::Itertools as _;
4
use std::sync::LazyLock;
5
use tor_cert::Ed25519Cert;
6
use tor_checkable::Timebound;
7
use tor_checkable::signed::SignatureGated;
8
use tor_checkable::timed::TimerangeBound;
9
use tor_error::internal;
10
use tor_hscrypto::pk::HsBlindId;
11
use tor_hscrypto::{RevisionCounter, Subcredential};
12
use tor_llcrypto::pk::ed25519::{self, Ed25519Identity, ValidatableEd25519Signature};
13
use tor_units::IntegerMinutes;
14

            
15
use crate::parse::tokenize::Item;
16
use crate::parse::{keyword::Keyword, parser::SectionRules, tokenize::NetDocReader};
17
use crate::types::misc::{B64, UnvalidatedEdCert};
18
use crate::{NetdocErrorKind as EK, Pos, Result};
19

            
20
use super::desc_enc;
21

            
22
/// The current version-number.
23
pub(super) const HS_DESC_VERSION_CURRENT: &str = "3";
24

            
25
/// The text the outer document signature is prefixed with.
26
pub(super) const HS_DESC_SIGNATURE_PREFIX: &[u8] = b"Tor onion service descriptor sig v3";
27

            
28
/// A more-or-less verbatim representation of the outermost plaintext document
29
/// of an onion service descriptor.
30
#[derive(Clone, Debug)]
31
pub struct HsDescOuter {
32
    /// The lifetime of this descriptor, in minutes.
33
    ///
34
    /// This doesn't actually list the starting time or the end time for the
35
    /// descriptor: presumably, because we didn't want to leak the onion
36
    /// service's view of the wallclock.
37
    pub(super) lifetime: IntegerMinutes<u16>,
38
    /// A certificate containing the descriptor-signing-key for this onion
39
    /// service (`KP_hs_desc_sign`) signed by the blinded ed25519 identity
40
    /// (`HS_blind_id`) for this onion service.
41
    pub(super) desc_signing_key_cert: Ed25519Cert,
42
    /// A revision counter to tell whether this descriptor is more or less recent
43
    /// than another one for the same blinded ID.
44
    pub(super) revision_counter: RevisionCounter,
45
    /// The encrypted contents of this onion service descriptor.
46
    ///
47
    /// Clients will decrypt this; onion service directories cannot.
48
    //
49
    // TODO: it might be a good idea to just discard this immediately (after checking it)
50
    // for the directory case.
51
    pub(super) superencrypted: Vec<u8>,
52
}
53

            
54
impl HsDescOuter {
55
    /// Return the blinded Id for this onion service descriptor.
56
2072
    pub(super) fn blinded_id(&self) -> HsBlindId {
57
2072
        let ident = self
58
2072
            .desc_signing_key_cert
59
2072
            .signing_key()
60
2072
            .expect("signing key was absent!?");
61
2072
        (*ident).into()
62
2072
    }
63

            
64
    /// Return the Id of the descriptor-signing key (`KP_desc_sign`) from this onion service descriptor.
65
690
    pub(super) fn desc_sign_key_id(&self) -> &Ed25519Identity {
66
690
        self.desc_signing_key_cert
67
690
            .subject_key()
68
690
            .as_ed25519()
69
690
            .expect(
70
690
                "Somehow constructed an HsDescOuter with a non-Ed25519 signing key in its cert.",
71
690
            )
72
690
    }
73

            
74
    /// Return the revision counter for this descriptor.
75
1378
    pub(super) fn revision_counter(&self) -> RevisionCounter {
76
1378
        self.revision_counter
77
1378
    }
78

            
79
    /// Decrypt and return the encrypted (middle document) body of this onion
80
    /// service descriptor.
81
692
    pub(super) fn decrypt_body(
82
692
        &self,
83
692
        subcredential: &Subcredential,
84
692
    ) -> std::result::Result<Vec<u8>, desc_enc::DecryptionError> {
85
692
        let decrypt = desc_enc::HsDescEncryption {
86
692
            blinded_id: &self.blinded_id(),
87
692
            desc_enc_nonce: None,
88
692
            subcredential,
89
692
            revision: self.revision_counter,
90
692
            string_const: b"hsdir-superencrypted-data",
91
692
        };
92

            
93
692
        let mut body = decrypt.decrypt(&self.superencrypted[..])?;
94
3865609
        let n_padding = body.iter().rev().take_while(|n| **n == 0).count();
95
692
        body.truncate(body.len() - n_padding);
96
        // Work around a bug in the C tor implementation: it doesn't
97
        // NL-terminate the final line of the middle document.
98
692
        if !body.ends_with(b"\n") {
99
688
            body.push(b'\n');
100
688
        }
101
692
        Ok(body)
102
692
    }
103
}
104

            
105
/// An `HsDescOuter` whose signatures have not yet been verified, and whose
106
/// timeliness has not been checked.
107
pub(super) type UncheckedHsDescOuter = SignatureGated<TimerangeBound<HsDescOuter>>;
108

            
109
decl_keyword! {
110
    pub(crate) HsOuterKwd {
111
        "hs-descriptor" => HS_DESCRIPTOR,
112
        "descriptor-lifetime" => DESCRIPTOR_LIFETIME,
113
        "descriptor-signing-key-cert" => DESCRIPTOR_SIGNING_KEY_CERT,
114
        "revision-counter" => REVISION_COUNTER,
115
        "superencrypted" => SUPERENCRYPTED,
116
        "signature" => SIGNATURE
117
    }
118
}
119

            
120
/// Check whether there are any extraneous spaces used for the encoding of `sig` within `within_string`.
121
/// Return an error if there are.
122
///
123
/// This check helps to prevent some length extension attacks.
124
700
fn validate_signature_item(item: &Item<'_, HsOuterKwd>, within_string: &str) -> Result<()> {
125
700
    let s = item
126
700
        .text_within(within_string)
127
700
        .ok_or_else(|| internal!("Signature item not from within expected string!?"))?;
128

            
129
67671
    let is_hspace = |b| b == b' ' || b == b'\t';
130

            
131
66944
    for (a, b) in s.bytes().tuple_windows() {
132
66944
        if is_hspace(a) && is_hspace(b) {
133
4
            return Err(EK::ExtraneousSpace.at_pos(item.pos()));
134
66940
        }
135
    }
136

            
137
696
    Ok(())
138
700
}
139

            
140
/// Rules about how keywords appear in the outer document of an onion service
141
/// descriptor.
142
106
static HS_OUTER_RULES: LazyLock<SectionRules<HsOuterKwd>> = LazyLock::new(|| {
143
    use HsOuterKwd::*;
144

            
145
106
    let mut rules = SectionRules::builder();
146
106
    rules.add(HS_DESCRIPTOR.rule().required().args(1..));
147
106
    rules.add(DESCRIPTOR_LIFETIME.rule().required().args(1..));
148
106
    rules.add(DESCRIPTOR_SIGNING_KEY_CERT.rule().required().obj_required());
149
106
    rules.add(REVISION_COUNTER.rule().required().args(1..));
150
106
    rules.add(SUPERENCRYPTED.rule().required().obj_required());
151
106
    rules.add(SIGNATURE.rule().required().args(1..));
152
106
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
153

            
154
106
    rules.build()
155
106
});
156

            
157
impl HsDescOuter {
158
    /// Try to parse an outer document of an onion service descriptor from a string.
159
696
    pub fn parse(s: &str) -> Result<UncheckedHsDescOuter> {
160
        // TOSO HS needs to be unchecked.
161
696
        let mut reader = NetDocReader::new(s)?;
162
696
        let result = HsDescOuter::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
163
696
        Ok(result)
164
696
    }
165

            
166
    /// Extract an HsDescOuter from a reader.
167
    ///
168
    /// The reader must contain a single HsDescOuter; we return an error if not.
169
696
    fn take_from_reader(reader: &mut NetDocReader<'_, HsOuterKwd>) -> Result<UncheckedHsDescOuter> {
170
        use crate::err::NetdocErrorKind as EK;
171
        use HsOuterKwd::*;
172

            
173
696
        let s = reader.str();
174
696
        let body = HS_OUTER_RULES.parse(reader)?;
175

            
176
        // Enforce that the object starts and ends with the right keywords, and
177
        // find the start and end of the signed material.
178
696
        let signed_text = {
179
696
            let first_item = body
180
696
                .first_item()
181
696
                .expect("Somehow parsing worked though no keywords were present‽");
182
696
            let last_item = body
183
696
                .last_item()
184
696
                .expect("Somehow parsing worked though no keywords were present‽");
185
696
            if first_item.kwd() != HS_DESCRIPTOR {
186
                return Err(EK::WrongStartingToken
187
                    .with_msg(first_item.kwd_str().to_string())
188
                    .at_pos(first_item.pos()));
189
696
            }
190
696
            if last_item.kwd() != SIGNATURE {
191
                return Err(EK::WrongEndingToken
192
                    .with_msg(last_item.kwd_str().to_string())
193
                    .at_pos(last_item.pos()));
194
696
            }
195
696
            validate_signature_item(last_item, s)?;
196
696
            let start_idx = first_item
197
696
                .pos()
198
696
                .offset_within(s)
199
696
                .expect("Token came from nowhere within the string‽");
200
696
            let end_idx = last_item
201
696
                .pos()
202
696
                .offset_within(s)
203
696
                .expect("Token came from nowhere within the string‽");
204
            // TODO: This way of handling prefixes does a needless
205
            // allocation. Someday we could make our signature-checking
206
            // logic even smarter.
207
696
            let mut signed_text = HS_DESC_SIGNATURE_PREFIX.to_vec();
208
696
            signed_text.extend_from_slice(
209
696
                s.get(start_idx..end_idx)
210
696
                    .expect("Somehow the first item came after the last‽")
211
696
                    .as_bytes(),
212
            );
213
696
            signed_text
214
        };
215

            
216
        // Check that the hs-descriptor version is 3.
217
        {
218
696
            let version = body.required(HS_DESCRIPTOR)?.required_arg(0)?;
219
696
            if version != HS_DESC_VERSION_CURRENT {
220
                return Err(EK::BadDocumentVersion
221
                    .with_msg(format!("Unexpected hsdesc version {}", version))
222
                    .at_pos(Pos::at(version)));
223
696
            }
224
        }
225

            
226
        // Parse `descryptor-lifetime`.
227
696
        let lifetime: IntegerMinutes<u16> = {
228
696
            let tok = body.required(DESCRIPTOR_LIFETIME)?;
229
696
            let lifetime_minutes: u16 = tok.parse_arg(0)?;
230
696
            if !(30..=720).contains(&lifetime_minutes) {
231
                return Err(EK::BadArgument
232
                    .with_msg(format!("Invalid HsDesc lifetime {}", lifetime_minutes))
233
                    .at_pos(tok.pos()));
234
696
            }
235
696
            lifetime_minutes.into()
236
        };
237

            
238
        // Parse `descriptor-signing-key-cert`.  This certificate is signed with
239
        // the blinded Id (`KP_blinded_id`), and used to authenticate the
240
        // descriptor signing key (`KP_hs_desc_sign`).
241
696
        let (unchecked_cert, kp_desc_sign) = {
242
696
            let cert_tok = body.required(DESCRIPTOR_SIGNING_KEY_CERT)?;
243
696
            let cert = cert_tok
244
696
                .parse_obj::<UnvalidatedEdCert>("ED25519 CERT")?
245
696
                .check_cert_type(tor_cert::CertType::HS_BLINDED_ID_V_SIGNING)?
246
696
                .into_unchecked()
247
696
                .should_have_signing_key()
248
696
                .map_err(|err| {
249
                    EK::BadObjectVal
250
                        .err()
251
                        .with_source(err)
252
                        .at_pos(cert_tok.pos())
253
                })?;
254
696
            let kp_desc_sign: ed25519::PublicKey = cert
255
696
                .peek_subject_key()
256
696
                .as_ed25519()
257
719
                .and_then(|id| id.try_into().ok())
258
696
                .ok_or_else(|| {
259
                    EK::BadObjectVal
260
                        .err()
261
                        .with_msg("Invalid ed25519 subject key")
262
                        .at_pos(cert_tok.pos())
263
                })?;
264
696
            (cert, kp_desc_sign)
265
        };
266

            
267
        // Parse remaining fields, which are nice and simple.
268
696
        let revision_counter = body.required(REVISION_COUNTER)?.parse_arg::<u64>(0)?.into();
269
696
        let encrypted_body: Vec<u8> = body.required(SUPERENCRYPTED)?.obj("MESSAGE")?;
270
696
        let signature = body
271
696
            .required(SIGNATURE)?
272
696
            .parse_arg::<B64>(0)?
273
696
            .into_array()
274
696
            .map_err(|_| EK::BadSignature.with_msg("Bad signature object length"))?;
275
696
        let signature = ed25519::Signature::from(signature);
276

            
277
        // Split apart the unchecked `descriptor-signing-key-cert`:
278
        // its constraints will become our own.
279
696
        let (desc_signing_key_cert, cert_signature) = unchecked_cert
280
696
            .dangerously_split()
281
            // we already checked that there is a public key, so an error should be impossible.
282
696
            .map_err(|e| EK::Internal.err().with_source(e))?;
283
696
        let desc_signing_key_cert = desc_signing_key_cert.dangerously_assume_timely();
284
        // NOTE: the C tor implementation checks this expiration time, so we must too.
285
696
        let expiration = desc_signing_key_cert.expiry();
286

            
287
        // Build our return value.
288
696
        let desc = HsDescOuter {
289
696
            lifetime,
290
696
            desc_signing_key_cert,
291
696
            revision_counter,
292
696
            superencrypted: encrypted_body,
293
696
        };
294
        // You can't have that until you check that it's timely.
295
696
        let desc = TimerangeBound::new(desc, ..expiration);
296
        // And you can't have _that_ until you check the signatures.
297
696
        let signatures: Vec<Box<dyn tor_llcrypto::pk::ValidatableSignature>> = vec![
298
696
            Box::new(cert_signature),
299
696
            Box::new(ValidatableEd25519Signature::new(
300
696
                kp_desc_sign,
301
696
                signature,
302
696
                &signed_text[..],
303
696
            )),
304
        ];
305
696
        Ok(SignatureGated::new(desc, signatures))
306
696
    }
307
}
308

            
309
#[cfg(test)]
310
mod test {
311
    // @@ begin test lint list maintained by maint/add_warning @@
312
    #![allow(clippy::bool_assert_comparison)]
313
    #![allow(clippy::clone_on_copy)]
314
    #![allow(clippy::dbg_macro)]
315
    #![allow(clippy::mixed_attributes_style)]
316
    #![allow(clippy::print_stderr)]
317
    #![allow(clippy::print_stdout)]
318
    #![allow(clippy::single_char_pattern)]
319
    #![allow(clippy::unwrap_used)]
320
    #![allow(clippy::unchecked_time_subtraction)]
321
    #![allow(clippy::useless_vec)]
322
    #![allow(clippy::needless_pass_by_value)]
323
    #![allow(clippy::string_slice)] // See arti#2571
324
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
325
    use super::*;
326
    use crate::doc::hsdesc::test_data::{TEST_DATA, TEST_SUBCREDENTIAL};
327
    use tor_checkable::SelfSigned;
328

            
329
    #[test]
330
    fn parse_good() -> Result<()> {
331
        let desc = HsDescOuter::parse(TEST_DATA)?;
332

            
333
        let desc = desc
334
            .check_signature()?
335
            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
336
            .unwrap();
337

            
338
        assert_eq!(desc.lifetime.as_minutes(), 180);
339
        assert_eq!(desc.revision_counter(), 19655750.into());
340
        assert_eq!(
341
            desc.desc_sign_key_id().to_string(),
342
            "CtiubqLBP1MCviR9SxAW9brjMKSguQFE/vHku3kE4Xo"
343
        );
344

            
345
        let subcred: tor_hscrypto::Subcredential = TEST_SUBCREDENTIAL.into();
346
        let inner = desc.decrypt_body(&subcred).unwrap();
347

            
348
        assert!(
349
            std::str::from_utf8(&inner)
350
                .unwrap()
351
                .starts_with("desc-auth-type")
352
        );
353

            
354
        Ok(())
355
    }
356

            
357
    #[test]
358
    fn invalidate_signature_items() {
359
        for s in &[
360
            "signature  CtiubqLBP1MCviR9SxAW9brjMKSguQFE/vHku3kE4Xo\n",
361
            "signature CtiubqLBP1MCviR9SxAW9brjMKSguQFE/vHku3kE4Xo  \n",
362
        ] {
363
            let mut reader = NetDocReader::<HsOuterKwd>::new(s).unwrap();
364
            let item = reader.next().unwrap().unwrap();
365
            let res = validate_signature_item(&item, s);
366
            let err = res.unwrap_err();
367
            assert!(err.netdoc_error_kind() == EK::ExtraneousSpace);
368
        }
369
    }
370
}