1use std::str::FromStr;
4use tor_error::Bug;
5
6use crate::encode::{NetdocEncodable, NetdocEncoder};
7use crate::parse2::{
8 ErrorProblem, IsStructural, ItemStream, KeywordRef, NetdocParseable, ParseInput,
9};
10
11use ErrorProblem as EP;
12
13use crate::doc::netstatus::vote::NetworkStatusUnverifiedParsedBody as NetworkStatusVoteUnverifiedParsedBody;
14
15#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, derive_more::AsRef)]
88pub struct EncodedAuthCert(#[as_ref(str)] String);
89
90#[derive(Copy, Clone, Debug, Eq, PartialEq)]
92enum ItemSequenceChecker {
93 Intro,
95 Body,
97 End,
99}
100
101struct IsOurStructural;
103
104const INTRO_KEYWORD: &str = "dir-key-certificate-version";
106const FINAL_KEYWORD: &str = "dir-key-certification";
108
109impl EncodedAuthCert {
110 pub fn as_str(&self) -> &str {
112 &self.0
113 }
114}
115
116impl ItemSequenceChecker {
117 fn start() -> Self {
119 use ItemSequenceChecker::*;
120 Intro
121 }
122
123 fn keyword(&mut self, kw: KeywordRef<'_>) -> Result<Option<IsOurStructural>, EP> {
125 use ItemSequenceChecker::*;
126
127 let mut change_state = |from, to| {
128 if *self == from {
129 *self = to;
130 Ok(Some(IsOurStructural))
131 } else {
132 Err(EP::OtherBadDocument("authcert bad structure"))
133 }
134 };
135
136 if kw == INTRO_KEYWORD {
137 change_state(Intro, Body)
138 } else if kw == FINAL_KEYWORD {
139 change_state(Body, End)
140 } else if *self != Body {
141 Err(EP::OtherBadDocument(
142 "authcert loose body item or missing intro keyword",
143 ))
144 } else if let Some(IsStructural) =
145 NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword(kw)
146 {
147 Err(EP::OtherBadDocument(
148 "authcert with vote structural keyword",
149 ))
150 } else if kw == "fingerprint" || kw.as_str().starts_with("dir-") {
151 Ok(None)
152 } else {
153 Err(EP::OtherBadDocument(
154 "authcert body keyword not dir- or fingerprint",
155 ))
156 }
157 }
158
159 fn finish(self) -> Result<(), EP> {
161 use ItemSequenceChecker::*;
162 match self {
163 End => Ok(()),
164 _other => Err(EP::OtherBadDocument(
165 "authcert missing end (signature) item",
166 )),
167 }
168 }
169}
170
171fn extra_lexical_checks(s: &str) -> Result<(), EP> {
176 let _without_trailing_nl = s
179 .strip_suffix("\n")
181 .ok_or(EP::OtherBadDocument("missing final newline"))?;
182
183 Ok(())
184}
185
186fn check(s: &str) -> Result<(), EP> {
188 extra_lexical_checks(s)?;
189
190 let input = ParseInput::new(s, "<authcert string>");
192 let mut lex = ItemStream::new(&input).map_err(|e| e.problem)?;
193 let mut seq = ItemSequenceChecker::start();
194 while let Some(item) = lex.next_item()? {
195 seq.keyword(item.keyword())?;
196 }
197 seq.finish()
198}
199
200impl TryFrom<String> for EncodedAuthCert {
201 type Error = ErrorProblem;
202 fn try_from(s: String) -> Result<Self, EP> {
203 check(&s)?;
204 Ok(EncodedAuthCert(s))
205 }
206}
207
208impl FromStr for EncodedAuthCert {
209 type Err = ErrorProblem;
210 fn from_str(s: &str) -> Result<Self, EP> {
211 s.to_owned().try_into()
212 }
213}
214
215impl NetdocParseable for EncodedAuthCert {
216 fn doctype_for_error() -> &'static str {
217 "encoded authority key certificate"
218 }
219
220 fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
221 kw == INTRO_KEYWORD
222 }
223 fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
224 (Self::is_intro_item_keyword(kw) || kw == FINAL_KEYWORD).then_some(IsStructural)
225 }
226
227 fn from_items(input: &mut ItemStream<'_>, stop_at: stop_at!()) -> Result<Self, EP> {
228 let start_pos = input.byte_position();
229 let mut seq = ItemSequenceChecker::start();
230 while seq != ItemSequenceChecker::End {
231 let item = input.next_item()?.ok_or(EP::MissingItem {
232 keyword: FINAL_KEYWORD,
233 })?;
234
235 let kw = item.keyword();
236
237 match seq.keyword(kw)? {
238 Some(IsOurStructural) => {} None => {
240 if stop_at.stop_at(kw) {
241 return Err(EP::Internal(
242 "bug! parent document structural keyword found while trying to process an embedded authcert, but was accepted by ItemSequenceChecker; authcert embedded in something other than a vote?",
243 ));
244 }
245 }
246 }
247 }
248 seq.finish()?;
249 let end_pos = input.byte_position();
250
251 let text = input
252 .whole_input()
253 .get(start_pos..end_pos)
254 .expect("start_pos wasn't included in the body so far?!");
255
256 extra_lexical_checks(text)?;
257
258 if let Some(next_item) = input.peek_keyword()? {
259 if !stop_at.stop_at(next_item) {
260 return Err(EP::OtherBadDocument(
261 "unexpected loose items after embedded authcert",
262 ));
263 }
264 }
265
266 Ok(EncodedAuthCert(text.to_string()))
267 }
268}
269
270impl NetdocEncodable for EncodedAuthCert {
271 fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
272 out.push_raw_string(&self.as_str());
274 Ok(())
275 }
276}
277
278#[cfg(test)]
279mod test {
280 #![allow(clippy::bool_assert_comparison)]
282 #![allow(clippy::clone_on_copy)]
283 #![allow(clippy::dbg_macro)]
284 #![allow(clippy::mixed_attributes_style)]
285 #![allow(clippy::print_stderr)]
286 #![allow(clippy::print_stdout)]
287 #![allow(clippy::single_char_pattern)]
288 #![allow(clippy::unwrap_used)]
289 #![allow(clippy::unchecked_time_subtraction)]
290 #![allow(clippy::useless_vec)]
291 #![allow(clippy::needless_pass_by_value)]
292 #![allow(clippy::string_slice)] use super::*;
295 use crate::parse2::parse_netdoc;
296 use derive_deftly::Deftly;
297 use std::fmt::{Debug, Display};
298
299 #[derive(Debug, Deftly)]
300 #[derive_deftly(NetdocParseable)]
301 #[allow(unused)]
302 struct Embeds {
303 e_intro: (),
304 #[deftly(netdoc(subdoc))]
305 cert: EncodedAuthCert,
306 #[deftly(netdoc(subdoc))]
307 subdocs: Vec<Subdoc>,
308 }
309 #[derive(Debug, Deftly)]
310 #[derive_deftly(NetdocParseable)]
311 #[allow(unused)]
312 struct Subdoc {
313 dir_e_subdoc: (),
314 }
315
316 fn chk(exp_sole: Result<(), &str>, exp_embed: Result<(), &str>, doc: &str) {
317 fn chk1<T: Debug, E: Debug + tor_error::ErrorReport + Display>(
318 exp: Result<(), &str>,
319 doc: &str,
320 what: &str,
321 got: Result<T, E>,
322 ) {
323 eprintln!("==========\n---- {what} 8<- ----\n{doc}---- ->8 {what} ----\n");
324 match got {
325 Err(got_e) => {
326 let got_m = got_e.report().to_string();
327 eprintln!("{what}, got error: {got_e:?}");
328 eprintln!("{what}, got error: {got_m:?}");
329 let exp_m = exp.expect_err("expected success!");
330 assert!(
331 got_m.contains(exp_m),
332 "{what}, expected different error: {exp_m:?}"
333 );
334 }
335 y @ Ok(_) => {
336 eprintln!("got {y:?}");
337 assert!(exp.is_ok(), "{what}, unexpected success; expected: {exp:?}");
338 }
339 }
340 }
341 chk1(exp_sole, doc, "from_str", EncodedAuthCert::from_str(doc));
342 chk1(
343 exp_sole,
344 doc,
345 "From<String>",
346 EncodedAuthCert::try_from(doc.to_owned()),
347 );
348 let embeds = format!(
349 r"e-intro
350ignored
351{doc}dir-e-subdoc
352dir-ignored-2
353"
354 );
355 let parse_input = ParseInput::new(&embeds, "<embeds>");
356 chk1(
357 exp_embed,
358 &embeds,
359 "embedded",
360 parse_netdoc::<Embeds>(&parse_input),
361 );
362 }
363
364 #[test]
365 fn bad_authcerts() {
366 NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword(
367 KeywordRef::new("dir-source").unwrap(),
368 )
369 .expect("structural dir-source");
370
371 chk(
376 Err("missing final newline"),
377 Err("missing item encoded authority key certificate"),
378 r"",
379 );
380 chk(
381 Err("authcert loose body item or missing intro keyword"),
382 Err("missing item encoded authority key certificate"),
383 r"wrong-intro
384",
385 );
386 chk(
387 Err("missing final newline"),
388 Err("missing item dir-key-certification"),
389 r"dir-key-certificate-version
390dir-missing-nl",
391 );
392 chk(
393 Err("authcert bad structure"),
394 Err("authcert bad structure"),
395 r"dir-key-certificate-version
396dir-key-certificate-version
397",
398 );
399 chk(
400 Err("authcert body keyword not dir- or fingerprint"),
401 Err("authcert body keyword not dir- or fingerprint"),
402 r"dir-key-certificate-version
403wrong-item
404dir-key-certification
405",
406 );
407 chk(
408 Err("authcert with vote structural keyword"),
409 Err("authcert with vote structural keyword"),
410 r"dir-key-certificate-version
411r
412dir-key-certification
413",
414 );
415 chk(
416 Err("authcert with vote structural keyword"),
417 Err("authcert with vote structural keyword"),
418 r"dir-key-certificate-version
419dir-source
420dir-key-certification
421",
422 );
423 chk(
424 Ok(()), Err("bug! parent document structural keyword found"),
426 r"dir-key-certificate-version
427dir-e-subdoc
428dir-key-certification
429",
430 );
431 chk(
432 Err("authcert with vote structural keyword"),
433 Err("authcert with vote structural keyword"),
434 r"dir-key-certificate-version
435dir-example-item
436r
437",
438 );
439 chk(
440 Err("authcert loose body item or missing intro keyword"),
441 Err("unexpected loose items after embedded authcert"),
442 r"dir-key-certificate-version
443dir-example-item
444dir-key-certification
445dir-extra-item
446r
447",
448 );
449 chk(
450 Err("authcert bad structure"),
451 Err("authcert bad structure"),
452 r"dir-key-certificate-version
453dir-key-certificate-version
454dir-example-item
455dir-key-certification
456dir-key-certification
457r
458",
459 );
460 chk(
461 Err("authcert bad structure"),
462 Err("unexpected loose items after embedded authcert"),
463 r"dir-key-certificate-version
464dir-example-item
465dir-key-certification
466dir-key-certification
467r
468",
469 );
470 }
471}