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

            
9
use crate::batching_split_before::IteratorExt as _;
10
use crate::parse::keyword::Keyword;
11
use crate::parse::parser::{Section, SectionRules};
12
use crate::parse::tokenize::{ItemResult, NetDocReader};
13
use crate::types::misc::{Fingerprint, Iso8601TimeSp, RsaPublicParse1Helper};
14
use crate::util::str::Extent;
15
use crate::{NetdocErrorKind as EK, NormalItemArgument, Result};
16

            
17
use tor_checkable::{signed, timed};
18
use tor_llcrypto::pk::rsa;
19
use tor_llcrypto::{d, pk, pk::rsa::RsaIdentity};
20

            
21
use std::sync::LazyLock;
22

            
23
use std::result::Result as StdResult;
24
use std::{net, time, time::Duration, time::SystemTime};
25

            
26
use derive_deftly::Deftly;
27
use digest::Digest;
28

            
29
#[cfg(feature = "build_docs")]
30
mod build;
31

            
32
#[cfg(feature = "build_docs")]
33
#[allow(deprecated)]
34
pub use build::AuthCertBuilder;
35

            
36
#[cfg(feature = "parse2")]
37
use crate::parse2::{self, ItemObjectParseable, SignatureHashInputs};
38

            
39
// TODO DIRAUTH untangle these feature(s)
40
#[cfg(all(feature = "parse2", feature = "plain-consensus"))]
41
mod encoded;
42
#[cfg(all(feature = "parse2", feature = "plain-consensus"))]
43
pub use encoded::EncodedAuthCert;
44

            
45
decl_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.
61
51
static AUTHCERT_RULES: LazyLock<SectionRules<AuthCertKwd>> = LazyLock::new(|| {
62
    use AuthCertKwd::*;
63

            
64
51
    let mut rules = SectionRules::builder();
65
51
    rules.add(DIR_KEY_CERTIFICATE_VERSION.rule().required().args(1..));
66
51
    rules.add(DIR_ADDRESS.rule().args(1..));
67
51
    rules.add(FINGERPRINT.rule().required().args(1..));
68
51
    rules.add(DIR_IDENTITY_KEY.rule().required().no_args().obj_required());
69
51
    rules.add(DIR_SIGNING_KEY.rule().required().no_args().obj_required());
70
51
    rules.add(DIR_KEY_PUBLISHED.rule().required());
71
51
    rules.add(DIR_KEY_EXPIRES.rule().required());
72
51
    rules.add(DIR_KEY_CROSSCERT.rule().required().no_args().obj_required());
73
51
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
74
51
    rules.add(
75
51
        DIR_KEY_CERTIFICATION
76
51
            .rule()
77
51
            .required()
78
51
            .no_args()
79
51
            .obj_required(),
80
    );
81
51
    rules.build()
82
51
});
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)]
98
pub 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]
167
pub enum AuthCertVersion {
168
    /// The current and only version understood.
169
    #[strum(serialize = "3")]
170
    V3,
171
}
172

            
173
impl 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)]
178
pub 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.
187
pub 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

            
195
impl 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
147
    pub fn within<'a>(&self, haystack: &'a str) -> Option<&'a str> {
204
147
        self.location
205
147
            .as_ref()
206
150
            .and_then(|ext| ext.reconstruct(haystack))
207
147
    }
208
}
209

            
210
impl 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
10
    pub fn builder() -> AuthCertBuilder {
217
10
        AuthCertBuilder::new()
218
10
    }
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
63
    pub fn parse(s: &str) -> Result<UncheckedAuthCert> {
225
63
        let mut reader = NetDocReader::new(s)?;
226
63
        let body = AUTHCERT_RULES.parse(&mut reader)?;
227
63
        reader.should_be_exhausted()?;
228
68
        AuthCert::from_body(&body, s).map_err(|e| e.within(s))
229
63
    }
230

            
231
    /// Return an iterator yielding authority certificates from a string.
