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

            
3
use once_cell::sync::Lazy;
4
use tor_cert::Ed25519Cert;
5
use tor_checkable::signed::SignatureGated;
6
use tor_checkable::timed::TimerangeBound;
7
use tor_checkable::Timebound;
8
use tor_hscrypto::pk::HsBlindId;
9
use tor_hscrypto::{RevisionCounter, Subcredential};
10
use tor_llcrypto::pk::ed25519::{self, Ed25519Identity, ValidatableEd25519Signature};
11
use tor_units::IntegerMinutes;
12

            
13
use crate::parse::{keyword::Keyword, parser::SectionRules, tokenize::NetDocReader};
14
use crate::types::misc::{UnvalidatedEdCert, B64};
15
use crate::{Pos, Result};
16

            
17
use super::desc_enc;
18

            
19
/// The current version-number.
20
pub(super) const HS_DESC_VERSION_CURRENT: &str = "3";
21

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

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

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

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

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

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

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

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

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

            
118
/// Rules about how keywords appear in the outer document of an onion service
119
/// descriptor.
120
84
static HS_OUTER_RULES: Lazy<SectionRules<HsOuterKwd>> = Lazy::new(|| {
121
    use HsOuterKwd::*;
122

            
123
84
    let mut rules = SectionRules::builder();
124
84
    rules.add(HS_DESCRIPTOR.rule().required().args(1..));
125
84
    rules.add(DESCRIPTOR_LIFETIME.rule().required().args(1..));
126
84
    rules.add(DESCRIPTOR_SIGNING_KEY_CERT.rule().required().obj_required());
127
84
    rules.add(REVISION_COUNTER.rule().required().args(1..));
128
84
    rules.add(SUPERENCRYPTED.rule().required().obj_required());
129
84
    rules.add(SIGNATURE.rule().required().args(1..));
130
84
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
131
84

            
132
84
    rules.build()
133
84
});
134

            
135
impl HsDescOuter {
136
    /// Try to parse an outer document of an onion service descriptor from a string.
137
307
    #[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
138
307
    pub(super) fn parse(s: &str) -> Result<UncheckedHsDescOuter> {
139
307
        // TOSO HS needs to be unchecked.
140
307
        let mut reader = NetDocReader::new(s);
141
307
        let result = HsDescOuter::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
142
307
        Ok(result)
143
307
    }
144

            
145
    /// Extract an HsDescOuter from a reader.
146
    ///
147
    /// The reader must contain a single HsDescOuter; we return an error if not.
148
307
    fn take_from_reader(reader: &mut NetDocReader<'_, HsOuterKwd>) -> Result<UncheckedHsDescOuter> {
149
        use crate::err::NetdocErrorKind as EK;
150
        use HsOuterKwd::*;
151

            
152
307
        let s = reader.str();
153
307
        let body = HS_OUTER_RULES.parse(reader)?;
154

            
155
        // Enforce that the object starts and ends with the right keywords, and
156
        // find the start and end of the signed material.
157
307
        let signed_text = {
158
307
            let first_item = body
159
307
                .first_item()
160
307
                .expect("Somehow parsing worked though no keywords were present‽");
161
307
            let last_item = body
162
307
                .last_item()
163
307
                .expect("Somehow parsing worked though no keywords were present‽");
164
307
            if first_item.kwd() != HS_DESCRIPTOR {
165
                return Err(EK::WrongStartingToken
166
                    .with_msg(first_item.kwd_str().to_string())
167
                    .at_pos(first_item.pos()));
168
307
            }
169
307
            if last_item.kwd() != SIGNATURE {
170
                return Err(EK::WrongEndingToken
171
                    .with_msg(last_item.kwd_str().to_string())
172
                    .at_pos(last_item.pos()));
173
307
            }
174
307
            let start_idx = first_item
175
307
                .pos()
176
307
                .offset_within(s)
177
307
                .expect("Token came from nowhere within the string‽");
178
307
            let end_idx = last_item
179
307
                .pos()
180
307
                .offset_within(s)
181
307
                .expect("Token came from nowhere within the string‽");
182
307
            // TODO: This way of handling prefixes does a needless
183
307
            // allocation. Someday we could make our signature-checking
184
307
            // logic even smarter.
185
307
            let mut signed_text = HS_DESC_SIGNATURE_PREFIX.to_vec();
186
307
            signed_text.extend_from_slice(
187
307
                s.get(start_idx..end_idx)
188
307
                    .expect("Somehow the first item came after the last‽")
189
307
                    .as_bytes(),
190
307
            );
191
307
            signed_text
192
        };
193

            
194
        // Check that the hs-descriptor version is 3.
195
        {
196
307
            let version = body.required(HS_DESCRIPTOR)?.required_arg(0)?;
197
307
            if version != HS_DESC_VERSION_CURRENT {
198
                return Err(EK::BadDocumentVersion
199
                    .with_msg(format!("Unexpected hsdesc version {}", version))
200
                    .at_pos(Pos::at(version)));
201
307
            }
202
        }
203

            
204
        // Parse `descryptor-lifetime`.
205
307
        let lifetime: IntegerMinutes<u16> = {
206
307
            let tok = body.required(DESCRIPTOR_LIFETIME)?;
207
307
            let lifetime_minutes: u16 = tok.parse_arg(0)?;
208
307
            if !(30..=720).contains(&lifetime_minutes) {
209
                return Err(EK::BadArgument
210
                    .with_msg(format!("Invalid HsDesc lifetime {}", lifetime_minutes))
211
                    .at_pos(tok.pos()));
212
307
            }
213
307
            lifetime_minutes.into()
214
        };
215

            
216
        // Parse `descriptor-signing-key-cert`.  This certificate is signed with
217
        // the blinded Id (`KP_blinded_id`), and used to authenticate the
218
        // descriptor signing key (`KP_hs_desc_sign`).
219
307
        let (unchecked_cert, kp_desc_sign) = {
220
307
            let cert_tok = body.required(DESCRIPTOR_SIGNING_KEY_CERT)?;
221
307
            let cert = cert_tok
222
307
                .parse_obj::<UnvalidatedEdCert>("ED25519 CERT")?
223
307
                .check_cert_type(tor_cert::CertType::HS_BLINDED_ID_V_SIGNING)?
224
307
                .into_unchecked()
225
307
                .should_have_signing_key()
226
307
                .map_err(|err| {
227
                    EK::BadObjectVal
228
                        .err()
229
                        .with_source(err)
230
                        .at_pos(cert_tok.pos())
231
307
                })?;
232
307
            let kp_desc_sign: ed25519::PublicKey = cert
233
307
                .peek_subject_key()
234
307
                .as_ed25519()
235
324
                .and_then(|id| id.try_into().ok())
236
307
                .ok_or_else(|| {
237
                    EK::BadObjectVal
238
                        .err()
239
                        .with_msg("Invalid ed25519 subject key")
240
                        .at_pos(cert_tok.pos())
241
307
                })?;
242
307
            (cert, kp_desc_sign)
243
        };
244

            
245
        // Parse remaining fields, which are nice and simple.
246
307
        let revision_counter = body.required(REVISION_COUNTER)?.parse_arg::<u64>(0)?.into();
247
307
        let encrypted_body: Vec<u8> = body.required(SUPERENCRYPTED)?.obj("MESSAGE")?;
248
307
        let signature = body
249
307
            .required(SIGNATURE)?
250
307
            .parse_arg::<B64>(0)?
251
307
            .into_array()
252
307
            .map_err(|_| EK::BadSignature.with_msg("Bad signature object length"))?;
253
307
        let signature = ed25519::Signature::from(signature);
254

            
255
        // Split apart the unchecked `descriptor-signing-key-cert`:
256
        // its constraints will become our own.
257
307
        let (desc_signing_key_cert, cert_signature) = unchecked_cert
258
307
            .dangerously_split()
259
307
            // we already checked that there is a public key, so an error should be impossible.
260
307
            .map_err(|e| EK::Internal.err().with_source(e))?;
261
307
        let desc_signing_key_cert = desc_signing_key_cert.dangerously_assume_timely();
262
307
        // NOTE: the C tor implementation checks this expiration time, so we must too.
263
307
        let expiration = desc_signing_key_cert.expiry();
264
307

            
265
307
        // Build our return value.
266
307
        let desc = HsDescOuter {
267
307
            lifetime,
268
307
            desc_signing_key_cert,
269
307
            revision_counter,
270
307
            superencrypted: encrypted_body,
271
307
        };
272
307
        // You can't have that until you check that it's timely.
273
307
        let desc = TimerangeBound::new(desc, ..expiration);
274
307
        // And you can't have _that_ until you check the signatures.
275
307
        let signatures: Vec<Box<dyn tor_llcrypto::pk::ValidatableSignature>> = vec![
276
307
            Box::new(cert_signature),
277
307
            Box::new(ValidatableEd25519Signature::new(
278
307
                kp_desc_sign,
279
307
                signature,
280
307
                &signed_text[..],
281
307
            )),
282
307
        ];
283
307
        Ok(SignatureGated::new(desc, signatures))
284
307
    }
285
}
286

            
287
#[cfg(test)]
288
mod test {
289
    // @@ begin test lint list maintained by maint/add_warning @@
290
    #![allow(clippy::bool_assert_comparison)]
291
    #![allow(clippy::clone_on_copy)]
292
    #![allow(clippy::dbg_macro)]
293
    #![allow(clippy::mixed_attributes_style)]
294
    #![allow(clippy::print_stderr)]
295
    #![allow(clippy::print_stdout)]
296
    #![allow(clippy::single_char_pattern)]
297
    #![allow(clippy::unwrap_used)]
298
    #![allow(clippy::unchecked_duration_subtraction)]
299
    #![allow(clippy::useless_vec)]
300
    #![allow(clippy::needless_pass_by_value)]
301
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
302
    use super::*;
303
    use crate::doc::hsdesc::test_data::{TEST_DATA, TEST_SUBCREDENTIAL};
304
    use tor_checkable::SelfSigned;
305

            
306
    #[test]
307
    fn parse_good() -> Result<()> {
308
        let desc = HsDescOuter::parse(TEST_DATA)?;
309

            
310
        let desc = desc
311
            .check_signature()?
312
            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
313
            .unwrap();
314

            
315
        assert_eq!(desc.lifetime.as_minutes(), 180);
316
        assert_eq!(desc.revision_counter(), 19655750.into());
317
        assert_eq!(
318
            desc.desc_sign_key_id().to_string(),
319
            "CtiubqLBP1MCviR9SxAW9brjMKSguQFE/vHku3kE4Xo"
320
        );
321

            
322
        let subcred: tor_hscrypto::Subcredential = TEST_SUBCREDENTIAL.into();
323
        let inner = desc.decrypt_body(&subcred).unwrap();
324

            
325
        assert!(std::str::from_utf8(&inner)
326
            .unwrap()
327
            .starts_with("desc-auth-type"));
328

            
329
        Ok(())
330
    }
331
}