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")]
33#[allow(deprecated)]
34pub use build::AuthCertBuilder;
35
36#[cfg(feature = "parse2")]
37use crate::parse2::{self, ItemObjectParseable, SignatureHashInputs};
38
39#[cfg(all(feature = "parse2", feature = "plain-consensus"))]
41mod encoded;
42#[cfg(all(feature = "parse2", feature = "plain-consensus"))]
43pub use encoded::EncodedAuthCert;
44
45decl_keyword! {
46 pub(crate) AuthCertKwd {
47 "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
48 "dir-address" => DIR_ADDRESS,
49 "fingerprint" => FINGERPRINT,
50 "dir-identity-key" => DIR_IDENTITY_KEY,
51 "dir-key-published" => DIR_KEY_PUBLISHED,
52 "dir-key-expires" => DIR_KEY_EXPIRES,
53 "dir-signing-key" => DIR_SIGNING_KEY,
54 "dir-key-crosscert" => DIR_KEY_CROSSCERT,
55 "dir-key-certification" => DIR_KEY_CERTIFICATION,
56 }
57}
58
59static AUTHCERT_RULES: LazyLock<SectionRules<AuthCertKwd>> = LazyLock::new(|| {
62 use AuthCertKwd::*;
63
64 let mut rules = SectionRules::builder();
65 rules.add(DIR_KEY_CERTIFICATE_VERSION.rule().required().args(1..));
66 rules.add(DIR_ADDRESS.rule().args(1..));
67 rules.add(FINGERPRINT.rule().required().args(1..));
68 rules.add(DIR_IDENTITY_KEY.rule().required().no_args().obj_required());
69 rules.add(DIR_SIGNING_KEY.rule().required().no_args().obj_required());
70 rules.add(DIR_KEY_PUBLISHED.rule().required());
71 rules.add(DIR_KEY_EXPIRES.rule().required());
72 rules.add(DIR_KEY_CROSSCERT.rule().required().no_args().obj_required());
73 rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
74 rules.add(
75 DIR_KEY_CERTIFICATION
76 .rule()
77 .required()
78 .no_args()
79 .obj_required(),
80 );
81 rules.build()
82});
83
84#[derive(Clone, Debug, Deftly)]
92#[derive_deftly(Constructor)]
93#[cfg_attr(feature = "parse2", derive_deftly(NetdocParseable, NetdocSigned))]
94#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
96#[cfg_attr(test, derive(PartialEq, Eq))]
97#[allow(clippy::manual_non_exhaustive)]
98pub struct AuthCert {
99 #[deftly(constructor(default = "AuthCertVersion::V3"))]
105 #[deftly(netdoc(single_arg))]
106 pub dir_key_certificate_version: AuthCertVersion,
107
108 #[deftly(netdoc(single_arg))]
110 pub dir_address: Option<net::SocketAddrV4>,
111
112 #[deftly(constructor)]
116 #[deftly(netdoc(single_arg))]
117 pub fingerprint: Fingerprint,
118
119 #[deftly(constructor)]
123 #[deftly(netdoc(single_arg))]
124 pub dir_key_published: Iso8601TimeSp,
125
126 #[deftly(constructor)]
130 #[deftly(netdoc(single_arg))]
131 pub dir_key_expires: Iso8601TimeSp,
132
133 #[deftly(constructor)]
139 pub dir_identity_key: rsa::PublicKey,
140
141 #[deftly(constructor)]
147 pub dir_signing_key: rsa::PublicKey,
148
149 #[deftly(constructor)]
153 pub dir_key_crosscert: CrossCert,
154
155 #[doc(hidden)]
156 #[deftly(netdoc(skip))]
157 __non_exhaustive: (),
158}
159
160#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, strum::EnumString, strum::Display)]
166#[non_exhaustive]
167pub enum AuthCertVersion {
168 #[strum(serialize = "3")]
170 V3,
171}
172
173impl NormalItemArgument for AuthCertVersion {}
174
175#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
177#[allow(clippy::exhaustive_structs)]
178pub struct AuthCertKeyIds {
179 pub id_fingerprint: rsa::RsaIdentity,
181 pub sk_fingerprint: rsa::RsaIdentity,
183}
184
185pub struct UncheckedAuthCert {
188 location: Option<Extent>,
190
191 c: signed::SignatureGated<timed::TimerangeBound<AuthCert>>,
193}
194
195impl UncheckedAuthCert {
196 pub fn within<'a>(&self, haystack: &'a str) -> Option<&'a str> {
204 self.location
205 .as_ref()
206 .and_then(|ext| ext.reconstruct(haystack))
207 }
208}
209
210impl AuthCert {
211 #[cfg(feature = "build_docs")]
214 #[deprecated = "use AuthCertConstructor instead"]
215 #[allow(deprecated)]
216 pub fn builder() -> AuthCertBuilder {
217 AuthCertBuilder::new()
218 }
219
220 pub fn parse(s: &str) -> Result<UncheckedAuthCert> {
225 let mut reader = NetDocReader::new(s)?;
226 let body = AUTHCERT_RULES.parse(&mut reader)?;
227 reader.should_be_exhausted()?;
228 AuthCert::from_body(&body, s).map_err(|e| e.within(s))
229 }
230
231 pub fn parse_multiple(s: &str) -> Result<impl Iterator<Item = Result<UncheckedAuthCert>> + '_> {
233 use AuthCertKwd::*;
234 let sections = NetDocReader::new(s)?
235 .batching_split_before_loose(|item| item.is_ok_with_kwd(DIR_KEY_CERTIFICATE_VERSION));
236 Ok(sections
237 .map(|mut section| {
238 let body = AUTHCERT_RULES.parse(&mut section)?;
239 AuthCert::from_body(&body, s)
240 })
241 .map(|r| r.map_err(|e| e.within(s))))
242 }
243 pub fn signing_key(&self) -> &rsa::PublicKey {
252 &self.dir_signing_key
253 }
254
255 pub fn key_ids(&self) -> AuthCertKeyIds {
258 AuthCertKeyIds {
259 id_fingerprint: self.fingerprint.0,
260 sk_fingerprint: self.dir_signing_key.to_rsa_identity(),
261 }
262 }
263
264 pub fn id_fingerprint(&self) -> &rsa::RsaIdentity {
266 &self.fingerprint
267 }
268
269 pub fn published(&self) -> time::SystemTime {
271 *self.dir_key_published
272 }
273
274 pub fn expires(&self) -> time::SystemTime {
276 *self.dir_key_expires
277 }
278
279 fn from_body(body: &Section<'_, AuthCertKwd>, s: &str) -> Result<UncheckedAuthCert> {
281 use AuthCertKwd::*;
282
283 let start_pos = {
288 #[allow(clippy::unwrap_used)]
291 let first_item = body.first_item().unwrap();
292 if first_item.kwd() != DIR_KEY_CERTIFICATE_VERSION {
293 return Err(EK::WrongStartingToken
294 .with_msg(first_item.kwd_str().to_string())
295 .at_pos(first_item.pos()));
296 }
297 first_item.pos()
298 };
299 let end_pos = {
300 #[allow(clippy::unwrap_used)]
303 let last_item = body.last_item().unwrap();
304 if last_item.kwd() != DIR_KEY_CERTIFICATION {
305 return Err(EK::WrongEndingToken
306 .with_msg(last_item.kwd_str().to_string())
307 .at_pos(last_item.pos()));
308 }
309 last_item.end_pos()
310 };
311
312 let version = body
313 .required(DIR_KEY_CERTIFICATE_VERSION)?
314 .parse_arg::<u32>(0)?;
315 if version != 3 {
316 return Err(EK::BadDocumentVersion.with_msg(format!("unexpected version {}", version)));
317 }
318 let dir_key_certificate_version = AuthCertVersion::V3;
319
320 let dir_signing_key: rsa::PublicKey = body
321 .required(DIR_SIGNING_KEY)?
322 .parse_obj::<RsaPublicParse1Helper>("RSA PUBLIC KEY")?
323 .check_len(1024..)?
324 .check_exponent(65537)?
325 .into();
326
327 let dir_identity_key: rsa::PublicKey = body
328 .required(DIR_IDENTITY_KEY)?
329 .parse_obj::<RsaPublicParse1Helper>("RSA PUBLIC KEY")?
330 .check_len(1024..)?
331 .check_exponent(65537)?
332 .into();
333
334 let dir_key_published = body
335 .required(DIR_KEY_PUBLISHED)?
336 .args_as_str()
337 .parse::<Iso8601TimeSp>()?;
338
339 let dir_key_expires = body
340 .required(DIR_KEY_EXPIRES)?
341 .args_as_str()
342 .parse::<Iso8601TimeSp>()?;
343
344 {
345 let fp_tok = body.required(FINGERPRINT)?;
347 let fingerprint: RsaIdentity = fp_tok.args_as_str().parse::<Fingerprint>()?.into();
348 if fingerprint != dir_identity_key.to_rsa_identity() {
349 return Err(EK::BadArgument
350 .at_pos(fp_tok.pos())
351 .with_msg("fingerprint does not match RSA identity"));
352 }
353 }
354
355 let dir_address = body
356 .maybe(DIR_ADDRESS)
357 .parse_args_as_str::<net::SocketAddrV4>()?;
358
359 let dir_key_crosscert;
361 let v_crosscert = {
362 let crosscert = body.required(DIR_KEY_CROSSCERT)?;
363 #[allow(clippy::unwrap_used)]
366 let mut tag = crosscert.obj_tag().unwrap();
367 if tag != "ID SIGNATURE" && tag != "SIGNATURE" {
369 tag = "ID SIGNATURE";
370 }
371 let sig = crosscert.obj(tag)?;
372
373 let signed = dir_identity_key.to_rsa_identity();
374 let v = rsa::ValidatableRsaSignature::new(&dir_signing_key, &sig, signed.as_bytes());
377
378 dir_key_crosscert = CrossCert {
379 signature: CrossCertObject(sig),
380 };
381
382 v
383 };
384
385 let v_sig = {
387 let signature = body.required(DIR_KEY_CERTIFICATION)?;
388 let sig = signature.obj("SIGNATURE")?;
389
390 let mut sha1 = d::Sha1::new();
391 #[allow(clippy::unwrap_used)]
394 let start_offset = body.first_item().unwrap().offset_in(s).unwrap();
395 #[allow(clippy::unwrap_used)]
396 let end_offset = body.last_item().unwrap().offset_in(s).unwrap();
397 let end_offset = end_offset + "dir-key-certification\n".len();
398 sha1.update(&s[start_offset..end_offset]);
399 let sha1 = sha1.finalize();
400 rsa::ValidatableRsaSignature::new(&dir_identity_key, &sig, &sha1)
403 };
404
405 let id_fingerprint = dir_identity_key.to_rsa_identity();
406
407 let location = {
408 let start_idx = start_pos.offset_within(s);
409 let end_idx = end_pos.offset_within(s);
410 match (start_idx, end_idx) {
411 (Some(a), Some(b)) => Extent::new(s, &s[a..b + 1]),
412 _ => None,
413 }
414 };
415
416 let authcert = AuthCert {
417 dir_key_certificate_version,
418 dir_address,
419 dir_identity_key,
420 dir_signing_key,
421 dir_key_published,
422 dir_key_expires,
423 dir_key_crosscert,
424 fingerprint: Fingerprint(id_fingerprint),
425 __non_exhaustive: (),
426 };
427
428 let signatures: Vec<Box<dyn pk::ValidatableSignature>> =
429 vec![Box::new(v_crosscert), Box::new(v_sig)];
430
431 let timed = timed::TimerangeBound::new(authcert, *dir_key_published..*dir_key_expires);
432 let signed = signed::SignatureGated::new(timed, signatures);
433 let unchecked = UncheckedAuthCert {
434 location,
435 c: signed,
436 };
437 Ok(unchecked)
438 }
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
455#[cfg_attr(
456 feature = "parse2",
457 derive_deftly(ItemValueParseable),
458 deftly(netdoc(no_extra_args))
459)]
460#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
462#[non_exhaustive]
463pub struct CrossCert {
464 #[deftly(netdoc(object))]
466 pub signature: CrossCertObject,
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)]
488#[non_exhaustive]
489pub struct CrossCertObject(pub Vec<u8>);
490
491#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
501#[cfg_attr(
502 feature = "parse2",
503 derive_deftly(NetdocParseable),
504 deftly(netdoc(signatures))
505)]
506#[non_exhaustive]
507pub struct AuthCertSignatures {
508 pub dir_key_certification: AuthCertSignature,
510}
511
512#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
522#[cfg_attr(
523 feature = "parse2",
524 derive_deftly(ItemValueParseable),
525 deftly(netdoc(no_extra_args))
526)]
527#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
529#[non_exhaustive]
530pub struct AuthCertSignature {
531 #[deftly(netdoc(object(label = "SIGNATURE"), with = "crate::parse2::raw_data_object"))]
533 pub signature: Vec<u8>,
534
535 #[deftly(netdoc(sig_hash = "whole_keyword_line_sha1"))]
537 pub hash: [u8; 20],
538}
539
540#[cfg(feature = "parse2")]
541impl ItemObjectParseable for CrossCertObject {
542 fn check_label(label: &str) -> StdResult<(), parse2::EP> {
543 match label {
544 "SIGNATURE" | "ID SIGNATURE" => Ok(()),
545 _ => Err(parse2::EP::ObjectIncorrectLabel),
546 }
547 }
548
549 fn from_bytes(input: &[u8]) -> StdResult<Self, parse2::EP> {
550 Ok(Self(input.to_vec()))
551 }
552}
553
554impl tor_checkable::SelfSigned<timed::TimerangeBound<AuthCert>> for UncheckedAuthCert {
555 type Error = signature::Error;
556
557 fn dangerously_assume_wellsigned(self) -> timed::TimerangeBound<AuthCert> {
558 self.c.dangerously_assume_wellsigned()
559 }
560 fn is_well_signed(&self) -> std::result::Result<(), Self::Error> {
561 self.c.is_well_signed()
562 }
563}
564
565#[cfg(feature = "parse2")]
566impl AuthCertSigned {
567 pub fn verify_self_signed(
585 self,
586 v3idents: &[RsaIdentity],
587 pre_tolerance: Duration,
588 post_tolerance: Duration,
589 now: SystemTime,
590 ) -> StdResult<AuthCert, parse2::VerifyFailed> {
591 let (body, signatures) = (self.body, self.signatures);
592
593 if !v3idents.contains(&body.fingerprint.0) {
595 return Err(parse2::VerifyFailed::InsufficientTrustedSigners);
596 }
597
598 let validity = *body.dir_key_published..=*body.dir_key_expires;
600 parse2::check_validity_time_tolerance(now, validity, pre_tolerance, post_tolerance)?;
601
602 if body.dir_identity_key.to_rsa_identity() != *body.fingerprint {
604 return Err(parse2::VerifyFailed::Inconsistent);
605 }
606
607 body.dir_signing_key.verify(
609 body.fingerprint.0.as_bytes(),
610 &body.dir_key_crosscert.signature,
611 )?;
612
613 body.dir_identity_key.verify(
615 &signatures.dir_key_certification.hash,
616 &signatures.dir_key_certification.signature,
617 )?;
618
619 Ok(body)
620 }
621}
622
623#[cfg(test)]
624mod test {
625 #![allow(clippy::bool_assert_comparison)]
627 #![allow(clippy::clone_on_copy)]
628 #![allow(clippy::dbg_macro)]
629 #![allow(clippy::mixed_attributes_style)]
630 #![allow(clippy::print_stderr)]
631 #![allow(clippy::print_stdout)]
632 #![allow(clippy::single_char_pattern)]
633 #![allow(clippy::unwrap_used)]
634 #![allow(clippy::unchecked_time_subtraction)]
635 #![allow(clippy::useless_vec)]
636 #![allow(clippy::needless_pass_by_value)]
637 use super::*;
639 use crate::{Error, Pos};
640 const TESTDATA: &str = include_str!("../../testdata/authcert1.txt");
641
642 fn bad_data(fname: &str) -> String {
643 use std::fs;
644 use std::path::PathBuf;
645 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
646 path.push("testdata");
647 path.push("bad-certs");
648 path.push(fname);
649
650 fs::read_to_string(path).unwrap()
651 }
652
653 #[test]
654 fn parse_one() -> Result<()> {
655 use tor_checkable::{SelfSigned, Timebound};
656 let cert = AuthCert::parse(TESTDATA)?
657 .check_signature()
658 .unwrap()
659 .dangerously_assume_timely();
660
661 assert_eq!(
663 cert.id_fingerprint().to_string(),
664 "$ed03bb616eb2f60bec80151114bb25cef515b226"
665 );
666 assert_eq!(
667 cert.key_ids().sk_fingerprint.to_string(),
668 "$c4f720e2c59f9ddd4867fff465ca04031e35648f"
669 );
670
671 Ok(())
672 }
673
674 #[test]
675 fn parse_bad() {
676 fn check(fname: &str, err: &Error) {
677 let contents = bad_data(fname);
678 let cert = AuthCert::parse(&contents);
679 assert!(cert.is_err());
680 assert_eq!(&cert.err().unwrap(), err);
681 }
682
683 check(
684 "bad-cc-tag",
685 &EK::WrongObject.at_pos(Pos::from_line(27, 12)),
686 );
687 check(
688 "bad-fingerprint",
689 &EK::BadArgument
690 .at_pos(Pos::from_line(2, 1))
691 .with_msg("fingerprint does not match RSA identity"),
692 );
693 check(
694 "bad-version",
695 &EK::BadDocumentVersion.with_msg("unexpected version 4"),
696 );
697 check(
698 "wrong-end",
699 &EK::WrongEndingToken
700 .with_msg("dir-key-crosscert")
701 .at_pos(Pos::from_line(37, 1)),
702 );
703 check(
704 "wrong-start",
705 &EK::WrongStartingToken
706 .with_msg("fingerprint")
707 .at_pos(Pos::from_line(1, 1)),
708 );
709 }
710
711 #[test]
712 fn test_recovery_1() {
713 let mut data = "<><><<><>\nfingerprint ABC\n".to_string();
714 data += TESTDATA;
715
716 let res: Vec<Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
717
718 assert!(res[0].is_err());
720 assert!(res[1].is_ok());
721 assert_eq!(res.len(), 2);
722 }
723
724 #[test]
725 fn test_recovery_2() {
726 let mut data = bad_data("bad-version");
727 data += TESTDATA;
728
729 let res: Vec<Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
730
731 assert!(res[0].is_err());
733 assert!(res[1].is_ok());
734 assert_eq!(res.len(), 2);
735 }
736
737 #[cfg(feature = "parse2")]
738 mod parse2_test {
739 use super::{
740 AuthCert, AuthCertSignature, AuthCertSignatures, AuthCertSigned, AuthCertVersion,
741 CrossCert, CrossCertObject,
742 };
743
744 use std::{
745 fs::File,
746 io::Read,
747 path::Path,
748 str::FromStr,
749 time::{Duration, SystemTime},
750 };
751
752 use crate::{
753 parse2::{self, ErrorProblem, ParseError, ParseInput, VerifyFailed},
754 types::{self, Iso8601TimeSp},
755 };
756
757 use base64ct::{Base64, Encoding};
758 use derive_deftly::Deftly;
759 use digest::Digest;
760 use tor_llcrypto::{
761 d::Sha1,
762 pk::rsa::{self, RsaIdentity},
763 };
764
765 fn read_b64<P: AsRef<Path>>(path: P) -> (String, Vec<u8>) {
767 let mut encoded = String::new();
768 File::open(path)
769 .unwrap()
770 .read_to_string(&mut encoded)
771 .unwrap();
772 let mut decoded = Vec::new();
773 base64ct::Decoder::<Base64>::new_wrapped(encoded.as_bytes(), 64)
774 .unwrap()
775 .decode_to_end(&mut decoded)
776 .unwrap();
777
778 (encoded, decoded)
779 }
780
781 fn to_der(s: &str) -> Vec<u8> {
783 let mut r = Vec::new();
784 for line in s.lines() {
785 r.extend(Base64::decode_vec(line).unwrap());
786 }
787 r
788 }
789
790 #[test]
792 fn dir_auth_cross_cert() {
793 #[derive(Debug, Clone, PartialEq, Eq, Deftly)]
794 #[derive_deftly(NetdocParseable)]
795 struct Dummy {
796 dir_key_crosscert: CrossCert,
797 }
798
799 let (encoded, decoded) = read_b64("testdata2/authcert-longclaw-crosscert-b64");
800
801 let cert = format!(
803 "dir-key-crosscert\n-----BEGIN SIGNATURE-----\n{encoded}\n-----END SIGNATURE-----"
804 );
805 let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, "")).unwrap();
806 assert_eq!(
807 res,
808 Dummy {
809 dir_key_crosscert: CrossCert {
810 signature: CrossCertObject(decoded.clone())
811 }
812 }
813 );
814
815 let cert = format!(
817 "dir-key-crosscert\n-----BEGIN ID SIGNATURE-----\n{encoded}\n-----END ID SIGNATURE-----"
818 );
819 let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, "")).unwrap();
820 assert_eq!(
821 res,
822 Dummy {
823 dir_key_crosscert: CrossCert {
824 signature: CrossCertObject(decoded.clone())
825 }
826 }
827 );
828
829 let cert =
831 format!("dir-key-crosscert\n-----BEGIN WHAT-----\n{encoded}\n-----END WHAT-----");
832 let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, ""));
833 match res {
834 Err(ParseError {
835 problem: ErrorProblem::ObjectIncorrectLabel,
836 doctype: "dir-key-crosscert",
837 file: _,
838 lno: 1,
839 column: None,
840 }) => {}
841 other => panic!("not expected error {other:#?}"),
842 }
843
844 let cert = format!(
846 "dir-key-crosscert arg1\n-----BEGIN ID SIGNATURE-----\n{encoded}\n-----END ID SIGNATURE-----"
847 );
848 let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, ""));
849 match res {
850 Err(ParseError {
851 problem: ErrorProblem::UnexpectedArgument { column: 19 },
852 doctype: "dir-key-crosscert",
853 file: _,
854 lno: 1,
855 column: Some(19),
856 }) => {}
857 other => panic!("not expected error {other:#?}"),
858 }
859 }
860
861 #[test]
862 fn dir_auth_key_cert_signatures() {
863 let (encoded, decoded) = read_b64("testdata2/authcert-longclaw-signature-b64");
864 let cert = format!(
865 "dir-key-certification\n-----BEGIN SIGNATURE-----\n{encoded}\n-----END SIGNATURE-----"
866 );
867 let hash: [u8; 20] = Sha1::digest("dir-key-certification\n").into();
868
869 let res =
870 parse2::parse_netdoc::<AuthCertSignatures>(&ParseInput::new(&cert, "")).unwrap();
871 assert_eq!(
872 res,
873 AuthCertSignatures {
874 dir_key_certification: AuthCertSignature {
875 signature: decoded.clone(),
876 hash
877 }
878 }
879 );
880
881 let cert = format!(
883 "dir-key-certification\n-----BEGIN ID SIGNATURE-----\n{encoded}\n-----END ID SIGNATURE-----"
884 );
885 let res = parse2::parse_netdoc::<AuthCertSignatures>(&ParseInput::new(&cert, ""));
886 match res {
887 Err(ParseError {
888 problem: ErrorProblem::ObjectIncorrectLabel,
889 doctype: "",
890 file: _,
891 lno: 1,
892 column: None,
893 }) => {}
894 other => panic!("not expected error {other:#?}"),
895 }
896
897 let cert = format!(
899 "dir-key-certification arg1\n-----BEGIN SIGNATURE-----\n{encoded}\n-----END SIGNATURE-----"
900 );
901 let res = parse2::parse_netdoc::<AuthCertSignatures>(&ParseInput::new(&cert, ""));
902 match res {
903 Err(ParseError {
904 problem: ErrorProblem::UnexpectedArgument { column: 23 },
905 doctype: "",
906 file: _,
907 lno: 1,
908 column: Some(23),
909 }) => {}
910 other => panic!("not expected error {other:#?}"),
911 }
912 }
913
914 #[test]
915 fn dir_auth_cert() {
916 let mut input = String::new();
919 File::open("testdata2/authcert-longclaw-full")
920 .unwrap()
921 .read_to_string(&mut input)
922 .unwrap();
923
924 let res = parse2::parse_netdoc::<AuthCert>(&ParseInput::new(&input, "")).unwrap();
925 assert_eq!(
926 res,
927 AuthCert {
928 dir_key_certificate_version: AuthCertVersion::V3,
929 dir_address: None,
930 fingerprint: types::Fingerprint(
931 RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()
932 ),
933 dir_key_published: Iso8601TimeSp::from_str("2025-08-17 20:34:03").unwrap(),
934 dir_key_expires: Iso8601TimeSp::from_str("2026-08-17 20:34:03").unwrap(),
935 dir_identity_key: rsa::PublicKey::from_der(&to_der(include_str!(
936 "../../testdata2/authcert-longclaw-id-rsa"
937 )))
938 .unwrap(),
939 dir_signing_key: rsa::PublicKey::from_der(&to_der(include_str!(
940 "../../testdata2/authcert-longclaw-sign-rsa"
941 )))
942 .unwrap(),
943 dir_key_crosscert: CrossCert {
944 signature: CrossCertObject(
945 read_b64("testdata2/authcert-longclaw-crosscert-b64").1
946 )
947 },
948 __non_exhaustive: (),
949 }
950 );
951 }
952
953 #[test]
954 fn dir_auth_signature() {
955 let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
956 include_str!("../../testdata2/authcert-longclaw-full"),
957 "",
958 ))
959 .unwrap();
960
961 res.clone()
963 .verify_self_signed(
964 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
965 Duration::ZERO,
966 Duration::ZERO,
967 SystemTime::UNIX_EPOCH
968 .checked_add(Duration::from_secs(1762946693)) .unwrap(),
970 )
971 .unwrap();
972
973 assert_eq!(
975 res.clone()
976 .verify_self_signed(
977 &[],
978 Duration::ZERO,
979 Duration::ZERO,
980 SystemTime::UNIX_EPOCH
981 .checked_add(Duration::from_secs(1762946693)) .unwrap(),
983 )
984 .unwrap_err(),
985 VerifyFailed::InsufficientTrustedSigners
986 );
987
988 assert_eq!(
990 res.clone()
991 .verify_self_signed(
992 &[
993 RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
994 .unwrap()
995 ],
996 Duration::ZERO,
997 Duration::ZERO,
998 SystemTime::UNIX_EPOCH,
999 )
1000 .unwrap_err(),
1001 VerifyFailed::TooNew
1002 );
1003
1004 res.clone()
1006 .verify_self_signed(
1007 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1008 Duration::ZERO,
1009 Duration::ZERO,
1010 SystemTime::UNIX_EPOCH
1011 .checked_add(Duration::from_secs(1755462843)) .unwrap(),
1013 )
1014 .unwrap();
1015
1016 assert_eq!(
1018 res.clone()
1019 .verify_self_signed(
1020 &[
1021 RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
1022 .unwrap()
1023 ],
1024 Duration::ZERO,
1025 Duration::ZERO,
1026 SystemTime::UNIX_EPOCH
1027 .checked_add(Duration::from_secs(1755462842)) .unwrap(),
1029 )
1030 .unwrap_err(),
1031 VerifyFailed::TooNew
1032 );
1033
1034 res.clone()
1036 .verify_self_signed(
1037 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1038 Duration::from_secs(1),
1039 Duration::ZERO,
1040 SystemTime::UNIX_EPOCH
1041 .checked_add(Duration::from_secs(1755462842)) .unwrap(),
1043 )
1044 .unwrap();
1045
1046 assert_eq!(
1048 res.clone()
1049 .verify_self_signed(
1050 &[
1051 RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
1052 .unwrap()
1053 ],
1054 Duration::ZERO,
1055 Duration::ZERO,
1056 SystemTime::UNIX_EPOCH
1057 .checked_add(Duration::from_secs(2000000000))
1058 .unwrap(),
1059 )
1060 .unwrap_err(),
1061 VerifyFailed::TooOld
1062 );
1063
1064 res.clone()
1066 .verify_self_signed(
1067 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1068 Duration::ZERO,
1069 Duration::ZERO,
1070 SystemTime::UNIX_EPOCH
1071 .checked_add(Duration::from_secs(1786998843)) .unwrap(),
1073 )
1074 .unwrap();
1075
1076 assert_eq!(
1078 res.clone()
1079 .verify_self_signed(
1080 &[
1081 RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
1082 .unwrap()
1083 ],
1084 Duration::ZERO,
1085 Duration::ZERO,
1086 SystemTime::UNIX_EPOCH
1087 .checked_add(Duration::from_secs(1786998844)) .unwrap(),
1089 )
1090 .unwrap_err(),
1091 VerifyFailed::TooOld
1092 );
1093
1094 res.clone()
1096 .verify_self_signed(
1097 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1098 Duration::ZERO,
1099 Duration::from_secs(1),
1100 SystemTime::UNIX_EPOCH
1101 .checked_add(Duration::from_secs(1786998844)) .unwrap(),
1103 )
1104 .unwrap();
1105
1106 let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
1108 include_str!("../../testdata2/authcert-longclaw-full-invalid-id-rsa"),
1109 "",
1110 ))
1111 .unwrap();
1112 assert_eq!(
1113 res.verify_self_signed(
1114 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1115 Duration::ZERO,
1116 Duration::ZERO,
1117 SystemTime::UNIX_EPOCH
1118 .checked_add(Duration::from_secs(1762946693)) .unwrap(),
1120 )
1121 .unwrap_err(),
1122 VerifyFailed::Inconsistent
1123 );
1124
1125 let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
1127 include_str!("../../testdata2/authcert-longclaw-full-invalid-cross"),
1128 "",
1129 ))
1130 .unwrap();
1131 assert_eq!(
1132 res.verify_self_signed(
1133 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1134 Duration::ZERO,
1135 Duration::ZERO,
1136 SystemTime::UNIX_EPOCH
1137 .checked_add(Duration::from_secs(1762946693)) .unwrap(),
1139 )
1140 .unwrap_err(),
1141 VerifyFailed::VerifyFailed
1142 );
1143
1144 let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
1146 include_str!("../../testdata2/authcert-longclaw-full-invalid-certification"),
1147 "",
1148 ))
1149 .unwrap();
1150 assert_eq!(
1151 res.verify_self_signed(
1152 &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
1153 Duration::ZERO,
1154 Duration::ZERO,
1155 SystemTime::UNIX_EPOCH
1156 .checked_add(Duration::from_secs(1762946693)) .unwrap(),
1158 )
1159 .unwrap_err(),
1160 VerifyFailed::VerifyFailed
1161 );
1162 }
1163 }
1164}