tor_netdoc/doc/hsdesc/
outer.rs1use 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
22pub(super) const HS_DESC_VERSION_CURRENT: &str = "3";
24
25pub(super) const HS_DESC_SIGNATURE_PREFIX: &[u8] = b"Tor onion service descriptor sig v3";
27
28#[derive(Clone, Debug)]
31#[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
32pub(super) struct HsDescOuter {
33 pub(super) lifetime: IntegerMinutes<u16>,
39 pub(super) desc_signing_key_cert: Ed25519Cert,
43 pub(super) revision_counter: RevisionCounter,
46 pub(super) superencrypted: Vec<u8>,
53}
54
55impl HsDescOuter {
56 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 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 pub(super) fn revision_counter(&self) -> RevisionCounter {
77 self.revision_counter
78 }
79
80 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 if !body.ends_with(b"\n") {
100 body.push(b'\n');
101 }
102 Ok(body)
103 }
104}
105
106pub(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
121fn 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
141static 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 #[cfg_attr(feature = "hsdesc-inner-docs", visibility::make(pub))]
161 pub(super) fn parse(s: &str) -> Result<UncheckedHsDescOuter> {
162 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 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 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 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 {
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 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 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 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 let (desc_signing_key_cert, cert_signature) = unchecked_cert
282 .dangerously_split()
283 .map_err(|e| EK::Internal.err().with_source(e))?;
285 let desc_signing_key_cert = desc_signing_key_cert.dangerously_assume_timely();
286 let expiration = desc_signing_key_cert.expiry();
288
289 let desc = HsDescOuter {
291 lifetime,
292 desc_signing_key_cert,
293 revision_counter,
294 superencrypted: encrypted_body,
295 };
296 let desc = TimerangeBound::new(desc, ..expiration);
298 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 #![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 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}