232
106
    pub fn parse_multiple(s: &str) -> Result<impl Iterator<Item = Result<UncheckedAuthCert>> + '_> {
233
        use AuthCertKwd::*;
234
106
        let sections = NetDocReader::new(s)?
235
314
            .batching_split_before_loose(|item| item.is_ok_with_kwd(DIR_KEY_CERTIFICATE_VERSION));
236
106
        Ok(sections
237
126
            .map(|mut section| {
238
26
                let body = AUTHCERT_RULES.parse(&mut section)?;
239
24
                AuthCert::from_body(&body, s)
240
26
            })
241
126
            .map(|r| r.map_err(|e| e.within(s))))
242
106
    }
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
114
    pub fn signing_key(&self) -> &rsa::PublicKey {
252
114
        &self.dir_signing_key
253
114
    }
254

            
255
    /// Return an AuthCertKeyIds object describing the keys in this
256
    /// certificate.
257
998
    pub fn key_ids(&self) -> AuthCertKeyIds {
258
998
        AuthCertKeyIds {
259
998
            id_fingerprint: self.fingerprint.0,
260
998
            sk_fingerprint: self.dir_signing_key.to_rsa_identity(),
261
998
        }
262
998
    }
263

            
264
    /// Return an RsaIdentity for this certificate's identity key.
265
16
    pub fn id_fingerprint(&self) -> &rsa::RsaIdentity {
266
16
        &self.fingerprint
267
16
    }
268

            
269
    /// Return the time when this certificate says it was published.
270
51
    pub fn published(&self) -> time::SystemTime {
271
51
        *self.dir_key_published
272
51
    }
273

            
274
    /// Return the time when this certificate says it should expire.
275
51
    pub fn expires(&self) -> time::SystemTime {
276
51
        *self.dir_key_expires
277
51
    }
278

            
279
    /// Parse an authority certificate from a reader.
280
181
    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
179
        let start_pos = {
288
            // Unwrap should be safe because `.parse()` would have already
289
            // returned an Error
290
            #[allow(clippy::unwrap_used)]
291
181
            let first_item = body.first_item().unwrap();
292
181
            if first_item.kwd() != DIR_KEY_CERTIFICATE_VERSION {
293
2
                return Err(EK::WrongStartingToken
294
2
                    .with_msg(first_item.kwd_str().to_string())
295
2
                    .at_pos(first_item.pos()));
296
179
            }
297
179
            first_item.pos()
298
        };
299
177
        let end_pos = {
300
            // Unwrap should be safe because `.parse()` would have already
301
            // returned an Error
302
            #[allow(clippy::unwrap_used)]
303
179
            let last_item = body.last_item().unwrap();
304
179
            if last_item.kwd() != DIR_KEY_CERTIFICATION {
305
2
                return Err(EK::WrongEndingToken
306
2
                    .with_msg(last_item.kwd_str().to_string())
307
2
                    .at_pos(last_item.pos()));
308
177
            }
309
177
            last_item.end_pos()
310
        };
311

            
312
177
        let version = body
313
177
            .required(DIR_KEY_CERTIFICATE_VERSION)?
314
177
            .parse_arg::<u32>(0)?;
315
177
        if version != 3 {
316
4
            return Err(EK::BadDocumentVersion.with_msg(format!("unexpected version {}", version)));
317
173
        }
318
173
        let dir_key_certificate_version = AuthCertVersion::V3;
319

            
320
173
        let dir_signing_key: rsa::PublicKey = body
321
173
            .required(DIR_SIGNING_KEY)?
322
173
            .parse_obj::<RsaPublicParse1Helper>("RSA PUBLIC KEY")?
323
173
            .check_len(1024..)?
324
173
            .check_exponent(65537)?
325
173
            .into();
326

            
327
173
        let dir_identity_key: rsa::PublicKey = body
328
173
            .required(DIR_IDENTITY_KEY)?
329
173
            .parse_obj::<RsaPublicParse1Helper>("RSA PUBLIC KEY")?
330
173
            .check_len(1024..)?
