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, RsaPublic};
14use crate::util::str::Extent;
15use crate::{NetdocErrorKind as EK, Result};
16
17use tor_checkable::{signed, timed};
18use tor_llcrypto::pk::rsa;
19use tor_llcrypto::{d, pk, pk::rsa::RsaIdentity};
20
21use once_cell::sync::Lazy;
22
23use std::{net, time};
24
25use digest::Digest;
26
27#[cfg(feature = "build_docs")]
28mod build;
29
30#[cfg(feature = "build_docs")]
31pub use build::AuthCertBuilder;
32
33decl_keyword! {
34    pub(crate) AuthCertKwd {
35        "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
36        "dir-address" => DIR_ADDRESS,
37        "fingerprint" => FINGERPRINT,
38        "dir-identity-key" => DIR_IDENTITY_KEY,
39        "dir-key-published" => DIR_KEY_PUBLISHED,
40        "dir-key-expires" => DIR_KEY_EXPIRES,
41        "dir-signing-key" => DIR_SIGNING_KEY,
42        "dir-key-crosscert" => DIR_KEY_CROSSCERT,
43        "dir-key-certification" => DIR_KEY_CERTIFICATION,
44    }
45}
46
47/// Rules about entries that must appear in an AuthCert, and how they must
48/// be formed.
49static AUTHCERT_RULES: Lazy<SectionRules<AuthCertKwd>> = Lazy::new(|| {
50    use AuthCertKwd::*;
51
52    let mut rules = SectionRules::builder();
53    rules.add(DIR_KEY_CERTIFICATE_VERSION.rule().required().args(1..));
54    rules.add(DIR_ADDRESS.rule().args(1..));
55    rules.add(FINGERPRINT.rule().required().args(1..));
56    rules.add(DIR_IDENTITY_KEY.rule().required().no_args().obj_required());
57    rules.add(DIR_SIGNING_KEY.rule().required().no_args().obj_required());
58    rules.add(DIR_KEY_PUBLISHED.rule().required());
59    rules.add(DIR_KEY_EXPIRES.rule().required());
60    rules.add(DIR_KEY_CROSSCERT.rule().required().no_args().obj_required());
61    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
62    rules.add(
63        DIR_KEY_CERTIFICATION
64            .rule()
65            .required()
66            .no_args()
67            .obj_required(),
68    );
69    rules.build()
70});
71
72/// A single authority certificate.
73///
74/// Authority certificates bind a long-term RSA identity key from a
75/// directory authority to a medium-term signing key.  The signing
76/// keys are the ones used to sign votes and consensuses; the identity
77/// keys can be kept offline.
78#[allow(dead_code)]
79#[derive(Clone, Debug)]
80pub struct AuthCert {
81    /// An IPv4 address for this authority.
82    address: Option<net::SocketAddrV4>,
83    /// The long-term RSA identity key for this authority
84    identity_key: rsa::PublicKey,
85    /// The medium-term RSA signing key for this authority
86    signing_key: rsa::PublicKey,
87    /// Declared time when this certificate was published
88    published: time::SystemTime,
89    /// Declared time when this certificate expires.
90    expires: time::SystemTime,
91
92    /// Derived field: fingerprints of the certificate's keys
93    key_ids: AuthCertKeyIds,
94}
95
96/// A pair of key identities that identifies a certificate.
97#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
98#[allow(clippy::exhaustive_structs)]
99pub struct AuthCertKeyIds {
100    /// Fingerprint of identity key
101    pub id_fingerprint: rsa::RsaIdentity,
102    /// Fingerprint of signing key
103    pub sk_fingerprint: rsa::RsaIdentity,
104}
105
106/// An authority certificate whose signature and validity time we
107/// haven't checked.
108pub struct UncheckedAuthCert {
109    /// Where we found this AuthCert within the string containing it.
110    location: Option<Extent>,
111
112    /// The actual unchecked certificate.
113    c: signed::SignatureGated<timed::TimerangeBound<AuthCert>>,
114}
115
116impl UncheckedAuthCert {
117    /// If this AuthCert was originally parsed from `haystack`, return its
118    /// text.
119    ///
120    /// TODO: This is a pretty bogus interface; there should be a
121    /// better way to remember where to look for this thing if we want
122    /// it without keeping the input alive forever.  We should
123    /// refactor.
124    pub fn within<'a>(&self, haystack: &'a str) -> Option<&'a str> {
125        self.location
126            .as_ref()
127            .and_then(|ext| ext.reconstruct(haystack))
128    }
129}
130
131impl AuthCert {
132    /// Make an [`AuthCertBuilder`] object that can be used to
133    /// construct authority certificates for testing.
134    #[cfg(feature = "build_docs")]
135    pub fn builder() -> AuthCertBuilder {
136        AuthCertBuilder::new()
137    }
138
139    /// Parse an authority certificate from a string.
140    ///
141    /// This function verifies the certificate's signatures, but doesn't
142    /// check its expiration dates.
143    pub fn parse(s: &str) -> Result<UncheckedAuthCert> {
144        let mut reader = NetDocReader::new(s)?;
145        let body = AUTHCERT_RULES.parse(&mut reader)?;
146        reader.should_be_exhausted()?;
147        AuthCert::from_body(&body, s).map_err(|e| e.within(s))
148    }
149
150    /// Return an iterator yielding authority certificates from a string.
151    pub fn parse_multiple(s: &str) -> Result<impl Iterator<Item = Result<UncheckedAuthCert>> + '_> {
152        use AuthCertKwd::*;
153        let sections = NetDocReader::new(s)?
154            .batching_split_before_loose(|item| item.is_ok_with_kwd(DIR_KEY_CERTIFICATE_VERSION));
155        Ok(sections
156            .map(|mut section| {
157                let body = AUTHCERT_RULES.parse(&mut section)?;
158                AuthCert::from_body(&body, s)
159            })
160            .map(|r| r.map_err(|e| e.within(s))))
161    }
162    /*
163        /// Return true if this certificate is expired at a given time, or
164        /// not yet valid at that time.
165        pub fn is_expired_at(&self, when: time::SystemTime) -> bool {
166            when < self.published || when > self.expires
167        }
168    */
169    /// Return the signing key certified by this certificate.
170    pub fn signing_key(&self) -> &rsa::PublicKey {
171        &self.signing_key
172    }
173
174    /// Return an AuthCertKeyIds object describing the keys in this
175    /// certificate.
176    pub fn key_ids(&self) -> &AuthCertKeyIds {
177        &self.key_ids
178    }
179
180    /// Return an RsaIdentity for this certificate's identity key.
181    pub fn id_fingerprint(&self) -> &rsa::RsaIdentity {
182        &self.key_ids.id_fingerprint
183    }
184
185    /// Return an RsaIdentity for this certificate's signing key.
186    pub fn sk_fingerprint(&self) -> &rsa::RsaIdentity {
187        &self.key_ids.sk_fingerprint
188    }
189
190    /// Return the time when this certificate says it was published.
191    pub fn published(&self) -> time::SystemTime {
192        self.published
193    }
194
195    /// Return the time when this certificate says it should expire.
196    pub fn expires(&self) -> time::SystemTime {
197        self.expires
198    }
199
200    /// Parse an authority certificate from a reader.
201    fn from_body(body: &Section<'_, AuthCertKwd>, s: &str) -> Result<UncheckedAuthCert> {
202        use AuthCertKwd::*;
203
204        // Make sure first and last element are correct types.  We can
205        // safely call unwrap() on first and last, since there are required
206        // tokens in the rules, so we know that at least one token will have
207        // been parsed.
208        let start_pos = {
209            // Unwrap should be safe because `.parse()` would have already
210            // returned an Error
211            #[allow(clippy::unwrap_used)]
212            let first_item = body.first_item().unwrap();
213            if first_item.kwd() != DIR_KEY_CERTIFICATE_VERSION {
214                return Err(EK::WrongStartingToken
215                    .with_msg(first_item.kwd_str().to_string())
216                    .at_pos(first_item.pos()));
217            }
218            first_item.pos()
219        };
220        let end_pos = {
221            // Unwrap should be safe because `.parse()` would have already
222            // returned an Error
223            #[allow(clippy::unwrap_used)]
224            let last_item = body.last_item().unwrap();
225            if last_item.kwd() != DIR_KEY_CERTIFICATION {
226                return Err(EK::WrongEndingToken
227                    .with_msg(last_item.kwd_str().to_string())
228                    .at_pos(last_item.pos()));
229            }
230            last_item.end_pos()
231        };
232
233        let version = body
234            .required(DIR_KEY_CERTIFICATE_VERSION)?
235            .parse_arg::<u32>(0)?;
236        if version != 3 {
237            return Err(EK::BadDocumentVersion.with_msg(format!("unexpected version {}", version)));
238        }
239
240        let signing_key: rsa::PublicKey = body
241            .required(DIR_SIGNING_KEY)?
242            .parse_obj::<RsaPublic>("RSA PUBLIC KEY")?
243            .check_len(1024..)?
244            .check_exponent(65537)?
245            .into();
246
247        let identity_key: rsa::PublicKey = body
248            .required(DIR_IDENTITY_KEY)?
249            .parse_obj::<RsaPublic>("RSA PUBLIC KEY")?
250            .check_len(1024..)?
251            .check_exponent(65537)?
252            .into();
253
254        let published = body
255            .required(DIR_KEY_PUBLISHED)?
256            .args_as_str()
257            .parse::<Iso8601TimeSp>()?
258            .into();
259
260        let expires = body
261            .required(DIR_KEY_EXPIRES)?
262            .args_as_str()
263            .parse::<Iso8601TimeSp>()?
264            .into();
265
266        {
267            // Check fingerprint for consistency with key.
268            let fp_tok = body.required(FINGERPRINT)?;
269            let fingerprint: RsaIdentity = fp_tok.args_as_str().parse::<Fingerprint>()?.into();
270            if fingerprint != identity_key.to_rsa_identity() {
271                return Err(EK::BadArgument
272                    .at_pos(fp_tok.pos())
273                    .with_msg("fingerprint does not match RSA identity"));
274            }
275        }
276
277        let address = body
278            .maybe(DIR_ADDRESS)
279            .parse_args_as_str::<net::SocketAddrV4>()?;
280
281        // check crosscert
282        let v_crosscert = {
283            let crosscert = body.required(DIR_KEY_CROSSCERT)?;
284            // Unwrap should be safe because `.parse()` and `required()` would
285            // have already returned an Error
286            #[allow(clippy::unwrap_used)]
287            let mut tag = crosscert.obj_tag().unwrap();
288            // we are required to support both.
289            if tag != "ID SIGNATURE" && tag != "SIGNATURE" {
290                tag = "ID SIGNATURE";
291            }
292            let sig = crosscert.obj(tag)?;
293
294            let signed = identity_key.to_rsa_identity();
295            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
296
297            rsa::ValidatableRsaSignature::new(&signing_key, &sig, signed.as_bytes())
298        };
299
300        // check the signature
301        let v_sig = {
302            let signature = body.required(DIR_KEY_CERTIFICATION)?;
303            let sig = signature.obj("SIGNATURE")?;
304
305            let mut sha1 = d::Sha1::new();
306            // Unwrap should be safe because `.parse()` would have already
307            // returned an Error
308            #[allow(clippy::unwrap_used)]
309            let start_offset = body.first_item().unwrap().offset_in(s).unwrap();
310            #[allow(clippy::unwrap_used)]
311            let end_offset = body.last_item().unwrap().offset_in(s).unwrap();
312            let end_offset = end_offset + "dir-key-certification\n".len();
313            sha1.update(&s[start_offset..end_offset]);
314            let sha1 = sha1.finalize();
315            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
316
317            rsa::ValidatableRsaSignature::new(&identity_key, &sig, &sha1)
318        };
319
320        let id_fingerprint = identity_key.to_rsa_identity();
321        let sk_fingerprint = signing_key.to_rsa_identity();
322        let key_ids = AuthCertKeyIds {
323            id_fingerprint,
324            sk_fingerprint,
325        };
326
327        let location = {
328            let start_idx = start_pos.offset_within(s);
329            let end_idx = end_pos.offset_within(s);
330            match (start_idx, end_idx) {
331                (Some(a), Some(b)) => Extent::new(s, &s[a..b + 1]),
332                _ => None,
333            }
334        };
335
336        let authcert = AuthCert {
337            address,
338            identity_key,
339            signing_key,
340            published,
341            expires,
342            key_ids,
343        };
344
345        let signatures: Vec<Box<dyn pk::ValidatableSignature>> =
346            vec![Box::new(v_crosscert), Box::new(v_sig)];
347
348        let timed = timed::TimerangeBound::new(authcert, published..expires);
349        let signed = signed::SignatureGated::new(timed, signatures);
350        let unchecked = UncheckedAuthCert {
351            location,
352            c: signed,
353        };
354        Ok(unchecked)
355    }
356}
357
358impl tor_checkable::SelfSigned<timed::TimerangeBound<AuthCert>> for UncheckedAuthCert {
359    type Error = signature::Error;
360
361    fn dangerously_assume_wellsigned(self) -> timed::TimerangeBound<AuthCert> {
362        self.c.dangerously_assume_wellsigned()
363    }
364    fn is_well_signed(&self) -> std::result::Result<(), Self::Error> {
365        self.c.is_well_signed()
366    }
367}
368
369#[cfg(test)]
370mod test {
371    // @@ begin test lint list maintained by maint/add_warning @@
372    #![allow(clippy::bool_assert_comparison)]
373    #![allow(clippy::clone_on_copy)]
374    #![allow(clippy::dbg_macro)]
375    #![allow(clippy::mixed_attributes_style)]
376    #![allow(clippy::print_stderr)]
377    #![allow(clippy::print_stdout)]
378    #![allow(clippy::single_char_pattern)]
379    #![allow(clippy::unwrap_used)]
380    #![allow(clippy::unchecked_duration_subtraction)]
381    #![allow(clippy::useless_vec)]
382    #![allow(clippy::needless_pass_by_value)]
383    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
384    use super::*;
385    use crate::{Error, Pos};
386    const TESTDATA: &str = include_str!("../../testdata/authcert1.txt");
387
388    fn bad_data(fname: &str) -> String {
389        use std::fs;
390        use std::path::PathBuf;
391        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
392        path.push("testdata");
393        path.push("bad-certs");
394        path.push(fname);
395
396        fs::read_to_string(path).unwrap()
397    }
398
399    #[test]
400    fn parse_one() -> Result<()> {
401        use tor_checkable::{SelfSigned, Timebound};
402        let cert = AuthCert::parse(TESTDATA)?
403            .check_signature()
404            .unwrap()
405            .dangerously_assume_timely();
406
407        // Taken from TESTDATA
408        assert_eq!(
409            cert.id_fingerprint().to_string(),
410            "$ed03bb616eb2f60bec80151114bb25cef515b226"
411        );
412        assert_eq!(
413            cert.sk_fingerprint().to_string(),
414            "$c4f720e2c59f9ddd4867fff465ca04031e35648f"
415        );
416
417        Ok(())
418    }
419
420    #[test]
421    fn parse_bad() {
422        fn check(fname: &str, err: &Error) {
423            let contents = bad_data(fname);
424            let cert = AuthCert::parse(&contents);
425            assert!(cert.is_err());
426            assert_eq!(&cert.err().unwrap(), err);
427        }
428
429        check(
430            "bad-cc-tag",
431            &EK::WrongObject.at_pos(Pos::from_line(27, 12)),
432        );
433        check(
434            "bad-fingerprint",
435            &EK::BadArgument
436                .at_pos(Pos::from_line(2, 1))
437                .with_msg("fingerprint does not match RSA identity"),
438        );
439        check(
440            "bad-version",
441            &EK::BadDocumentVersion.with_msg("unexpected version 4"),
442        );
443        check(
444            "wrong-end",
445            &EK::WrongEndingToken
446                .with_msg("dir-key-crosscert")
447                .at_pos(Pos::from_line(37, 1)),
448        );
449        check(
450            "wrong-start",
451            &EK::WrongStartingToken
452                .with_msg("fingerprint")
453                .at_pos(Pos::from_line(1, 1)),
454        );
455    }
456
457    #[test]
458    fn test_recovery_1() {
459        let mut data = "<><><<><>\nfingerprint ABC\n".to_string();
460        data += TESTDATA;
461
462        let res: Vec<Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
463
464        // We should recover from the failed case and read the next data fine.
465        assert!(res[0].is_err());
466        assert!(res[1].is_ok());
467        assert_eq!(res.len(), 2);
468    }
469
470    #[test]
471    fn test_recovery_2() {
472        let mut data = bad_data("bad-version");
473        data += TESTDATA;
474
475        let res: Vec<Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
476
477        // We should recover from the failed case and read the next data fine.
478        assert!(res[0].is_err());
479        assert!(res[1].is_ok());
480        assert_eq!(res.len(), 2);
481    }
482}