1use crate::batching_split_before::IteratorExt as _;
10use crate::parse::keyword::Keyword;
11use crate::parse::parser::{Section, SectionRules};
12use crate::parse::tokenize::{ItemResult, NetDocReader};
13use crate::types::misc::{Fingerprint, Iso8601TimeSp, RsaPublic};
14use crate::util::str::Extent;
15use crate::{NetdocErrorKind as EK, Result};
16
17use tor_checkable::{signed, timed};
18use tor_llcrypto::pk::rsa;
19use tor_llcrypto::{d, pk, pk::rsa::RsaIdentity};
20
21use once_cell::sync::Lazy;
22
23use std::{net, time};
24
25use digest::Digest;
26
27#[cfg(feature = "build_docs")]
28mod build;
29
30#[cfg(feature = "build_docs")]
31pub use build::AuthCertBuilder;
32
33decl_keyword! {
34 pub(crate) AuthCertKwd {
35 "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
36 "dir-address" => DIR_ADDRESS,
37 "fingerprint" => FINGERPRINT,
38 "dir-identity-key" => DIR_IDENTITY_KEY,
39 "dir-key-published" => DIR_KEY_PUBLISHED,
40 "dir-key-expires" => DIR_KEY_EXPIRES,
41 "dir-signing-key" => DIR_SIGNING_KEY,
42 "dir-key-crosscert" => DIR_KEY_CROSSCERT,
43 "dir-key-certification" => DIR_KEY_CERTIFICATION,
44 }
45}
46
47static AUTHCERT_RULES: Lazy<SectionRules<AuthCertKwd>> = Lazy::new(|| {
50 use AuthCertKwd::*;
51
52 let mut rules = SectionRules::builder();
53 rules.add(DIR_KEY_CERTIFICATE_VERSION.rule().required().args(1..));
54 rules.add(DIR_ADDRESS.rule().args(1..));
55 rules.add(FINGERPRINT.rule().required().args(1..));
56 rules.add(DIR_IDENTITY_KEY.rule().required().no_args().obj_required());
57 rules.add(DIR_SIGNING_KEY.rule().required().no_args().obj_required());
58 rules.add(DIR_KEY_PUBLISHED.rule().required());
59 rules.add(DIR_KEY_EXPIRES.rule().required());
60 rules.add(DIR_KEY_CROSSCERT.rule().required().no_args().obj_required());
61 rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
62 rules.add(
63 DIR_KEY_CERTIFICATION
64 .rule()
65 .required()
66 .no_args()
67 .obj_required(),
68 );
69 rules.build()
70});
71
72#[allow(dead_code)]
79#[derive(Clone, Debug)]
80pub struct AuthCert {
81 address: Option<net::SocketAddrV4>,
83 identity_key: rsa::PublicKey,
85 signing_key: rsa::PublicKey,
87 published: time::SystemTime,
89 expires: time::SystemTime,
91
92 key_ids: AuthCertKeyIds,
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
98#[allow(clippy::exhaustive_structs)]
99pub struct AuthCertKeyIds {
100 pub id_fingerprint: rsa::RsaIdentity,
102 pub sk_fingerprint: rsa::RsaIdentity,
104}
105
106pub struct UncheckedAuthCert {
109 location: Option<Extent>,
111
112 c: signed::SignatureGated<timed::TimerangeBound<AuthCert>>,
114}
115
116impl UncheckedAuthCert {
117 pub fn within<'a>(&self, haystack: &'a str) -> Option<&'a str> {
125 self.location
126 .as_ref()
127 .and_then(|ext| ext.reconstruct(haystack))
128 }
129}
130
131impl AuthCert {
132 #[cfg(feature = "build_docs")]
135 pub fn builder() -> AuthCertBuilder {
136 AuthCertBuilder::new()
137 }
138
139 pub fn parse(s: &str) -> Result<UncheckedAuthCert> {
144 let mut reader = NetDocReader::new(s)?;
145 let body = AUTHCERT_RULES.parse(&mut reader)?;
146 reader.should_be_exhausted()?;
147 AuthCert::from_body(&body, s).map_err(|e| e.within(s))
148 }
149
150 pub fn parse_multiple(s: &str) -> Result<impl Iterator<Item = Result<UncheckedAuthCert>> + '_> {
152 use AuthCertKwd::*;
153 let sections = NetDocReader::new(s)?
154 .batching_split_before_loose(|item| item.is_ok_with_kwd(DIR_KEY_CERTIFICATE_VERSION));
155 Ok(sections
156 .map(|mut section| {
157 let body = AUTHCERT_RULES.parse(&mut section)?;
158 AuthCert::from_body(&body, s)
159 })
160 .map(|r| r.map_err(|e| e.within(s))))
161 }
162 pub fn signing_key(&self) -> &rsa::PublicKey {
171 &self.signing_key
172 }
173
174 pub fn key_ids(&self) -> &AuthCertKeyIds {
177 &self.key_ids
178 }
179
180 pub fn id_fingerprint(&self) -> &rsa::RsaIdentity {
182 &self.key_ids.id_fingerprint
183 }
184
185 pub fn sk_fingerprint(&self) -> &rsa::RsaIdentity {
187 &self.key_ids.sk_fingerprint
188 }
189
190 pub fn published(&self) -> time::SystemTime {
192 self.published
193 }
194
195 pub fn expires(&self) -> time::SystemTime {
197 self.expires
198 }
199
200 fn from_body(body: &Section<'_, AuthCertKwd>, s: &str) -> Result<UncheckedAuthCert> {
202 use AuthCertKwd::*;
203
204 let start_pos = {
209 #[allow(clippy::unwrap_used)]
212 let first_item = body.first_item().unwrap();
213 if first_item.kwd() != DIR_KEY_CERTIFICATE_VERSION {
214 return Err(EK::WrongStartingToken
215 .with_msg(first_item.kwd_str().to_string())
216 .at_pos(first_item.pos()));
217 }
218 first_item.pos()
219 };
220 let end_pos = {
221 #[allow(clippy::unwrap_used)]
224 let last_item = body.last_item().unwrap();
225 if last_item.kwd() != DIR_KEY_CERTIFICATION {
226 return Err(EK::WrongEndingToken
227 .with_msg(last_item.kwd_str().to_string())
228 .at_pos(last_item.pos()));
229 }
230 last_item.end_pos()
231 };
232
233 let version = body
234 .required(DIR_KEY_CERTIFICATE_VERSION)?
235 .parse_arg::<u32>(0)?;
236 if version != 3 {
237 return Err(EK::BadDocumentVersion.with_msg(format!("unexpected version {}", version)));
238 }
239
240 let signing_key: rsa::PublicKey = body
241 .required(DIR_SIGNING_KEY)?
242 .parse_obj::<RsaPublic>("RSA PUBLIC KEY")?
243 .check_len(1024..)?
244 .check_exponent(65537)?
245 .into();
246
247 let identity_key: rsa::PublicKey = body
248 .required(DIR_IDENTITY_KEY)?
249 .parse_obj::<RsaPublic>("RSA PUBLIC KEY")?
250 .check_len(1024..)?
251 .check_exponent(65537)?
252 .into();
253
254 let published = body
255 .required(DIR_KEY_PUBLISHED)?
256 .args_as_str()
257 .parse::<Iso8601TimeSp>()?
258 .into();
259
260 let expires = body
261 .required(DIR_KEY_EXPIRES)?
262 .args_as_str()
263 .parse::<Iso8601TimeSp>()?
264 .into();
265
266 {
267 let fp_tok = body.required(FINGERPRINT)?;
269 let fingerprint: RsaIdentity = fp_tok.args_as_str().parse::<Fingerprint>()?.into();
270 if fingerprint != identity_key.to_rsa_identity() {
271 return Err(EK::BadArgument
272 .at_pos(fp_tok.pos())
273 .with_msg("fingerprint does not match RSA identity"));
274 }
275 }
276
277 let address = body
278 .maybe(DIR_ADDRESS)
279 .parse_args_as_str::<net::SocketAddrV4>()?;
280
281 let v_crosscert = {
283 let crosscert = body.required(DIR_KEY_CROSSCERT)?;
284 #[allow(clippy::unwrap_used)]
287 let mut tag = crosscert.obj_tag().unwrap();
288 if tag != "ID SIGNATURE" && tag != "SIGNATURE" {
290 tag = "ID SIGNATURE";
291 }
292 let sig = crosscert.obj(tag)?;
293
294 let signed = identity_key.to_rsa_identity();
295 rsa::ValidatableRsaSignature::new(&signing_key, &sig, signed.as_bytes())
298 };
299
300 let v_sig = {
302 let signature = body.required(DIR_KEY_CERTIFICATION)?;
303 let sig = signature.obj("SIGNATURE")?;
304
305 let mut sha1 = d::Sha1::new();
306 #[allow(clippy::unwrap_used)]
309 let start_offset = body.first_item().unwrap().offset_in(s).unwrap();
310 #[allow(clippy::unwrap_used)]
311 let end_offset = body.last_item().unwrap().offset_in(s).unwrap();
312 let end_offset = end_offset + "dir-key-certification\n".len();
313 sha1.update(&s[start_offset..end_offset]);
314 let sha1 = sha1.finalize();
315 rsa::ValidatableRsaSignature::new(&identity_key, &sig, &sha1)
318 };
319
320 let id_fingerprint = identity_key.to_rsa_identity();
321 let sk_fingerprint = signing_key.to_rsa_identity();
322 let key_ids = AuthCertKeyIds {
323 id_fingerprint,
324 sk_fingerprint,
325 };
326
327 let location = {
328 let start_idx = start_pos.offset_within(s);
329 let end_idx = end_pos.offset_within(s);
330 match (start_idx, end_idx) {
331 (Some(a), Some(b)) => Extent::new(s, &s[a..b + 1]),
332 _ => None,
333 }
334 };
335
336 let authcert = AuthCert {
337 address,
338 identity_key,
339 signing_key,
340 published,
341 expires,
342 key_ids,
343 };
344
345 let signatures: Vec<Box<dyn pk::ValidatableSignature>> =
346 vec![Box::new(v_crosscert), Box::new(v_sig)];
347
348 let timed = timed::TimerangeBound::new(authcert, published..expires);
349 let signed = signed::SignatureGated::new(timed, signatures);
350 let unchecked = UncheckedAuthCert {
351 location,
352 c: signed,
353 };
354 Ok(unchecked)
355 }
356}
357
358impl tor_checkable::SelfSigned<timed::TimerangeBound<AuthCert>> for UncheckedAuthCert {
359 type Error = signature::Error;
360
361 fn dangerously_assume_wellsigned(self) -> timed::TimerangeBound<AuthCert> {
362 self.c.dangerously_assume_wellsigned()
363 }
364 fn is_well_signed(&self) -> std::result::Result<(), Self::Error> {
365 self.c.is_well_signed()
366 }
367}
368
369#[cfg(test)]
370mod test {
371 #![allow(clippy::bool_assert_comparison)]
373 #![allow(clippy::clone_on_copy)]
374 #![allow(clippy::dbg_macro)]
375 #![allow(clippy::mixed_attributes_style)]
376 #![allow(clippy::print_stderr)]
377 #![allow(clippy::print_stdout)]
378 #![allow(clippy::single_char_pattern)]
379 #![allow(clippy::unwrap_used)]
380 #![allow(clippy::unchecked_duration_subtraction)]
381 #![allow(clippy::useless_vec)]
382 #![allow(clippy::needless_pass_by_value)]
383 use super::*;
385 use crate::{Error, Pos};
386 const TESTDATA: &str = include_str!("../../testdata/authcert1.txt");
387
388 fn bad_data(fname: &str) -> String {
389 use std::fs;
390 use std::path::PathBuf;
391 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
392 path.push("testdata");
393 path.push("bad-certs");
394 path.push(fname);
395
396 fs::read_to_string(path).unwrap()
397 }
398
399 #[test]
400 fn parse_one() -> Result<()> {
401 use tor_checkable::{SelfSigned, Timebound};
402 let cert = AuthCert::parse(TESTDATA)?
403 .check_signature()
404 .unwrap()
405 .dangerously_assume_timely();
406
407 assert_eq!(
409 cert.id_fingerprint().to_string(),
410 "$ed03bb616eb2f60bec80151114bb25cef515b226"
411 );
412 assert_eq!(
413 cert.sk_fingerprint().to_string(),
414 "$c4f720e2c59f9ddd4867fff465ca04031e35648f"
415 );
416
417 Ok(())
418 }
419
420 #[test]
421 fn parse_bad() {
422 fn check(fname: &str, err: &Error) {
423 let contents = bad_data(fname);
424 let cert = AuthCert::parse(&contents);
425 assert!(cert.is_err());
426 assert_eq!(&cert.err().unwrap(), err);
427 }
428
429 check(
430 "bad-cc-tag",
431 &EK::WrongObject.at_pos(Pos::from_line(27, 12)),
432 );
433 check(
434 "bad-fingerprint",
435 &EK::BadArgument
436 .at_pos(Pos::from_line(2, 1))
437 .with_msg("fingerprint does not match RSA identity"),
438 );
439 check(
440 "bad-version",
441 &EK::BadDocumentVersion.with_msg("unexpected version 4"),
442 );
443 check(
444 "wrong-end",
445 &EK::WrongEndingToken
446 .with_msg("dir-key-crosscert")
447 .at_pos(Pos::from_line(37, 1)),
448 );
449 check(
450 "wrong-start",
451 &EK::WrongStartingToken
452 .with_msg("fingerprint")
453 .at_pos(Pos::from_line(1, 1)),
454 );
455 }
456
457 #[test]
458 fn test_recovery_1() {
459 let mut data = "<><><<><>\nfingerprint ABC\n".to_string();
460 data += TESTDATA;
461
462 let res: Vec<Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
463
464 assert!(res[0].is_err());
466 assert!(res[1].is_ok());
467 assert_eq!(res.len(), 2);
468 }
469
470 #[test]
471 fn test_recovery_2() {
472 let mut data = bad_data("bad-version");
473 data += TESTDATA;
474
475 let res: Vec<Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
476
477 assert!(res[0].is_err());
479 assert!(res[1].is_ok());
480 assert_eq!(res.len(), 2);
481 }
482}