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, RsaPublicParse1Helper};
14use crate::util::str::Extent;
15use crate::{NetdocErrorKind as EK, NormalItemArgument, Result};
16
17use tor_checkable::{signed, timed};
18use tor_llcrypto::pk::rsa;
19use tor_llcrypto::{d, pk, pk::rsa::RsaIdentity};
20
21use std::sync::LazyLock;
22
23use std::result::Result as StdResult;
24use std::{net, time, time::Duration, time::SystemTime};
25
26use derive_deftly::Deftly;
27use digest::Digest;
28
29#[cfg(feature = "build_docs")]
30mod build;
31
32#[cfg(feature = "build_docs")]
33pub use build::AuthCertBuilder;
34
35#[cfg(feature = "parse2")]
36use crate::parse2::{self, ItemObjectParseable, SignatureHashInputs};
37
38decl_keyword! {
39 pub(crate) AuthCertKwd {
40 "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
41 "dir-address" => DIR_ADDRESS,
42 "fingerprint" => FINGERPRINT,
43 "dir-identity-key" => DIR_IDENTITY_KEY,
44 "dir-key-published" => DIR_KEY_PUBLISHED,
45 "dir-key-expires" => DIR_KEY_EXPIRES,
46 "dir-signing-key" => DIR_SIGNING_KEY,
47 "dir-key-crosscert" => DIR_KEY_CROSSCERT,
48 "dir-key-certification" => DIR_KEY_CERTIFICATION,
49 }
50}
51
52static AUTHCERT_RULES: LazyLock<SectionRules<AuthCertKwd>> = LazyLock::new(|| {
55 use AuthCertKwd::*;
56
57 let mut rules = SectionRules::builder();
58 rules.add(DIR_KEY_CERTIFICATE_VERSION.rule().required().args(1..));
59 rules.add(DIR_ADDRESS.rule().args(1..));
60 rules.add(FINGERPRINT.rule().required().args(1..));
61 rules.add(DIR_IDENTITY_KEY.rule().required().no_args().obj_required());
62 rules.add(DIR_SIGNING_KEY.rule().required().no_args().obj_required());
63 rules.add(DIR_KEY_PUBLISHED.rule().required());
64 rules.add(DIR_KEY_EXPIRES.rule().required());
65 rules.add(DIR_KEY_CROSSCERT.rule().required().no_args().obj_required());
66 rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
67 rules.add(
68 DIR_KEY_CERTIFICATION
69 .rule()
70 .required()
71 .no_args()
72 .obj_required(),
73 );
74 rules.build()
75});
76
77#[derive(Clone, Debug, Deftly)]
83#[cfg_attr(feature = "parse2", derive_deftly(NetdocParseable, NetdocSigned))]
84#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
86#[cfg_attr(test, derive(PartialEq, Eq))]
87#[non_exhaustive]
88pub struct AuthCert {
91 #[deftly(netdoc(single_arg))]
97 pub dir_key_certificate_version: AuthCertVersion,
98
99 #[deftly(netdoc(single_arg))]
101 pub dir_address: Option<net::SocketAddrV4>,
102
103 #[deftly(netdoc(single_arg))]
105 pub fingerprint: Fingerprint,
108
109 #[deftly(netdoc(single_arg))]
113 pub dir_key_published: Iso8601TimeSp,
114
115 #[deftly(netdoc(single_arg))]
119 pub dir_key_expires: Iso8601TimeSp,
120
121 pub dir_identity_key: rsa::PublicKey,
127
128 pub dir_signing_key: rsa::PublicKey,
134
135 pub dir_key_crosscert: CrossCert,
139}
140
141#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, strum::EnumString, strum::Display)]
147#[non_exhaustive]
148pub enum AuthCertVersion {
149 #[strum(serialize = "3")]
151 V3,
152}
153
154impl NormalItemArgument for AuthCertVersion {}
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
158#[allow(clippy::exhaustive_structs)]
159pub struct AuthCertKeyIds {
160 pub id_fingerprint: rsa::RsaIdentity,
162 pub sk_fingerprint: rsa::RsaIdentity,
164}
165
166pub struct UncheckedAuthCert {
169 location: Option<Extent>,
171
172 c: signed::SignatureGated<timed::TimerangeBound<AuthCert>>,
174}
175
176impl UncheckedAuthCert {
177 pub fn within<'a>(&self, haystack: &'a str) -> Option<&'a str> {
185 self.location
186 .as_ref()
187 .and_then(|ext| ext.reconstruct(haystack))
188 }
189}
190
191impl AuthCert {
192 #[cfg(feature = "build_docs")]
195 pub fn builder() -> AuthCertBuilder {
196 AuthCertBuilder::new()
197 }
198
199 pub fn parse(s: &str) -> Result<UncheckedAuthCert> {
204 let mut reader = NetDocReader::new(s)?;
205 let body = AUTHCERT_RULES.parse(&mut reader)?;
206 reader.should_be_exhausted()?;
207 AuthCert::from_body(&body, s).map_err(|e| e.within(s))
208 }
209
210 pub fn parse_multiple(s: &str) -> Result<impl Iterator<Item = Result<UncheckedAuthCert>> + '_> {
212 use AuthCertKwd::*;
213 let sections = NetDocReader::new(s)?
214 .batching_split_before_loose(|item| item.is_ok_with_kwd(DIR_KEY_CERTIFICATE_VERSION));
215 Ok(sections
216 .map(|mut section| {
217 let body = AUTHCERT_RULES.parse(&mut section)?;
218 AuthCert::from_body(&body, s)
219 })
220 .map(|r| r.map_err(|e| e.within(s))))
221 }
222 pub fn signing_key(&self) -> &rsa::PublicKey {
231 &self.dir_signing_key
232 }
233
234 pub fn key_ids(&self) -> AuthCertKeyIds {
237 AuthCertKeyIds {
238 id_fingerprint: self.fingerprint.0,
239 sk_fingerprint: self.dir_signing_key.to_rsa_identity(),
240 }
241 }
242
243 pub fn id_fingerprint(&self) -> &rsa::RsaIdentity {
245 &self.fingerprint
246 }
247
248 pub fn published(&self) -> time::SystemTime {
250 *self.dir_key_published
251 }
252
253 pub fn expires(&self) -> time::SystemTime {
255 *self.dir_key_expires
256 }
257
258 fn from_body(body: &Section<'_, AuthCertKwd>, s: &str) -> Result<UncheckedAuthCert> {
260 use AuthCertKwd::*;
261
262 let start_pos = {
267 #[allow(clippy::unwrap_used)]
270 let first_item = body.first_item().unwrap();
271 if first_item.kwd() != DIR_KEY_CERTIFICATE_VERSION {
272 return Err(EK::WrongStartingToken
273 .with_msg(first_item.kwd_str().to_string())
274 .at_pos(first_item.pos()));
275 }
276 first_item.pos()
277 };
278 let end_pos = {
279 #[allow(clippy::unwrap_used)]
282 let last_item = body.last_item().unwrap();
283 if last_item.kwd() != DIR_KEY_CERTIFICATION {
284 return Err(EK::WrongEndingToken
285 .with_msg(last_item.kwd_str().to_string())
286 .at_pos(last_item.pos()));
287 }
288 last_item.end_pos()
289 };
290
291 let version = body
292 .required(DIR_KEY_CERTIFICATE_VERSION)?
293 .parse_arg::<u32>(0)?;
294 if version != 3 {
295 return Err(EK::BadDocumentVersion.with_msg(format!("unexpected version {}", version)));
296 }
297 let dir_key_certificate_version = AuthCertVersion::V3;
298
299 let dir_signing_key: rsa::PublicKey = body
300 .required(DIR_SIGNING_KEY)?
301 .parse_obj::<RsaPublicParse1Helper>("RSA PUBLIC KEY")?
302 .check_len(1024..)?
303 .check_exponent(65537)?
304 .into();
305
306 let dir_identity_key: rsa::PublicKey = body
307 .required(DIR_IDENTITY_KEY)?
308 .parse_obj::<RsaPublicParse1Helper>("RSA PUBLIC KEY")?
309 .check_len(1024..)?
310 .check_exponent(65537)?
311 .into();
312
313 let dir_key_published = body
314 .required(DIR_KEY_PUBLISHED)?
315 .args_as_str()
316 .parse::<Iso8601TimeSp>()?;
317
318 let dir_key_expires = body
319 .required(DIR_KEY_EXPIRES)?
320 .args_as_str()
321 .parse::<Iso8601TimeSp>()?;
322
323 {
324 let fp_tok = body.required(FINGERPRINT)?;
326 let fingerprint: RsaIdentity = fp_tok.args_as_str().parse::<Fingerprint>()?.into();
327 if fingerprint != dir_identity_key.to_rsa_identity() {
328 return Err(EK::BadArgument
329 .at_pos(fp_tok.pos())
330 .with_msg("fingerprint does not match RSA identity"));
331 }
332 }
333
334 let dir_address = body
335 .maybe(DIR_ADDRESS)
336 .parse_args_as_str::<net::SocketAddrV4>()?;
337
338 let dir_key_crosscert;
340 let v_crosscert = {
341 let crosscert = body.required(DIR_KEY_CROSSCERT)?;
342 #[allow(clippy::unwrap_used)]
345 let mut tag = crosscert.obj_tag().unwrap();
346 if tag != "ID SIGNATURE" && tag != "SIGNATURE" {
348 tag = "ID SIGNATURE";
349 }
350 let sig = crosscert.obj(tag)?;
351
352 let signed = dir_identity_key.to_rsa_identity();
353 let v = rsa::ValidatableRsaSignature::new(&dir_signing_key, &sig, signed.as_bytes());
356
357 dir_key_crosscert = CrossCert {
358 signature: CrossCertObject(sig),
359 };
360
361 v
362 };
363
364 let v_sig = {
366 let signature = body.required(DIR_KEY_CERTIFICATION)?;
367 let sig = signature.obj("SIGNATURE")?;
368
369 let mut sha1 = d::Sha1::new();
370 #[allow(clippy::unwrap_used)]
373 let start_offset = body.first_item().unwrap().offset_in(s).unwrap();
374 #[allow(clippy::unwrap_used)]
375 let end_offset = body.last_item().unwrap().offset_in(s).unwrap();
376 let end_offset = end_offset + "dir-key-certification\n".len();
377 sha1.update(&s[start_offset..end_offset]);
378 let sha1 = sha1.finalize();
379 rsa::ValidatableRsaSignature::new(&dir_identity_key, &sig, &sha1)
382 };
383
384 let id_fingerprint = dir_identity_key.to_rsa_identity();
385
386 let location = {
387 let start_idx = start_pos.offset_within(s);
388 let end_idx = end_pos.offset_within(s);
389 match (start_idx, end_idx) {
390 (Some(a), Some(b)) => Extent::new(s, &s[a..b + 1]),
391 _ => None,
392 }
393 };
394
395 let authcert = AuthCert {
396 dir_key_certificate_version,
397 dir_address,
398 dir_identity_key,
399 dir_signing_key,
400 dir_key_published,
401 dir_key_expires,
402 dir_key_crosscert,
403 fingerprint: Fingerprint(id_fingerprint),
404 };
405
406 let signatures: Vec<Box<dyn pk::ValidatableSignature>> =
407 vec![Box::new(v_crosscert), Box::new(v_sig)];
408
409 let timed = timed::TimerangeBound::new(authcert, *dir_key_published..*dir_key_expires);
410 let signed = signed::SignatureGated::new(timed, signatures);
411 let unchecked = UncheckedAuthCert {
412 location,
413 c: signed,
414 };
415 Ok(unchecked)
416 }
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
433#[cfg_attr(
434 feature = "parse2",
435 derive_deftly(ItemValueParseable),
436 deftly(netdoc(no_extra_args))
437)]
438#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
440#[non_exhaustive]
441pub struct CrossCert {
442 #[deftly(netdoc(object))]
444 pub signature: CrossCertObject,
445}
446
447#[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)]
466#[non_exhaustive]
467pub struct CrossCertObject(pub Vec<u8>);
468
469#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
479#[cfg_attr(
480 feature = "parse2",
481 derive_deftly(NetdocParseable),
482 deftly(netdoc(signatures))
483)]
484#[non_exhaustive]
485pub struct AuthCertSignatures {
486 pub dir_key_certification: AuthCertSignature,
488}
489
490#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
500#[cfg_attr(
501 feature = "parse2",
502 derive_deftly(ItemValueParseable),
503 deftly(netdoc(no_extra_args))
504)]
505#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
507#[non_exhaustive]
508pub struct AuthCertSignature {
509 #[deftly(netdoc(object(label = "SIGNATURE"), with = "crate::parse2::raw_data_object"))]
511 pub signature: Vec<u8>,
512
513 #[deftly(netdoc(sig_hash = "whole_keyword_line_sha1"))]
515 pub hash: [u8; 20],
516}
517
518#[cfg(feature = "parse2")]
519impl ItemObjectParseable for CrossCertObject {
520 fn check_label(label: &str) -> StdResult<(), parse2::EP> {
521 match label {
522 "SIGNATURE" | "ID SIGNATURE" => Ok(()),
523 _ => Err(parse2::EP::ObjectIncorrectLabel),
524 }
525 }
526
527 fn from_bytes(input: &[u8]) -> StdResult<Self, parse2::EP> {
528 Ok(Self(input.to_vec()))
529 }
530}
531
532impl tor_checkable::SelfSigned<timed::TimerangeBound<AuthCert>> for UncheckedAuthCert {
533 type Error = signature::Error;
534
535 fn dangerously_assume_wellsigned(self) -> timed::TimerangeBound<AuthCert> {
536 self.c.dangerously_assume_wellsigned()
537 }
538 fn is_well_signed(&self) -> std::result::Result<(), Self::Error> {
539 self.c.is_well_signed()
540 }
541}
542
543#[cfg(feature = "parse2")]
544impl AuthCertSigned {
545 pub fn verify_self_signed(
563 self,
564 v3idents: &[RsaIdentity],
565 pre_tolerance: Duration,
566 post_tolerance: Duration,
567 now: SystemTime,
568 ) -> StdResult<AuthCert, parse2::VerifyFailed> {
569 let (body, signatures) = (self.body, self.signatures);
570
571 if !v3idents.contains(&body.fingerprint.0) {
573 return Err(parse2::VerifyFailed::InsufficientTrustedSigners);
574 }
575
576 let validity = *body.dir_key_published..=*body.dir_key_expires;
578 parse2::check_validity_time_tolerance(now, validity, pre_tolerance, post_tolerance)?;
579
580 if body.dir_identity_key.to_rsa_identity() != *body.fingerprint {
582 return Err(parse2::VerifyFailed::Inconsistent);
583 }
584
585 body.dir_signing_key.verify(
587 body.fingerprint.0.as_bytes(),
588 &body.dir_key_crosscert.signature,
589 )?;
590
591 body.dir_identity_key.verify(
593 &signatures.dir_key_certification.hash,
594 &signatures.dir_key_certification.signature,
595 )?;
596
597 Ok(body)
598 }
599}
600
601#[cfg(test)]
602mod test {
603 #![allow(clippy::bool_assert_comparison)]
605 #![allow(clippy::clone_on_copy)]
606 #![allow(clippy::dbg_macro)]
607 #![allow(clippy::mixed_attributes_style)]
608 #![allow(clippy::print_stderr)]
609 #![allow(clippy::print_stdout)]
610 #![allow(clippy::single_char_pattern)]
611 #![allow(clippy::unwrap_used)]
612 #![allow(clippy::unchecked_time_subtraction)]
613 #![allow(clippy::useless_vec)]
614 #![allow(clippy::needless_pass_by_value)]
615 use super::*;
617 use crate::{Error, Pos};
618 const TESTDATA: &str = include_str!("../../testdata/authcert1.txt");
619
620 fn bad_data(fname: &str) -> String {
621 use std::fs;
622 use std::path::PathBuf;
623 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
624 path.push("testdata");
625 path.push("bad-certs");
626 path.push(fname);
627
628 fs::read_to_string(path).unwrap()
629 }
630
631 #[test]
632 fn parse_one() -> Result<()> {
633 use tor_checkable::{SelfSigned, Timebound};
634 let cert = AuthCert::parse(TESTDATA)?
635 .check_signature()
636 .unwrap()
637 .dangerously_assume_timely();
638
639 assert_eq!(
641 cert.id_fingerprint().to_string(),
642 "$ed03bb616eb2f60bec80151114bb25cef515b226"
643 );
644 assert_eq!(
645 cert.key_ids().sk_fingerprint.to_string(),
646 "$c4f720e2c59f9ddd4867fff465ca04031e35648f"
647 );
648
649 Ok(())
650 }
651
652 #[test]
653 fn parse_bad() {
654 fn check(fname: &str, err: &Error) {
655 let contents = bad_data(fname);
656 let cert = AuthCert::parse(&contents);
657 assert!(cert.is_err());
658 assert_eq!(&cert.err().unwrap(), err);
659 }
660
661 check(
662 "bad-cc-tag",
663 &EK::WrongObject.at_pos(Pos::from_line(27, 12)),
664 );
665 check(
666 "bad-fingerprint",
667 &EK::BadArgument
668 .at_pos(Pos::from_line(2, 1))
669 .with_msg("fingerprint does not match RSA identity"),
670 );
671 check(
672 "bad-version",
673 &EK::BadDocumentVersion.with_msg("unexpected version 4"),
674 );
675 check(
676 "wrong-end",
677 &EK::WrongEndingToken
678 .with_msg("dir-key-crosscert")
679 .at_pos(Pos::from_line(37, 1)),
680 );
681 check(
682 "wrong-start",
683 &EK::WrongStartingToken
684 .with_msg("fingerprint")
685 .at_pos(Pos::from_line(1, 1)),
686 );
687 }
688
689 #[test]
690 fn test_recovery_1() {
691 let mut data = "<><><<><>\nfingerprint ABC\n".to_string();
692 data += TESTDATA;
693
694 let res: Vec<Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
695
696 assert!(res[0].is_err());
698 assert!(res[1].is_ok());
699 assert_eq!(res.len(), 2);
700 }
701
702 #[test]
703 fn test_recovery_2() {
704 let mut data = bad_data("bad-version");
705 data += TESTDATA;
706
707 let res: Vec<Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
708
709 assert!(res[0].is_err());
711 assert!(res[1].is_ok());
712 assert_eq!(res.len(), 2);
713 }
714
715 #[cfg(feature = "parse2")]
716 mod parse2_test {
717 use super::{
718 AuthCert, AuthCertSignature, AuthCertSignatures, AuthCertSigned, AuthCertVersion,
719 CrossCert, CrossCertObject,
720 };
721
722 use std::{
723 fs::File,
724 io::Read,
725 path::Path,
726 str::FromStr,
727 time::{Duration, SystemTime},
728 };
729
730 use crate::{
731 parse2::{self, ErrorProblem, ParseError, ParseInput, VerifyFailed},
732 types::{self, Iso8601TimeSp},
733 };
734
735 use base64ct::{Base64, Encoding};
736 use derive_deftly::Deftly;
737 use digest::Digest;
738 use tor_llcrypto::{
739 d::Sha1,
740 pk::rsa::{self, RsaIdentity},
741 };
742
743 fn read_b64<P: AsRef<Path>>(path: P) -> (String, Vec<u8>) {
745 let mut encoded = String::new();
746 File::open(path)
747 .unwrap()
748 .read_to_string(&mut encoded)
749 .unwrap();
750 let mut decoded = Vec::new();
751 base64ct::Decoder::<Base64>::new_wrapped(encoded.as_bytes(), 64)
752 .unwrap()
753 .decode_to_end(&mut decoded)
754 .unwrap();
755
756 (encoded, decoded)
757 }
758
759 fn to_der(s: &str) -> Vec<u8> {
761 let mut r = Vec::new();
762 for line in s.lines() {
763 r.extend(Base64::decode_vec(line).unwrap());
764 }
765 r
766 }
767
768 #[test]
770 fn dir_auth_cross_cert() {
771 #[derive(Debug, Clone, PartialEq, Eq, Deftly)]
772 #[derive_deftly(NetdocParseable)]
773 struct Dummy {
774 dir_key_crosscert: CrossCert,
775 }
776
777 let (encoded, decoded) = read_b64("testdata2/authcert-longclaw-crosscert-b64");
778
779 let cert = format!(
781 "dir-key-crosscert\n-----BEGIN SIGNATURE-----\n{encoded}\n-----END SIGNATURE-----"
782 );
783 let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, "")).unwrap();
784 assert_eq!(
785 res,
786 Dummy {
787 dir_key_crosscert: CrossCert {
788 signature: CrossCertObject(decoded.clone())
789 }
790 }
791 );
792
793 let cert = format!(
795 "dir-key-crosscert\n-----BEGIN ID SIGNATURE-----\n{encoded}\n-----END ID SIGNATURE-----"
796 );
797 let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, "")).unwrap();
798 assert_eq!(
799 res,
800 Dummy {
801 dir_key_crosscert: CrossCert {
802 signature: CrossCertObject(decoded.clone())
803 }
804 }
805 );
806
807 let cert =
809 format!("dir-key-crosscert\n-----BEGIN WHAT-----\n{encoded}\n-----END WHAT-----");
810 let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, ""));
811 match res {
812 Err(ParseError {
813 problem: ErrorProblem::ObjectIncorrectLabel,
814 doctype: "dir-key-crosscert",
815 file: _,
816 lno: 1,
817 column: None,
818 }) => {}
819 other => panic!("not expected error {other:#?}"),
820 }
821
822 let cert = format!(
824 "dir-key-crosscert arg1\n-----BEGIN ID SIGNATURE-----\n{encoded}\n-----END ID SIGNATURE-----"
825 );
826 let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, ""));
827 match res {
828 Err(ParseError {
829 problem: ErrorProblem::UnexpectedArgument { column: 19 },
830 doctype: "dir-key-crosscert",
831 file: _,
832 lno: 1,
833 column: Some(19),
834 }) => {}
835 other => panic!("not expected error {other:#?}"),
836 }
837 }
838
839 #[test]
840 fn dir_auth_key_cert_signatures() {
841 let (encoded, decoded) = read_b64("testdata2/authcert-longclaw-signature-b64");
842 let cert = format!(
843 "dir-key-certification\n-----BEGIN SIGNATURE-----\n{encoded}\n-----END SIGNATURE-----"
844 );
845 let hash: [u8; 20] = Sha1::digest("dir-key-certification\n").into();
846
847 let res =
848 parse2::parse_netdoc::<AuthCertSignatures>(&ParseInput::new(&cert, "")).unwrap();
849 assert_eq!(
850 res,
851 AuthCertSignatures {
852 dir_key_certification: AuthCertSignature {
853 signature: decoded.clone(),
854 hash
855 }
856 }
857 );
858
859 let cert = format!(
861 "dir-key-certification\n-----BEGIN ID SIGNATURE-----\n{encoded}\n-----END ID SIGNATURE-----"
862 );
863 let res = parse2::parse_netdoc::<AuthCertSignatures>(&ParseInput::new(&cert, ""));
864 match res {
865 Err(ParseError {
866 problem: ErrorProblem::ObjectIncorrectLabel,
867 doctype: "",
868 file: _,
869 lno: 1,
870 column: None,
871 }) => {}
872 other => panic!("not expected error {other:#?}"),
873 }
874
875 let cert = format!(
877 "dir-key-certification arg1\n-----BEGIN SIGNATURE-----\n{encoded}\n-----END SIGNATURE-----"
878 );
879 let res = parse2::parse_netdoc::<AuthCertSignatures>(&ParseInput::new(&cert, ""));
880 match res {
881 Err(ParseError {
882 problem: ErrorProblem::UnexpectedArgument { column: 23 },
883 doctype: "",
884 file: _,
885 lno: 1,
886 column: Some(23),
887 }) => {}
888 other => panic!("not expected error {other:#?}"),
889 }
890 }
891
892 #[test]
893 fn dir_auth_cert() {
894 let mut input = String::new();
897 File::open("testdata2/authcert-longclaw-full")
898 .unwrap()
899 .read_to_string(&mut input)
900 .unwrap();
901
902 let res = parse2::parse_netdoc::<AuthCert>(&ParseInput::new(&input, "")).unwrap();
903 assert_eq!(
904 res,
905 AuthCert {
906 dir_key_certificate_version: AuthCertVersion::V3,
907 dir_address: None,
908 fingerprint: types::Fingerprint(
909 RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()
910 ),
911 dir_key_published: Iso8601TimeSp::from_str("2025-08-17 20:34:03").unwrap(),
912 dir_key_expires: Iso8601TimeSp::from_str("2026-08-17 20:34:03").unwrap(),
913 dir_identity_key: rsa::PublicKey::from_der(&to_der(include_str!(
914 "../../testdata2/authcert-longclaw-id-rsa"
915 )))
916 .unwrap(),
917 dir_signing_key: rsa::PublicKey::from_der(&to_der(include_str!(
918 "../../testdata2/authcert-longclaw-sign-rsa"
919 )))
920 .unwrap(),
921 dir_key_crosscert: CrossCert {
922 signature: CrossCertObject(
923 read_b64("testdata2/authcert-longclaw-crosscert-b64").1
924 )
925 }
926 }
927 );
928 }
929
930 #[test]
931 fn dir_auth_signature() {
932 let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
933 include_str!("../../testdata2/authcert-longclaw-full"),
934 "",
935 ))
936 .unwrap();
937
938 res.clone()
940 .verify_self_signed(
941 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
942 Duration::ZERO,
943 Duration::ZERO,
944 SystemTime::UNIX_EPOCH
945 .checked_add(Duration::from_secs(1762946693)) .unwrap(),
947 )
948 .unwrap();
949
950 assert_eq!(
952 res.clone()
953 .verify_self_signed(
954 &[],
955 Duration::ZERO,
956 Duration::ZERO,
957 SystemTime::UNIX_EPOCH
958 .checked_add(Duration::from_secs(1762946693)) .unwrap(),
960 )
961 .unwrap_err(),
962 VerifyFailed::InsufficientTrustedSigners
963 );
964
965 assert_eq!(
967 res.clone()
968 .verify_self_signed(
969 &[
970 RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
971 .unwrap()
972 ],
973 Duration::ZERO,
974 Duration::ZERO,
975 SystemTime::UNIX_EPOCH,
976 )
977 .unwrap_err(),
978 VerifyFailed::TooNew
979 );
980
981 res.clone()
983 .verify_self_signed(
984 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
985 Duration::ZERO,
986 Duration::ZERO,
987 SystemTime::UNIX_EPOCH
988 .checked_add(Duration::from_secs(1755462843)) .unwrap(),
990 )
991 .unwrap();
992
993 assert_eq!(
995 res.clone()
996 .verify_self_signed(
997 &[
998 RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
999 .unwrap()
1000 ],
1001 Duration::ZERO,
1002 Duration::ZERO,
1003 SystemTime::UNIX_EPOCH
1004 .checked_add(Duration::from_secs(1755462842)) .unwrap(),
1006 )
1007 .unwrap_err(),
1008 VerifyFailed::TooNew
1009 );
1010
1011 res.clone()
1013 .verify_self_signed(
1014 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1015 Duration::from_secs(1),
1016 Duration::ZERO,
1017 SystemTime::UNIX_EPOCH
1018 .checked_add(Duration::from_secs(1755462842)) .unwrap(),
1020 )
1021 .unwrap();
1022
1023 assert_eq!(
1025 res.clone()
1026 .verify_self_signed(
1027 &[
1028 RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
1029 .unwrap()
1030 ],
1031 Duration::ZERO,
1032 Duration::ZERO,
1033 SystemTime::UNIX_EPOCH
1034 .checked_add(Duration::from_secs(2000000000))
1035 .unwrap(),
1036 )
1037 .unwrap_err(),
1038 VerifyFailed::TooOld
1039 );
1040
1041 res.clone()
1043 .verify_self_signed(
1044 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1045 Duration::ZERO,
1046 Duration::ZERO,
1047 SystemTime::UNIX_EPOCH
1048 .checked_add(Duration::from_secs(1786998843)) .unwrap(),
1050 )
1051 .unwrap();
1052
1053 assert_eq!(
1055 res.clone()
1056 .verify_self_signed(
1057 &[
1058 RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
1059 .unwrap()
1060 ],
1061 Duration::ZERO,
1062 Duration::ZERO,
1063 SystemTime::UNIX_EPOCH
1064 .checked_add(Duration::from_secs(1786998844)) .unwrap(),
1066 )
1067 .unwrap_err(),
1068 VerifyFailed::TooOld
1069 );
1070
1071 res.clone()
1073 .verify_self_signed(
1074 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1075 Duration::ZERO,
1076 Duration::from_secs(1),
1077 SystemTime::UNIX_EPOCH
1078 .checked_add(Duration::from_secs(1786998844)) .unwrap(),
1080 )
1081 .unwrap();
1082
1083 let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
1085 include_str!("../../testdata2/authcert-longclaw-full-invalid-id-rsa"),
1086 "",
1087 ))
1088 .unwrap();
1089 assert_eq!(
1090 res.verify_self_signed(
1091 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1092 Duration::ZERO,
1093 Duration::ZERO,
1094 SystemTime::UNIX_EPOCH
1095 .checked_add(Duration::from_secs(1762946693)) .unwrap(),
1097 )
1098 .unwrap_err(),
1099 VerifyFailed::Inconsistent
1100 );
1101
1102 let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
1104 include_str!("../../testdata2/authcert-longclaw-full-invalid-cross"),
1105 "",
1106 ))
1107 .unwrap();
1108 assert_eq!(
1109 res.verify_self_signed(
1110 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1111 Duration::ZERO,
1112 Duration::ZERO,
1113 SystemTime::UNIX_EPOCH
1114 .checked_add(Duration::from_secs(1762946693)) .unwrap(),
1116 )
1117 .unwrap_err(),
1118 VerifyFailed::VerifyFailed
1119 );
1120
1121 let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
1123 include_str!("../../testdata2/authcert-longclaw-full-invalid-certification"),
1124 "",
1125 ))
1126 .unwrap();
1127 assert_eq!(
1128 res.verify_self_signed(
1129 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1130 Duration::ZERO,
1131 Duration::ZERO,
1132 SystemTime::UNIX_EPOCH
1133 .checked_add(Duration::from_secs(1762946693)) .unwrap(),
1135 )
1136 .unwrap_err(),
1137 VerifyFailed::VerifyFailed
1138 );
1139 }
1140 }
1141}