331
173
            .check_exponent(65537)?
332
173
            .into();
333

            
334
173
        let dir_key_published = body
335
173
            .required(DIR_KEY_PUBLISHED)?
336
173
            .args_as_str()
337
173
            .parse::<Iso8601TimeSp>()?;
338

            
339
173
        let dir_key_expires = body
340
173
            .required(DIR_KEY_EXPIRES)?
341
173
            .args_as_str()
342
173
            .parse::<Iso8601TimeSp>()?;
343

            
344
        {
345
            // Check fingerprint for consistency with key.
346
173
            let fp_tok = body.required(FINGERPRINT)?;
347
173
            let fingerprint: RsaIdentity = fp_tok.args_as_str().parse::<Fingerprint>()?.into();
348
173
            if fingerprint != dir_identity_key.to_rsa_identity() {
349
2
                return Err(EK::BadArgument
350
2
                    .at_pos(fp_tok.pos())
351
2
                    .with_msg("fingerprint does not match RSA identity"));
352
171
            }
353
        }
354

            
355
171
        let dir_address = body
356
171
            .maybe(DIR_ADDRESS)
357
171
            .parse_args_as_str::<net::SocketAddrV4>()?;
358

            
359
        // check crosscert
360
        let dir_key_crosscert;
361
169
        let v_crosscert = {
362
171
            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
171
            let mut tag = crosscert.obj_tag().unwrap();
367
            // we are required to support both.
368
171
            if tag != "ID SIGNATURE" && tag != "SIGNATURE" {
369
2
                tag = "ID SIGNATURE";
370
169
            }
371
171
            let sig = crosscert.obj(tag)?;
372

            
373
169
            let signed = dir_identity_key.to_rsa_identity();
374
            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
375

            
376
169
            let v = rsa::ValidatableRsaSignature::new(&dir_signing_key, &sig, signed.as_bytes());
377

            
378
169
            dir_key_crosscert = CrossCert {
379
169
                signature: CrossCertObject(sig),
380
169
            };
381

            
382
169
            v
383
        };
384

            
385
        // check the signature
386
169
        let v_sig = {
387
169
            let signature = body.required(DIR_KEY_CERTIFICATION)?;
388
169
            let sig = signature.obj("SIGNATURE")?;
389

            
390
169
            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
169
            let start_offset = body.first_item().unwrap().offset_in(s).unwrap();
395
            #[allow(clippy::unwrap_used)]
396
169
            let end_offset = body.last_item().unwrap().offset_in(s).unwrap();
397
169
            let end_offset = end_offset + "dir-key-certification\n".len();
398
169
            sha1.update(&s[start_offset..end_offset]);
399
169
            let sha1 = sha1.finalize();
400
            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
401

            
402
169
            rsa::ValidatableRsaSignature::new(&dir_identity_key, &sig, &sha1)
403
        };
404

            
405
169
        let id_fingerprint = dir_identity_key.to_rsa_identity();
406

            
407
169
        let location = {
408
169
            let start_idx = start_pos.offset_within(s);
409
169
            let end_idx = end_pos.offset_within(s);
410
169
            match (start_idx, end_idx) {
411
169
                (Some(a), Some(b)) => Extent::new(s, &s[a..b + 1]),
412
                _ => None,
413
            }
414
        };
415

            
416
169
        let authcert = AuthCert {
417
169
            dir_key_certificate_version,
418
169
            dir_address,
419
169
            dir_identity_key,
420
169
            dir_signing_key,
421
169
            dir_key_published,
422
169
            dir_key_expires,
423
169
            dir_key_crosscert,
424
169
            fingerprint: Fingerprint(id_fingerprint),
425
169
            __non_exhaustive: (),
426
169
        };
427

            
428
169
        let signatures: Vec<Box<dyn pk::ValidatableSignature>> =
429
169
            vec![Box::new(v_crosscert), Box::new(v_sig)];
430

            
431
169
        let timed = timed::TimerangeBound::new(authcert, *dir_key_published..*dir_key_expires);
