tor_netdoc/doc/hsdesc/
outer.rs

1//! Implement parsing for the outer document of an onion service descriptor.
2
3use itertools::Itertools as _;
4use std::sync::LazyLock;
5use tor_cert::Ed25519Cert;
6use tor_checkable::signed::SignatureGated;
7use tor_checkable::timed::TimerangeBound;
8use tor_checkable::Timebound;
9use tor_error::internal;
10use tor_hscrypto::pk::HsBlindId;
11use tor_hscrypto::{RevisionCounter, Subcredential};
12use tor_llcrypto::pk::ed25519::{self, Ed25519Identity, ValidatableEd25519Signature};
13use tor_units::IntegerMinutes;
14
15use crate::parse::tokenize::Item;
16use crate::parse::{keyword::Keyword, parser::SectionRules, tokenize::NetDocReader};
17use crate::types::misc::{UnvalidatedEdCert, B64};
18use crate::{NetdocErrorKind as EK, Pos, Result};
19
20use super::desc_enc;
21
22/// The current version-number.
23pub(super) const HS_DESC_VERSION_CURRENT: &str = "3";
24
25/// The text the outer document signature is prefixed with.
26pub(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#[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
32pub(super) struct HsDescOuter {
33    /// The lifetime of this descriptor, in minutes.
34    ///
35    /// This doesn't actually list the starting time or the end time for the
36    /// descriptor: presumably, because we didn't want to leak the onion
37    /// service's view of the wallclock.
38    pub(super) lifetime: IntegerMinutes<u16>,
39    /// A certificate containing the descriptor-signing-key for this onion
40    /// service (`KP_hs_desc_sign`) signed by the blinded ed25519 identity
41    /// (`HS_blind_id`) for this onion service.
42    pub(super) desc_signing_key_cert: Ed25519Cert,
43    /// A revision counter to tell whether this descriptor is more or less recent
44    /// than another one for the same blinded ID.
45    pub(super) revision_counter: RevisionCounter,
46    /// The encrypted contents of this onion service descriptor.
47    ///
48    /// Clients will decrypt this; onion service directories cannot.
49    //
50    // TODO: it might be a good idea to just discard this immediately (after checking it)
51    // for the directory case.
52    pub(super) superencrypted: Vec<u8>,
53}
54
55impl HsDescOuter {
56    /// Return the blinded Id for this onion service descriptor.
57    pub(super) fn blinded_id(&self) -> HsBlindId {
58        let ident = self
59            .desc_signing_key_cert
60            .signing_key()
61            .expect("signing key was absent!?");
62        (*ident).into()
63    }
64
65    /// Return the Id of the descriptor-signing key (`KP_desc_sign`) from this onion service descriptor.
66    pub(super) fn desc_sign_key_id(&self) -> &Ed25519Identity {
67        self.desc_signing_key_cert
68            .subject_key()
69            .as_ed25519()
70            .expect(
71                "Somehow constructed an HsDescOuter with a non-Ed25519 signing key in its cert.",
72            )
73    }
74
75    /// Return the revision counter for this descriptor.
76    pub(super) fn revision_counter(&self) -> RevisionCounter {
77        self.revision_counter
78    }
79
80    /// Decrypt and return the encrypted (middle document) body of this onion
81    /// service descriptor.
82    pub(super) fn decrypt_body(
83        &self,
84        subcredential: &Subcredential,
85    ) -> std::result::Result<Vec<u8>, desc_enc::DecryptionError> {
86        let decrypt = desc_enc::HsDescEncryption {
87            blinded_id: &self.blinded_id(),
88            desc_enc_nonce: None,
89            subcredential,
90            revision: self.revision_counter,
91            string_const: b"hsdir-superencrypted-data",
92        };
93
94        let mut body = decrypt.decrypt(&self.superencrypted[..])?;
95        let n_padding = body.iter().rev().take_while(|n| **n == 0).count();
96        body.truncate(body.len() - n_padding);
97        // Work around a bug in the C tor implementation: it doesn't
98        // NL-terminate the final line of the middle document.
99        if !body.ends_with(b"\n") {
100            body.push(b'\n');
101        }
102        Ok(body)
103    }
104}
105
106/// An `HsDescOuter` whose signatures have not yet been verified, and whose
107/// timeliness has not been checked.
108pub(super) type UncheckedHsDescOuter = SignatureGated<TimerangeBound<HsDescOuter>>;
109
110decl_keyword! {
111    pub(crate) HsOuterKwd {
112        "hs-descriptor" => HS_DESCRIPTOR,
113        "descriptor-lifetime" => DESCRIPTOR_LIFETIME,
114        "descriptor-signing-key-cert" => DESCRIPTOR_SIGNING_KEY_CERT,
115        "revision-counter" => REVISION_COUNTER,
116        "superencrypted" => SUPERENCRYPTED,
117        "signature" => SIGNATURE
118    }
119}
120
121/// Check whether there are any extraneous spaces used for the encoding of `sig` within `within_string`.
122/// Return an error if there are.
123///
124/// This check helps to prevent some length extension attacks.
125fn validate_signature_item(item: &Item<'_, HsOuterKwd>, within_string: &str) -> Result<()> {
126    let s = item
127        .text_within(within_string)
128        .ok_or_else(|| internal!("Signature item not from within expected string!?"))?;
129
130    let is_hspace = |b| b == b' ' || b == b'\t';
131
132    for (a, b) in s.bytes().tuple_windows() {
133        if is_hspace(a) && is_hspace(b) {
134            return Err(EK::ExtraneousSpace.at_pos(item.pos()));
135        }
136    }
137
138    Ok(())
139}
140
141/// Rules about how keywords appear in the outer document of an onion service
142/// descriptor.
143static HS_OUTER_RULES: LazyLock<SectionRules<HsOuterKwd>> = LazyLock::new(|| {
144    use HsOuterKwd::*;
145
146    let mut rules = SectionRules::builder();
147    rules.add(HS_DESCRIPTOR.rule().required().args(1..));
148    rules.add(DESCRIPTOR_LIFETIME.rule().required().args(1..));
149    rules.add(DESCRIPTOR_SIGNING_KEY_CERT.rule().required().obj_required());
150    rules.add(REVISION_COUNTER.rule().required().args(1..));
151    rules.add(SUPERENCRYPTED.rule().required().obj_required());
152    rules.add(SIGNATURE.rule().required().args(1..));
153    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
154
155    rules.build()
156});
157
158impl HsDescOuter {
159    /// Try to parse an outer document of an onion service descriptor from a string.
160    #[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
161    pub(super) fn parse(s: &str) -> Result<UncheckedHsDescOuter> {
162        // TOSO HS needs to be unchecked.
163        let mut reader = NetDocReader::new(s)?;
164        let result = HsDescOuter::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
165        Ok(result)
166    }
167
168    /// Extract an HsDescOuter from a reader.
169    ///
170    /// The reader must contain a single HsDescOuter; we return an error if not.
171    fn take_from_reader(reader: &mut NetDocReader<'_, HsOuterKwd>) -> Result<UncheckedHsDescOuter> {
172        use crate::err::NetdocErrorKind as EK;
173        use HsOuterKwd::*;
174
175        let s = reader.str();
176        let body = HS_OUTER_RULES.parse(reader)?;
177
178        // Enforce that the object starts and ends with the right keywords, and
179        // find the start and end of the signed material.
180        let signed_text = {
181            let first_item = body
182                .first_item()
183                .expect("Somehow parsing worked though no keywords were present‽");
184            let last_item = body
185                .last_item()
186                .expect("Somehow parsing worked though no keywords were present‽");
187            if first_item.kwd() != HS_DESCRIPTOR {
188                return Err(EK::WrongStartingToken
189                    .with_msg(first_item.kwd_str().to_string())
190                    .at_pos(first_item.pos()));
191            }
192            if last_item.kwd() != SIGNATURE {
193                return Err(EK::WrongEndingToken
194                    .with_msg(last_item.kwd_str().to_string())
195                    .at_pos(last_item.pos()));
196            }
197            validate_signature_item(last_item, s)?;
198            let start_idx = first_item
199                .pos()
200                .offset_within(s)
201                .expect("Token came from nowhere within the string‽");
202            let end_idx = last_item
203                .pos()
204                .offset_within(s)
205                .expect("Token came from nowhere within the string‽");
206            // TODO: This way of handling prefixes does a needless
207            // allocation. Someday we could make our signature-checking
208            // logic even smarter.
209            let mut signed_text = HS_DESC_SIGNATURE_PREFIX.to_vec();
210            signed_text.extend_from_slice(
211                s.get(start_idx..end_idx)
212                    .expect("Somehow the first item came after the last‽")
213                    .as_bytes(),
214            );
215            signed_text
216        };
217
218        // Check that the hs-descriptor version is 3.
219        {
220            let version = body.required(HS_DESCRIPTOR)?.required_arg(0)?;
221            if version != HS_DESC_VERSION_CURRENT {
222                return Err(EK::BadDocumentVersion
223                    .with_msg(format!("Unexpected hsdesc version {}", version))
224                    .at_pos(Pos::at(version)));
225            }
226        }
227
228        // Parse `descryptor-lifetime`.
229        let lifetime: IntegerMinutes<u16> = {
230            let tok = body.required(DESCRIPTOR_LIFETIME)?;
231            let lifetime_minutes: u16 = tok.parse_arg(0)?;
232            if !(30..=720).contains(&lifetime_minutes) {
233                return Err(EK::BadArgument
234                    .with_msg(format!("Invalid HsDesc lifetime {}", lifetime_minutes))
235                    .at_pos(tok.pos()));
236            }
237            lifetime_minutes.into()
238        };
239
240        // Parse `descriptor-signing-key-cert`.  This certificate is signed with
241        // the blinded Id (`KP_blinded_id`), and used to authenticate the
242        // descriptor signing key (`KP_hs_desc_sign`).
243        let (unchecked_cert, kp_desc_sign) = {
244            let cert_tok = body.required(DESCRIPTOR_SIGNING_KEY_CERT)?;
245            let cert = cert_tok
246                .parse_obj::<UnvalidatedEdCert>("ED25519 CERT")?
247                .check_cert_type(tor_cert::CertType::HS_BLINDED_ID_V_SIGNING)?
248                .into_unchecked()
249                .should_have_signing_key()
250                .map_err(|err| {
251                    EK::BadObjectVal
252                        .err()
253                        .with_source(err)
254                        .at_pos(cert_tok.pos())
255                })?;
256            let kp_desc_sign: ed25519::PublicKey = cert
257                .peek_subject_key()
258                .as_ed25519()
259                .and_then(|id| id.try_into().ok())
260                .ok_or_else(|| {
261                    EK::BadObjectVal
262                        .err()
263                        .with_msg("Invalid ed25519 subject key")
264                        .at_pos(cert_tok.pos())
265                })?;
266            (cert, kp_desc_sign)
267        };
268
269        // Parse remaining fields, which are nice and simple.
270        let revision_counter = body.required(REVISION_COUNTER)?.parse_arg::<u64>(0)?.into();
271        let encrypted_body: Vec<u8> = body.required(SUPERENCRYPTED)?.obj("MESSAGE")?;
272        let signature = body
273            .required(SIGNATURE)?
274            .parse_arg::<B64>(0)?
275            .into_array()
276            .map_err(|_| EK::BadSignature.with_msg("Bad signature object length"))?;
277        let signature = ed25519::Signature::from(signature);
278
279        // Split apart the unchecked `descriptor-signing-key-cert`:
280        // its constraints will become our own.
281        let (desc_signing_key_cert, cert_signature) = unchecked_cert
282            .dangerously_split()
283            // we already checked that there is a public key, so an error should be impossible.
284            .map_err(|e| EK::Internal.err().with_source(e))?;
285        let desc_signing_key_cert = desc_signing_key_cert.dangerously_assume_timely();
286        // NOTE: the C tor implementation checks this expiration time, so we must too.
287        let expiration = desc_signing_key_cert.expiry();
288
289        // Build our return value.
290        let desc = HsDescOuter {
291            lifetime,
292            desc_signing_key_cert,
293            revision_counter,
294            superencrypted: encrypted_body,
295        };
296        // You can't have that until you check that it's timely.
297        let desc = TimerangeBound::new(desc, ..expiration);
298        // And you can't have _that_ until you check the signatures.
299        let signatures: Vec<Box<dyn tor_llcrypto::pk::ValidatableSignature>> = vec![
300            Box::new(cert_signature),
301            Box::new(ValidatableEd25519Signature::new(
302                kp_desc_sign,
303                signature,
304                &signed_text[..],
305            )),
306        ];
307        Ok(SignatureGated::new(desc, signatures))
308    }
309}
310
311#[cfg(test)]
312mod test {
313    // @@ begin test lint list maintained by maint/add_warning @@
314    #![allow(clippy::bool_assert_comparison)]
315    #![allow(clippy::clone_on_copy)]
316    #![allow(clippy::dbg_macro)]
317    #![allow(clippy::mixed_attributes_style)]
318    #![allow(clippy::print_stderr)]
319    #![allow(clippy::print_stdout)]
320    #![allow(clippy::single_char_pattern)]
321    #![allow(clippy::unwrap_used)]
322    #![allow(clippy::unchecked_duration_subtraction)]
323    #![allow(clippy::useless_vec)]
324    #![allow(clippy::needless_pass_by_value)]
325    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
326    use super::*;
327    use crate::doc::hsdesc::test_data::{TEST_DATA, TEST_SUBCREDENTIAL};
328    use tor_checkable::SelfSigned;
329
330    #[test]
331    fn parse_good() -> Result<()> {
332        let desc = HsDescOuter::parse(TEST_DATA)?;
333
334        let desc = desc
335            .check_signature()?
336            .check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
337            .unwrap();
338
339        assert_eq!(desc.lifetime.as_minutes(), 180);
340        assert_eq!(desc.revision_counter(), 19655750.into());
341        assert_eq!(
342            desc.desc_sign_key_id().to_string(),
343            "CtiubqLBP1MCviR9SxAW9brjMKSguQFE/vHku3kE4Xo"
344        );
345
346        let subcred: tor_hscrypto::Subcredential = TEST_SUBCREDENTIAL.into();
347        let inner = desc.decrypt_body(&subcred).unwrap();
348
349        assert!(std::str::from_utf8(&inner)
350            .unwrap()
351            .starts_with("desc-auth-type"));
352
353        Ok(())
354    }
355
356    #[test]
357    fn invalidate_signature_items() {
358        for s in &[
359            "signature  CtiubqLBP1MCviR9SxAW9brjMKSguQFE/vHku3kE4Xo\n",
360            "signature CtiubqLBP1MCviR9SxAW9brjMKSguQFE/vHku3kE4Xo  \n",
361        ] {
362            let mut reader = NetDocReader::<HsOuterKwd>::new(s).unwrap();
363            let item = reader.next().unwrap().unwrap();
364            let res = validate_signature_item(&item, s);
365            let err = res.unwrap_err();
366            assert!(err.netdoc_error_kind() == EK::ExtraneousSpace);
367        }
368    }
369}