Skip to main content

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")]
33#[allow(deprecated)]
34pub use build::AuthCertBuilder;
35
36#[cfg(feature = "parse2")]
37use crate::parse2::{self, ItemObjectParseable, SignatureHashInputs};
38
39// TODO DIRAUTH untangle these feature(s)
40#[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
59/// Rules about entries that must appear in an AuthCert, and how they must
60/// be formed.
61static 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/// A single directory authority key certificate
85///
86/// This is the body, not including signatures.
87///
88/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html>
89///
90/// To make a fresh `AuthCert`, use [`AuthCertConstructor`].
91#[derive(Clone, Debug, Deftly)]
92#[derive_deftly(Constructor)]
93#[cfg_attr(feature = "parse2", derive_deftly(NetdocParseable, NetdocSigned))]
94// derive_deftly_adhoc disables unused deftly attribute checking, so we needn't cfg_attr them all
95#[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    /// Intro line
100    ///
101    /// Currently must be version 3.
102    ///
103    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certificate-version>
104    #[deftly(constructor(default = "AuthCertVersion::V3"))]
105    #[deftly(netdoc(single_arg))]
106    pub dir_key_certificate_version: AuthCertVersion,
107
108    /// An IPv4 address for this authority.
109    #[deftly(netdoc(single_arg))]
110    pub dir_address: Option<net::SocketAddrV4>,
111
112    /// H(KP_auth_id_rsa)
113    ///
114    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:fingerprint>
115    #[deftly(constructor)]
116    #[deftly(netdoc(single_arg))]
117    pub fingerprint: Fingerprint,
118
119    /// Declared time when this certificate was published
120    ///
121    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-published>
122    #[deftly(constructor)]
123    #[deftly(netdoc(single_arg))]
124    pub dir_key_published: Iso8601TimeSp,
125
126    /// Declared time when this certificate expires.
127    ///
128    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-expires>
129    #[deftly(constructor)]
130    #[deftly(netdoc(single_arg))]
131    pub dir_key_expires: Iso8601TimeSp,
132
133    /// KP_auth_id_rsa
134    ///
135    /// The long-term RSA identity key for this authority
136    ///
137    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-identity-key>
138    #[deftly(constructor)]
139    pub dir_identity_key: rsa::PublicKey,
140
141    /// KP_auth_sign_rsa
142    ///
143    /// The medium-term RSA signing key for this authority
144    ///
145    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-signing-key>
146    #[deftly(constructor)]
147    pub dir_signing_key: rsa::PublicKey,
148
149    /// SHA1(DER(KP_auth_id_rsa)) signed by KP_auth_sign_rsa
150    ///
151    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-crosscert>
152    #[deftly(constructor)]
153    pub dir_key_crosscert: CrossCert,
154
155    #[doc(hidden)]
156    #[deftly(netdoc(skip))]
157    __non_exhaustive: (),
158}
159
160/// Represents the version of an [`AuthCert`].
161///
162/// Single argument.
163///
164/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certificate-version>
165#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, strum::EnumString, strum::Display)]
166#[non_exhaustive]
167pub enum AuthCertVersion {
168    /// The current and only version understood.
169    #[strum(serialize = "3")]
170    V3,
171}
172
173impl NormalItemArgument for AuthCertVersion {}
174
175/// A pair of key identities that identifies a certificate.
176#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
177#[allow(clippy::exhaustive_structs)]
178pub struct AuthCertKeyIds {
179    /// Fingerprint of identity key
180    pub id_fingerprint: rsa::RsaIdentity,
181    /// Fingerprint of signing key
182    pub sk_fingerprint: rsa::RsaIdentity,
183}
184
185/// An authority certificate whose signature and validity time we
186/// haven't checked.
187pub struct UncheckedAuthCert {
188    /// Where we found this AuthCert within the string containing it.
189    location: Option<Extent>,
190
191    /// The actual unchecked certificate.
192    c: signed::SignatureGated<timed::TimerangeBound<AuthCert>>,
193}
194
195impl UncheckedAuthCert {
196    /// If this AuthCert was originally parsed from `haystack`, return its
197    /// text.
198    ///
199    /// TODO: This is a pretty bogus interface; there should be a
200    /// better way to remember where to look for this thing if we want
201    /// it without keeping the input alive forever.  We should
202    /// refactor.
203    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    /// Make an [`AuthCertBuilder`] object that can be used to
212    /// construct authority certificates for testing.
213    #[cfg(feature = "build_docs")]
214    #[deprecated = "use AuthCertConstructor instead"]
215    #[allow(deprecated)]
216    pub fn builder() -> AuthCertBuilder {
217        AuthCertBuilder::new()
218    }
219
220    /// Parse an authority certificate from a string.
221    ///
222    /// This function verifies the certificate's signatures, but doesn't
223    /// check its expiration dates.
224    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    /// Return an iterator yielding authority certificates from a string.
232    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    /*
244        /// Return true if this certificate is expired at a given time, or
245        /// not yet valid at that time.
246        pub fn is_expired_at(&self, when: time::SystemTime) -> bool {
247            when < self.published || when > self.expires
248        }
249    */
250    /// Return the signing key certified by this certificate.
251    pub fn signing_key(&self) -> &rsa::PublicKey {
252        &self.dir_signing_key
253    }
254
255    /// Return an AuthCertKeyIds object describing the keys in this
256    /// certificate.
257    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    /// Return an RsaIdentity for this certificate's identity key.
265    pub fn id_fingerprint(&self) -> &rsa::RsaIdentity {
266        &self.fingerprint
267    }
268
269    /// Return the time when this certificate says it was published.
270    pub fn published(&self) -> time::SystemTime {
271        *self.dir_key_published
272    }
273
274    /// Return the time when this certificate says it should expire.
275    pub fn expires(&self) -> time::SystemTime {
276        *self.dir_key_expires
277    }
278
279    /// Parse an authority certificate from a reader.
280    fn from_body(body: &Section<'_, AuthCertKwd>, s: &str) -> Result<UncheckedAuthCert> {
281        use AuthCertKwd::*;
282
283        // Make sure first and last element are correct types.  We can
284        // safely call unwrap() on first and last, since there are required
285        // tokens in the rules, so we know that at least one token will have
286        // been parsed.
287        let start_pos = {
288            // Unwrap should be safe because `.parse()` would have already
289            // returned an Error
290            #[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            // Unwrap should be safe because `.parse()` would have already
301            // returned an Error
302            #[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            // Check fingerprint for consistency with key.
346            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        // check crosscert
360        let dir_key_crosscert;
361        let v_crosscert = {
362            let crosscert = body.required(DIR_KEY_CROSSCERT)?;
363            // Unwrap should be safe because `.parse()` and `required()` would
364            // have already returned an Error
365            #[allow(clippy::unwrap_used)]
366            let mut tag = crosscert.obj_tag().unwrap();
367            // we are required to support both.
368            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            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
375
376            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        // check the signature
386        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            // Unwrap should be safe because `.parse()` would have already
392            // returned an Error
393            #[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            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
401
402            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/// Pseudo-Signature of the long-term identity key by the medium-term key.
442///
443/// This type does not implement `SignatureItemParseable` because that trait
444/// is reserved for signatures on *netdocs*, such as [`AuthCertSignature`].
445/// As `CrossCert` does not sign a full document, it implements only
446/// `ItemValueParseable`, instead.
447///
448/// Verification of this signature is done in `AuthCertSigned::verify_self_signed`,
449/// and during parsing by the old parser.
450/// So a `CrossCert` in [`AuthCert::dir_key_crosscert`] in a bare `AuthCert` has been validated.
451//
452// TODO SPEC (Diziet): it is far from clear to me that this cert serves any useful purpose.
453// However, we are far too busy now with rewriting the universe to consider transitioning it away.
454#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
455#[cfg_attr(
456    feature = "parse2",
457    derive_deftly(ItemValueParseable),
458    deftly(netdoc(no_extra_args))
459)]
460// derive_deftly_adhoc disables unused deftly attribute checking, so we needn't cfg_attr them all
461#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
462#[non_exhaustive]
463pub struct CrossCert {
464    /// The bytes of the signature (base64-decoded).
465    #[deftly(netdoc(object))]
466    pub signature: CrossCertObject,
467}
468
469/// Wrapper around [`Vec<u8>`] implementing [`ItemObjectParseable`] properly.
470///
471/// Unfortunately, this wrapper is necessary, because the specification
472/// demands that these certificate objects must accept two labels:
473/// `SIGNATURE` and `ID SIGNATURE`.  Because the deftly template for
474/// `ItemValueParseable` only allows for a single label
475/// (`#[deftly(netdoc(object(label = "LABEL")))]`), we must implement this
476/// trait ourselves in order to allow multiple ones.
477///
478/// TODO: In the future, it might be nice to let the respective fmeta
479/// accept a pattern, as pattern matching would allow trivially for one
480/// to infinity different combinations.
481/// TODO SPEC: Alternatively we could abolish the wrong labels,
482/// or we could abolish Objects completely and just have long lines.
483///
484/// # Specifications
485///
486/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-crosscert>
487#[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)]
488#[non_exhaustive]
489pub struct CrossCertObject(pub Vec<u8>);
490
491/// Signatures for [`AuthCert`]
492///
493/// Signed by [`AuthCert::dir_identity_key`] in order to prove ownership.
494/// Can be seen as the opposite of [`AuthCert::dir_key_crosscert`].
495///
496/// # Specifications
497///
498/// * <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certification>
499/// * <https://spec.torproject.org/dir-spec/netdoc.html#signing>
500#[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    /// Contains the actual signature, see [`AuthCertSignatures`].
509    pub dir_key_certification: AuthCertSignature,
510}
511
512/// RSA signature for data in [`AuthCert`] and related structures
513///
514/// <https://spec.torproject.org/dir-spec/netdoc.html#signing>
515///
516/// # Caveats
517///
518/// This type **MUST NOT** be used for [`AuthCert::dir_key_crosscert`]
519/// because its set of object labels is a strict superset of the object
520/// labels used by this type.
521#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
522#[cfg_attr(
523    feature = "parse2",
524    derive_deftly(ItemValueParseable),
525    deftly(netdoc(no_extra_args))
526)]
527// derive_deftly_adhoc disables unused deftly attribute checking, so we needn't cfg_attr them all
528#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
529#[non_exhaustive]
530pub struct AuthCertSignature {
531    /// The bytes of the signature (base64-decoded).
532    #[deftly(netdoc(object(label = "SIGNATURE"), with = "crate::parse2::raw_data_object"))]
533    pub signature: Vec<u8>,
534
535    /// The SHA1 hash of the document.
536    #[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    /// Verifies the signature of a [`AuthCert`]
568    ///
569    /// # Algorithm
570    ///
571    /// 1. Check whether this comes from a valid authority in `v3idents`.
572    /// 2. Check whether the timestamps are valid (± tolerance).
573    /// 3. Check whether the fingerprint and long-term identity key match.
574    /// 4. Check the cross-certificate (proof-of-ownership of signing key).
575    /// 5. Check the outer certificate (proof-of-ownership of identity key).
576    ///
577    /// TODO: Replace `pre_tolerance` and `post_tolerance` with
578    /// `tor_dircommon::config::DirTolerance` which is not possible at the
579    /// moment due to a circular dependency of `tor-dircommon` depending
580    /// upon `tor-netdoc`.
581    ///
582    /// TODO: Consider whether to try to deduplicate this signature checking
583    /// somehow, wrt to [`UncheckedAuthCert`].
584    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        // (1) Check whether this comes from a valid authority in `v3idents`.
594        if !v3idents.contains(&body.fingerprint.0) {
595            return Err(parse2::VerifyFailed::InsufficientTrustedSigners);
596        }
597
598        // (2) Check whether the timestamps are valid (± tolerance).
599        let validity = *body.dir_key_published..=*body.dir_key_expires;
600        parse2::check_validity_time_tolerance(now, validity, pre_tolerance, post_tolerance)?;
601
602        // (3) Check whether the fingerprint and long-term identity key match.
603        if body.dir_identity_key.to_rsa_identity() != *body.fingerprint {
604            return Err(parse2::VerifyFailed::Inconsistent);
605        }
606
607        // (4) Check the cross-certificate (proof-of-ownership of signing key).
608        body.dir_signing_key.verify(
609            body.fingerprint.0.as_bytes(),
610            &body.dir_key_crosscert.signature,
611        )?;
612
613        // (5) Check the outer certificate (proof-of-ownership of identity key).
614        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    // @@ begin test lint list maintained by maint/add_warning @@
626    #![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    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
638    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        // Taken from TESTDATA
662        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        // We should recover from the failed case and read the next data fine.
719        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        // We should recover from the failed case and read the next data fine.
732        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        /// Reads a b64 encoded file and returns its content encoded and decoded.
766        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        /// Converts PEM to DER (without BEGIN and END lines).
782        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        /// Tests whether a [`DirKeyCrossCert`] can be parsed properly.
791        #[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            // Try with `SIGNATURE`.
802            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            // Try with `ID SIGNATURE`.
816            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            // Try with different label and fail.
830            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            // Try with extra args.
845            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            // Test incorrect label.
882            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            // Test additional args.
898            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            // This is longclaw.
917
918            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            // Test a valid signature.
962            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)) // Wed Nov 12 12:24:53 CET 2025
969                        .unwrap(),
970                )
971                .unwrap();
972
973            // Test with an invalid authority.
974            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)) // Wed Nov 12 12:24:53 CET 2025
982                            .unwrap(),
983                    )
984                    .unwrap_err(),
985                VerifyFailed::InsufficientTrustedSigners
986            );
987
988            // Test a key too far in the future.
989            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            // Test an almost too new.
1005            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)) // 2025-08-17 20:34:03
1012                        .unwrap(),
1013                )
1014                .unwrap();
1015
1016            // Now fail when we are 1s below ...
1017            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)) // 2025-08-17 20:34:02
1028                            .unwrap(),
1029                    )
1030                    .unwrap_err(),
1031                VerifyFailed::TooNew
1032            );
1033
1034            // ... but succeed again with a clock skew tolerance.
1035            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)) // 2025-08-17 20:34:02
1042                        .unwrap(),
1043                )
1044                .unwrap();
1045
1046            // Test a key too old.
1047            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            // Test an almost too old.
1065            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)) // 2026-08-17 20:34:03
1072                        .unwrap(),
1073                )
1074                .unwrap();
1075
1076            // Now fail when we are 1s above ...
1077            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)) // 2026-08-17 20:34:04
1088                            .unwrap(),
1089                    )
1090                    .unwrap_err(),
1091                VerifyFailed::TooOld
1092            );
1093
1094            // ... but succeed again with a clock skew tolerance.
1095            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)) // 2026-08-17 20:34:04
1102                        .unwrap(),
1103                )
1104                .unwrap();
1105
1106            // Check with non-matching fingerprint and long-term identity key.
1107            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)) // Wed Nov 12 12:24:53 CET 2025
1119                        .unwrap(),
1120                )
1121                .unwrap_err(),
1122                VerifyFailed::Inconsistent
1123            );
1124
1125            // Check invalid cross-cert.
1126            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)) // Wed Nov 12 12:24:53 CET 2025
1138                        .unwrap(),
1139                )
1140                .unwrap_err(),
1141                VerifyFailed::VerifyFailed
1142            );
1143
1144            // Check outer signature.
1145            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)) // Wed Nov 12 12:24:53 CET 2025
1157                        .unwrap(),
1158                )
1159                .unwrap_err(),
1160                VerifyFailed::VerifyFailed
1161            );
1162        }
1163    }
1164}