432
169
        let signed = signed::SignatureGated::new(timed, signatures);
433
169
        let unchecked = UncheckedAuthCert {
434
169
            location,
435
169
            c: signed,
436
169
        };
437
169
        Ok(unchecked)
438
181
    }
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]
463
pub 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]
489
pub 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]
507
pub 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]
530
pub 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")]
541
impl ItemObjectParseable for CrossCertObject {
542
126
    fn check_label(label: &str) -> StdResult<(), parse2::EP> {
543
126
        match label {
544
126
            "SIGNATURE" | "ID SIGNATURE" => Ok(()),
545
2
            _ => Err(parse2::EP::ObjectIncorrectLabel),
546
        }
547
126
    }
548

            
549
124
    fn from_bytes(input: &[u8]) -> StdResult<Self, parse2::EP> {
550
124
        Ok(Self(input.to_vec()))
551
124
    }
552
}
553

            
554
impl tor_checkable::SelfSigned<timed::TimerangeBound<AuthCert>> for UncheckedAuthCert {
555
    type Error = signature::Error;
556

            
557
163
    fn dangerously_assume_wellsigned(self) -> timed::TimerangeBound<AuthCert> {
558
163
        self.c.dangerously_assume_wellsigned()
559
163
    }
560
163
    fn is_well_signed(&self) -> std::result::Result<(), Self::Error> {
561
163
        self.c.is_well_signed()
562
163
    }
563
}
564

            
565
#[cfg(feature = "parse2")]
566
impl 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
75
    pub fn verify_self_signed(
585
75
        self,
586
75
        v3idents: &[RsaIdentity],
587
75
        pre_tolerance: Duration,
588
75
        post_tolerance: Duration,
589
75
        now: SystemTime,
590
75
    ) -> StdResult<AuthCert, parse2::VerifyFailed> {
591
75
        let (body, signatures) = (self.body, self.signatures);
592

            
593
        // (1) Check whether this comes from a valid authority in `v3idents`.
594
75
        if !v3idents.contains(&body.fingerprint.0) {
595
2
            return Err(parse2::VerifyFailed::InsufficientTrustedSigners);
596
73
        }
597

            
598
        // (2) Check whether the timestamps are valid (± tolerance).
599
73
        let validity = *body.dir_key_published..=*body.dir_key_expires;
600
73
        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
65
        if body.dir_identity_key.to_rsa_identity() != *body.fingerprint {
604
2
            return Err(parse2::VerifyFailed::Inconsistent);
605
63
        }
606

            
607
        // (4) Check the cross-certificate (proof-of-ownership of signing key).
608
63
        body.dir_signing_key.verify(
609
63
            body.fingerprint.0.as_bytes(),
610
63
            &body.dir_key_crosscert.signature,
611
2
        )?;
612

            
613
        // (5) Check the outer certificate (proof-of-ownership of identity key).
614
61
        body.dir_identity_key.verify(
615
61
            &signatures.dir_key_certification.hash,
616
61
            &signatures.dir_key_certification.signature,
617
2
        )?;
618

            
619
59
        Ok(body)
620
75
    }
