tor_netdoc/doc/
authcert.rs

1//! Parsing implementation for Tor authority certificates
2//!
3//! An "authority certificate" is a short signed document that binds a
4//! directory authority's permanent "identity key" to its medium-term
5//! "signing key".  Using separate keys here enables the authorities
6//! to keep their identity keys securely offline, while using the
7//! signing keys to sign votes and consensuses.
8
9use 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
52/// Rules about entries that must appear in an AuthCert, and how they must
53/// be formed.
54static 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/// A single directory authority key certificate
78///
79/// This is the body, not including signatures.
80///
81/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html>
82#[derive(Clone, Debug, Deftly)]
83#[cfg_attr(feature = "parse2", derive_deftly(NetdocParseable, NetdocSigned))]
84// derive_deftly_adhoc disables unused deftly attribute checking, so we needn't cfg_attr them all
85#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
86#[cfg_attr(test, derive(PartialEq, Eq))]
87#[non_exhaustive]
88// TODO DIRAUTH make a way to construct this! and deprecate the old builder
89// https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/3555#note_3320387
90pub struct AuthCert {
91    /// Intro line
92    ///
93    /// Currently must be version 3.
94    ///
95    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certificate-version>
96    #[deftly(netdoc(single_arg))]
97    pub dir_key_certificate_version: AuthCertVersion,
98
99    /// An IPv4 address for this authority.
100    #[deftly(netdoc(single_arg))]
101    pub dir_address: Option<net::SocketAddrV4>,
102
103    /// H(KP_auth_id_rsa)
104    #[deftly(netdoc(single_arg))]
105    ///
106    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:fingerprint>
107    pub fingerprint: Fingerprint,
108
109    /// Declared time when this certificate was published
110    ///
111    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-published>
112    #[deftly(netdoc(single_arg))]
113    pub dir_key_published: Iso8601TimeSp,
114
115    /// Declared time when this certificate expires.
116    ///
117    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-expires>
118    #[deftly(netdoc(single_arg))]
119    pub dir_key_expires: Iso8601TimeSp,
120
121    /// KP_auth_id_rsa
122    ///
123    /// The long-term RSA identity key for this authority
124    ///
125    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-identity-key>
126    pub dir_identity_key: rsa::PublicKey,
127
128    /// KP_auth_sign_rsa
129    ///
130    /// The medium-term RSA signing key for this authority
131    ///
132    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-signing-key>
133    pub dir_signing_key: rsa::PublicKey,
134
135    /// SHA1(DER(KP_auth_id_rsa)) signed by KP_auth_sign_rsa
136    ///
137    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-crosscert>
138    pub dir_key_crosscert: CrossCert,
139}
140
141/// Represents the version of an [`AuthCert`].
142///
143/// Single argument.
144///
145/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certificate-version>
146#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, strum::EnumString, strum::Display)]
147#[non_exhaustive]
148pub enum AuthCertVersion {
149    /// The current and only version understood.
150    #[strum(serialize = "3")]
151    V3,
152}
153
154impl NormalItemArgument for AuthCertVersion {}
155
156/// A pair of key identities that identifies a certificate.
157#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
158#[allow(clippy::exhaustive_structs)]
159pub struct AuthCertKeyIds {
160    /// Fingerprint of identity key
161    pub id_fingerprint: rsa::RsaIdentity,
162    /// Fingerprint of signing key
163    pub sk_fingerprint: rsa::RsaIdentity,
164}
165
166/// An authority certificate whose signature and validity time we
167/// haven't checked.
168pub struct UncheckedAuthCert {
169    /// Where we found this AuthCert within the string containing it.
170    location: Option<Extent>,
171
172    /// The actual unchecked certificate.
173    c: signed::SignatureGated<timed::TimerangeBound<AuthCert>>,
174}
175
176impl UncheckedAuthCert {
177    /// If this AuthCert was originally parsed from `haystack`, return its
178    /// text.
179    ///
180    /// TODO: This is a pretty bogus interface; there should be a
181    /// better way to remember where to look for this thing if we want
182    /// it without keeping the input alive forever.  We should
183    /// refactor.
184    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    /// Make an [`AuthCertBuilder`] object that can be used to
193    /// construct authority certificates for testing.
194    #[cfg(feature = "build_docs")]
195    pub fn builder() -> AuthCertBuilder {
196        AuthCertBuilder::new()
197    }
198
199    /// Parse an authority certificate from a string.
200    ///
201    /// This function verifies the certificate's signatures, but doesn't
202    /// check its expiration dates.
203    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    /// Return an iterator yielding authority certificates from a string.
211    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    /*
223        /// Return true if this certificate is expired at a given time, or
224        /// not yet valid at that time.
225        pub fn is_expired_at(&self, when: time::SystemTime) -> bool {
226            when < self.published || when > self.expires
227        }
228    */
229    /// Return the signing key certified by this certificate.
230    pub fn signing_key(&self) -> &rsa::PublicKey {
231        &self.dir_signing_key
232    }
233
234    /// Return an AuthCertKeyIds object describing the keys in this
235    /// certificate.
236    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    /// Return an RsaIdentity for this certificate's identity key.
244    pub fn id_fingerprint(&self) -> &rsa::RsaIdentity {
245        &self.fingerprint
246    }
247
248    /// Return the time when this certificate says it was published.
249    pub fn published(&self) -> time::SystemTime {
250        *self.dir_key_published
251    }
252
253    /// Return the time when this certificate says it should expire.
254    pub fn expires(&self) -> time::SystemTime {
255        *self.dir_key_expires
256    }
257
258    /// Parse an authority certificate from a reader.
259    fn from_body(body: &Section<'_, AuthCertKwd>, s: &str) -> Result<UncheckedAuthCert> {
260        use AuthCertKwd::*;
261
262        // Make sure first and last element are correct types.  We can
263        // safely call unwrap() on first and last, since there are required
264        // tokens in the rules, so we know that at least one token will have
265        // been parsed.
266        let start_pos = {
267            // Unwrap should be safe because `.parse()` would have already
268            // returned an Error
269            #[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            // Unwrap should be safe because `.parse()` would have already
280            // returned an Error
281            #[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            // Check fingerprint for consistency with key.
325            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        // check crosscert
339        let dir_key_crosscert;
340        let v_crosscert = {
341            let crosscert = body.required(DIR_KEY_CROSSCERT)?;
342            // Unwrap should be safe because `.parse()` and `required()` would
343            // have already returned an Error
344            #[allow(clippy::unwrap_used)]
345            let mut tag = crosscert.obj_tag().unwrap();
346            // we are required to support both.
347            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            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
354
355            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        // check the signature
365        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            // Unwrap should be safe because `.parse()` would have already
371            // returned an Error
372            #[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            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
380
381            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/// Pseudo-Signature of the long-term identity key by the medium-term key.
420///
421/// This type does not implement `SignatureItemParseable` because that trait
422/// is reserved for signatures on *netdocs*, such as [`AuthCertSignature`].
423/// As `CrossCert` does not sign a full document, it implements only
424/// `ItemValueParseable`, instead.
425///
426/// Verification of this signature is done in `AuthCertSigned::verify_self_signed`,
427/// and during parsing by the old parser.
428/// So a `CrossCert` in [`AuthCert::dir_key_crosscert`] in a bare `AuthCert` has been validated.
429//
430// TODO SPEC (Diziet): it is far from clear to me that this cert serves any useful purpose.
431// However, we are far too busy now with rewriting the universe to consider transitioning it away.
432#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
433#[cfg_attr(
434    feature = "parse2",
435    derive_deftly(ItemValueParseable),
436    deftly(netdoc(no_extra_args))
437)]
438// derive_deftly_adhoc disables unused deftly attribute checking, so we needn't cfg_attr them all
439#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
440#[non_exhaustive]
441pub struct CrossCert {
442    /// The bytes of the signature (base64-decoded).
443    #[deftly(netdoc(object))]
444    pub signature: CrossCertObject,
445}
446
447/// Wrapper around [`Vec<u8>`] implementing [`ItemObjectParseable`] properly.
448///
449/// Unfortunately, this wrapper is necessary, because the specification
450/// demands that these certificate objects must accept two labels:
451/// `SIGNATURE` and `ID SIGNATURE`.  Because the deftly template for
452/// `ItemValueParseable` only allows for a single label
453/// (`#[deftly(netdoc(object(label = "LABEL")))]`), we must implement this
454/// trait ourselves in order to allow multiple ones.
455///
456/// TODO: In the future, it might be nice to let the respective fmeta
457/// accept a pattern, as pattern matching would allow trivially for one
458/// to infinity different combinations.
459/// TODO SPEC: Alternatively we could abolish the wrong labels,
460/// or we could abolish Objects completely and just have long lines.
461///
462/// # Specifications
463///
464/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-crosscert>
465#[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)]
466#[non_exhaustive]
467pub struct CrossCertObject(pub Vec<u8>);
468
469/// Signatures for [`AuthCert`]
470///
471/// Signed by [`AuthCert::dir_identity_key`] in order to prove ownership.
472/// Can be seen as the opposite of [`AuthCert::dir_key_crosscert`].
473///
474/// # Specifications
475///
476/// * <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certification>
477/// * <https://spec.torproject.org/dir-spec/netdoc.html#signing>
478#[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    /// Contains the actual signature, see [`AuthCertSignatures`].
487    pub dir_key_certification: AuthCertSignature,
488}
489
490/// RSA signature for data in [`AuthCert`] and related structures
491///
492/// <https://spec.torproject.org/dir-spec/netdoc.html#signing>
493///
494/// # Caveats
495///
496/// This type **MUST NOT** be used for [`AuthCert::dir_key_crosscert`]
497/// because its set of object labels is a strict superset of the object
498/// labels used by this type.
499#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
500#[cfg_attr(
501    feature = "parse2",
502    derive_deftly(ItemValueParseable),
503    deftly(netdoc(no_extra_args))
504)]
505// derive_deftly_adhoc disables unused deftly attribute checking, so we needn't cfg_attr them all
506#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
507#[non_exhaustive]
508pub struct AuthCertSignature {
509    /// The bytes of the signature (base64-decoded).
510    #[deftly(netdoc(object(label = "SIGNATURE"), with = "crate::parse2::raw_data_object"))]
511    pub signature: Vec<u8>,
512
513    /// The SHA1 hash of the document.
514    #[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    /// Verifies the signature of a [`AuthCert`]
546    ///
547    /// # Algorithm
548    ///
549    /// 1. Check whether this comes from a valid authority in `v3idents`.
550    /// 2. Check whether the timestamps are valid (± tolerance).
551    /// 3. Check whether the fingerprint and long-term identity key match.
552    /// 4. Check the cross-certificate (proof-of-ownership of signing key).
553    /// 5. Check the outer certificate (proof-of-ownership of identity key).
554    ///
555    /// TODO: Replace `pre_tolerance` and `post_tolerance` with
556    /// `tor_dircommon::config::DirTolerance` which is not possible at the
557    /// moment due to a circular dependency of `tor-dircommon` depending
558    /// upon `tor-netdoc`.
559    ///
560    /// TODO: Consider whether to try to deduplicate this signature checking
561    /// somehow, wrt to [`UncheckedAuthCert`].
562    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        // (1) Check whether this comes from a valid authority in `v3idents`.
572        if !v3idents.contains(&body.fingerprint.0) {
573            return Err(parse2::VerifyFailed::InsufficientTrustedSigners);
574        }
575
576        // (2) Check whether the timestamps are valid (± tolerance).
577        let validity = *body.dir_key_published..=*body.dir_key_expires;
578        parse2::check_validity_time_tolerance(now, validity, pre_tolerance, post_tolerance)?;
579
580        // (3) Check whether the fingerprint and long-term identity key match.
581        if body.dir_identity_key.to_rsa_identity() != *body.fingerprint {
582            return Err(parse2::VerifyFailed::Inconsistent);
583        }
584
585        // (4) Check the cross-certificate (proof-of-ownership of signing key).
586        body.dir_signing_key.verify(
587            body.fingerprint.0.as_bytes(),
588            &body.dir_key_crosscert.signature,
589        )?;
590
591        // (5) Check the outer certificate (proof-of-ownership of identity key).
592        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    // @@ begin test lint list maintained by maint/add_warning @@
604    #![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    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
616    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        // Taken from TESTDATA
640        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        // We should recover from the failed case and read the next data fine.
697        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        // We should recover from the failed case and read the next data fine.
710        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        /// Reads a b64 encoded file and returns its content encoded and decoded.
744        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        /// Converts PEM to DER (without BEGIN and END lines).
760        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        /// Tests whether a [`DirKeyCrossCert`] can be parsed properly.
769        #[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            // Try with `SIGNATURE`.
780            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            // Try with `ID SIGNATURE`.
794            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            // Try with different label and fail.
808            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            // Try with extra args.
823            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            // Test incorrect label.
860            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            // Test additional args.
876            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            // This is longclaw.
895
896            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            // Test a valid signature.
939            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)) // Wed Nov 12 12:24:53 CET 2025
946                        .unwrap(),
947                )
948                .unwrap();
949
950            // Test with an invalid authority.
951            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)) // Wed Nov 12 12:24:53 CET 2025
959                            .unwrap(),
960                    )
961                    .unwrap_err(),
962                VerifyFailed::InsufficientTrustedSigners
963            );
964
965            // Test a key too far in the future.
966            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            // Test an almost too new.
982            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)) // 2025-08-17 20:34:03
989                        .unwrap(),
990                )
991                .unwrap();
992
993            // Now fail when we are 1s below ...
994            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)) // 2025-08-17 20:34:02
1005                            .unwrap(),
1006                    )
1007                    .unwrap_err(),
1008                VerifyFailed::TooNew
1009            );
1010
1011            // ... but succeed again with a clock skew tolerance.
1012            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)) // 2025-08-17 20:34:02
1019                        .unwrap(),
1020                )
1021                .unwrap();
1022
1023            // Test a key too old.
1024            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            // Test an almost too old.
1042            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)) // 2026-08-17 20:34:03
1049                        .unwrap(),
1050                )
1051                .unwrap();
1052
1053            // Now fail when we are 1s above ...
1054            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)) // 2026-08-17 20:34:04
1065                            .unwrap(),
1066                    )
1067                    .unwrap_err(),
1068                VerifyFailed::TooOld
1069            );
1070
1071            // ... but succeed again with a clock skew tolerance.
1072            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)) // 2026-08-17 20:34:04
1079                        .unwrap(),
1080                )
1081                .unwrap();
1082
1083            // Check with non-matching fingerprint and long-term identity key.
1084            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)) // Wed Nov 12 12:24:53 CET 2025
1096                        .unwrap(),
1097                )
1098                .unwrap_err(),
1099                VerifyFailed::Inconsistent
1100            );
1101
1102            // Check invalid cross-cert.
1103            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)) // Wed Nov 12 12:24:53 CET 2025
1115                        .unwrap(),
1116                )
1117                .unwrap_err(),
1118                VerifyFailed::VerifyFailed
1119            );
1120
1121            // Check outer signature.
1122            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)) // Wed Nov 12 12:24:53 CET 2025
1134                        .unwrap(),
1135                )
1136                .unwrap_err(),
1137                VerifyFailed::VerifyFailed
1138            );
1139        }
1140    }
1141}