621
}
622

            
623
#[cfg(test)]
624
mod 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
                    )
                    .unwrap_err(),
                VerifyFailed::TooNew
            );
            // Test an almost too new.
            res.clone()
                .verify_self_signed(
                    &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
                    Duration::ZERO,
                    Duration::ZERO,
                    SystemTime::UNIX_EPOCH
                        .checked_add(Duration::from_secs(1755462843)) // 2025-08-17 20:34:03
                        .unwrap(),
                )
                .unwrap();
            // Now fail when we are 1s below ...
            assert_eq!(
                res.clone()
                    .verify_self_signed(
                        &[
                            RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
                                .unwrap()
                        ],
                        Duration::ZERO,
                        Duration::ZERO,
                        SystemTime::UNIX_EPOCH
                            .checked_add(Duration::from_secs(1755462842)) // 2025-08-17 20:34:02
                            .unwrap(),
                    )
                    .unwrap_err(),
                VerifyFailed::TooNew
            );
            // ... but succeed again with a clock skew tolerance.
            res.clone()
                .verify_self_signed(
                    &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
                    Duration::from_secs(1),
                    Duration::ZERO,
                    SystemTime::UNIX_EPOCH
                        .checked_add(Duration::from_secs(1755462842)) // 2025-08-17 20:34:02
                        .unwrap(),
                )
                .unwrap();
            // Test a key too old.
            assert_eq!(
                res.clone()
                    .verify_self_signed(
                        &[
                            RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
                                .unwrap()
                        ],
                        Duration::ZERO,
                        Duration::ZERO,
                        SystemTime::UNIX_EPOCH
                            .checked_add(Duration::from_secs(2000000000))
                            .unwrap(),
                    )
                    .unwrap_err(),
                VerifyFailed::TooOld
            );
            // Test an almost too old.
            res.clone()
                .verify_self_signed(
                    &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
                    Duration::ZERO,
                    Duration::ZERO,
                    SystemTime::UNIX_EPOCH
                        .checked_add(Duration::from_secs(1786998843)) // 2026-08-17 20:34:03
                        .unwrap(),
                )
                .unwrap();
            // Now fail when we are 1s above ...
            assert_eq!(
                res.clone()
                    .verify_self_signed(
                        &[
                            RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
                                .unwrap()
                        ],
                        Duration::ZERO,
                        Duration::ZERO,
                        SystemTime::UNIX_EPOCH
                            .checked_add(Duration::from_secs(1786998844)) // 2026-08-17 20:34:04
                            .unwrap(),
                    )
                    .unwrap_err(),
                VerifyFailed::TooOld
            );
            // ... but succeed again with a clock skew tolerance.
            res.clone()
                .verify_self_signed(
                    &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
                    Duration::ZERO,
                    Duration::from_secs(1),
                    SystemTime::UNIX_EPOCH
                        .checked_add(Duration::from_secs(1786998844)) // 2026-08-17 20:34:04
                        .unwrap(),
                )
                .unwrap();
            // Check with non-matching fingerprint and long-term identity key.
            let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
                include_str!("../../testdata2/authcert-longclaw-full-invalid-id-rsa"),
                "",
            ))
            .unwrap();
            assert_eq!(
                res.verify_self_signed(
                    &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
                    Duration::ZERO,
                    Duration::ZERO,
                    SystemTime::UNIX_EPOCH
                        .checked_add(Duration::from_secs(1762946693)) // Wed Nov 12 12:24:53 CET 2025
                        .unwrap(),
                )
                .unwrap_err(),
                VerifyFailed::Inconsistent
            );
            // Check invalid cross-cert.
            let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
                include_str!("../../testdata2/authcert-longclaw-full-invalid-cross"),
                "",
            ))
            .unwrap();
            assert_eq!(
                res.verify_self_signed(
                    &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
                    Duration::ZERO,
                    Duration::ZERO,
                    SystemTime::UNIX_EPOCH
                        .checked_add(Duration::from_secs(1762946693)) // Wed Nov 12 12:24:53 CET 2025
                        .unwrap(),
                )
                .unwrap_err(),
                VerifyFailed::VerifyFailed
            );
            // Check outer signature.
            let res = parse2::parse_netdoc::<AuthCertSigned>(&ParseInput::new(
                include_str!("../../testdata2/authcert-longclaw-full-invalid-certification"),
                "",
            ))
            .unwrap();
            assert_eq!(
                res.verify_self_signed(
                    &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
                    Duration::ZERO,
                    Duration::ZERO,
                    SystemTime::UNIX_EPOCH
                        .checked_add(Duration::from_secs(1762946693)) // Wed Nov 12 12:24:53 CET 2025
                        .unwrap(),
                )
                .unwrap_err(),
                VerifyFailed::VerifyFailed
            );
        }
    }
}