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::{
38
    self, ItemObjectParseable, SignatureHashInputs, sig_hashes::Sha1WholeKeywordLine,
39
};
40

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

            
47
decl_keyword! {
48
    pub(crate) AuthCertKwd {
49
        "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
50
        "dir-address" => DIR_ADDRESS,
51
        "fingerprint" => FINGERPRINT,
52
        "dir-identity-key" => DIR_IDENTITY_KEY,
53
        "dir-key-published" => DIR_KEY_PUBLISHED,
54
        "dir-key-expires" => DIR_KEY_EXPIRES,
55
        "dir-signing-key" => DIR_SIGNING_KEY,
56
        "dir-key-crosscert" => DIR_KEY_CROSSCERT,
57
        "dir-key-certification" => DIR_KEY_CERTIFICATION,
58
    }
59
}
60

            
61
/// Rules about entries that must appear in an AuthCert, and how they must
62
/// be formed.
63
55
static AUTHCERT_RULES: LazyLock<SectionRules<AuthCertKwd>> = LazyLock::new(|| {
64
    use AuthCertKwd::*;
65

            
66
55
    let mut rules = SectionRules::builder();
67
55
    rules.add(DIR_KEY_CERTIFICATE_VERSION.rule().required().args(1..));
68
55
    rules.add(DIR_ADDRESS.rule().args(1..));
69
55
    rules.add(FINGERPRINT.rule().required().args(1..));
70
55
    rules.add(DIR_IDENTITY_KEY.rule().required().no_args().obj_required());
71
55
    rules.add(DIR_SIGNING_KEY.rule().required().no_args().obj_required());
72
55
    rules.add(DIR_KEY_PUBLISHED.rule().required());
73
55
    rules.add(DIR_KEY_EXPIRES.rule().required());
74
55
    rules.add(DIR_KEY_CROSSCERT.rule().required().no_args().obj_required());
75
55
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
76
55
    rules.add(
77
55
        DIR_KEY_CERTIFICATION
78
55
            .rule()
79
55
            .required()
80
55
            .no_args()
81
55
            .obj_required(),
82
    );
83
55
    rules.build()
84
55
});
85

            
86
/// A single directory authority key certificate
87
///
88
/// This is the body, not including signatures.
89
///
90
/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html>
91
///
92
/// To make a fresh `AuthCert`, use [`AuthCertConstructor`].
93
#[derive(Clone, Debug, Deftly)]
94
#[derive_deftly(Constructor)]
95
#[cfg_attr(feature = "parse2", derive_deftly(NetdocParseableUnverified))]
96
// derive_deftly_adhoc disables unused deftly attribute checking, so we needn't cfg_attr them all
97
#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
98
#[cfg_attr(test, derive(PartialEq, Eq))]
99
#[allow(clippy::manual_non_exhaustive)]
100
pub struct AuthCert {
101
    /// Intro line
102
    ///
103
    /// Currently must be version 3.
104
    ///
105
    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certificate-version>
106
    #[deftly(constructor(default = "AuthCertVersion::V3"))]
107
    #[deftly(netdoc(single_arg))]
108
    pub dir_key_certificate_version: AuthCertVersion,
109

            
110
    /// An IPv4 address for this authority.
111
    #[deftly(netdoc(single_arg))]
112
    pub dir_address: Option<net::SocketAddrV4>,
113

            
114
    /// H(KP_auth_id_rsa)
115
    ///
116
    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:fingerprint>
117
    #[deftly(constructor)]
118
    #[deftly(netdoc(single_arg))]
119
    pub fingerprint: Fingerprint,
120

            
121
    /// Declared time when this certificate was published
122
    ///
123
    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-published>
124
    #[deftly(constructor)]
125
    #[deftly(netdoc(single_arg))]
126
    pub dir_key_published: Iso8601TimeSp,
127

            
128
    /// Declared time when this certificate expires.
129
    ///
130
    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-expires>
131
    #[deftly(constructor)]
132
    #[deftly(netdoc(single_arg))]
133
    pub dir_key_expires: Iso8601TimeSp,
134

            
135
    /// KP_auth_id_rsa
136
    ///
137
    /// The long-term RSA identity key for this authority
138
    ///
139
    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-identity-key>
140
    #[deftly(constructor)]
141
    pub dir_identity_key: rsa::PublicKey,
142

            
143
    /// KP_auth_sign_rsa
144
    ///
145
    /// The medium-term RSA signing key for this authority
146
    ///
147
    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-signing-key>
148
    #[deftly(constructor)]
149
    pub dir_signing_key: rsa::PublicKey,
150

            
151
    /// SHA1(DER(KP_auth_id_rsa)) signed by KP_auth_sign_rsa
152
    ///
153
    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-crosscert>
154
    #[deftly(constructor)]
155
    pub dir_key_crosscert: CrossCert,
156

            
157
    #[doc(hidden)]
158
    #[deftly(netdoc(skip))]
159
    __non_exhaustive: (),
160
}
161

            
162
/// Represents the version of an [`AuthCert`].
163
///
164
/// Single argument.
165
///
166
/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certificate-version>
167
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, strum::EnumString, strum::Display)]
168
#[non_exhaustive]
169
pub enum AuthCertVersion {
170
    /// The current and only version understood.
171
    #[strum(serialize = "3")]
172
    V3,
173
}
174

            
175
impl NormalItemArgument for AuthCertVersion {}
176

            
177
/// A pair of key identities that identifies a certificate.
178
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
179
#[allow(clippy::exhaustive_structs)]
180
pub struct AuthCertKeyIds {
181
    /// Fingerprint of identity key
182
    pub id_fingerprint: rsa::RsaIdentity,
183
    /// Fingerprint of signing key
184
    pub sk_fingerprint: rsa::RsaIdentity,
185
}
186

            
187
/// An authority certificate whose signature and validity time we
188
/// haven't checked.
189
pub struct UncheckedAuthCert {
190
    /// Where we found this AuthCert within the string containing it.
191
    location: Option<Extent>,
192

            
193
    /// The actual unchecked certificate.
194
    c: signed::SignatureGated<timed::TimerangeBound<AuthCert>>,
195
}
196

            
197
impl UncheckedAuthCert {
198
    /// If this AuthCert was originally parsed from `haystack`, return its
199
    /// text.
200
    ///
201
    /// TODO: This is a pretty bogus interface; there should be a
202
    /// better way to remember where to look for this thing if we want
203
    /// it without keeping the input alive forever.  We should
204
    /// refactor.
205
159
    pub fn within<'a>(&self, haystack: &'a str) -> Option<&'a str> {
206
159
        self.location
207
159
            .as_ref()
208
162
            .and_then(|ext| ext.reconstruct(haystack))
209
159
    }
210
}
211

            
212
impl AuthCert {
213
    /// Make an [`AuthCertBuilder`] object that can be used to
214
    /// construct authority certificates for testing.
215
    #[cfg(feature = "build_docs")]
216
    #[deprecated = "use AuthCertConstructor instead"]
217
    #[allow(deprecated)]
218
10
    pub fn builder() -> AuthCertBuilder {
219
10
        AuthCertBuilder::new()
220
10
    }
221

            
222
    /// Parse an authority certificate from a string.
223
    ///
224
    /// This function verifies the certificate's signatures, but doesn't
225
    /// check its expiration dates.
226
67
    pub fn parse(s: &str) -> Result<UncheckedAuthCert> {
227
67
        let mut reader = NetDocReader::new(s)?;
228
67
        let body = AUTHCERT_RULES.parse(&mut reader)?;
229
67
        reader.should_be_exhausted()?;
230
72
        AuthCert::from_body(&body, s).map_err(|e| e.within(s))
231
67
    }
232

            
233
    /// Return an iterator yielding authority certificates from a string.
234
114
    pub fn parse_multiple(s: &str) -> Result<impl Iterator<Item = Result<UncheckedAuthCert>> + '_> {
235
        use AuthCertKwd::*;
236
114
        let sections = NetDocReader::new(s)?
237
322
            .batching_split_before_loose(|item| item.is_ok_with_kwd(DIR_KEY_CERTIFICATE_VERSION));
238
114
        Ok(sections
239
134
            .map(|mut section| {
240
26
                let body = AUTHCERT_RULES.parse(&mut section)?;
241
24
                AuthCert::from_body(&body, s)
242
26
            })
243
134
            .map(|r| r.map_err(|e| e.within(s))))
244
114
    }
245
    /*
246
        /// Return true if this certificate is expired at a given time, or
247
        /// not yet valid at that time.
248
        pub fn is_expired_at(&self, when: time::SystemTime) -> bool {
249
            when < self.published || when > self.expires
250
        }
251
    */
252
    /// Return the signing key certified by this certificate.
253
122
    pub fn signing_key(&self) -> &rsa::PublicKey {
254
122
        &self.dir_signing_key
255
122
    }
256

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

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

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

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

            
281
    /// Parse an authority certificate from a reader.
282
193
    fn from_body(body: &Section<'_, AuthCertKwd>, s: &str) -> Result<UncheckedAuthCert> {
283
        use AuthCertKwd::*;
284

            
285
        // Make sure first and last element are correct types.  We can
286
        // safely call unwrap() on first and last, since there are required
287
        // tokens in the rules, so we know that at least one token will have
288
        // been parsed.
289
191
        let start_pos = {
290
            // Unwrap should be safe because `.parse()` would have already
291
            // returned an Error
292
            #[allow(clippy::unwrap_used)]
293
193
            let first_item = body.first_item().unwrap();
294
193
            if first_item.kwd() != DIR_KEY_CERTIFICATE_VERSION {
295
2
                return Err(EK::WrongStartingToken
296
2
                    .with_msg(first_item.kwd_str().to_string())
297
2
                    .at_pos(first_item.pos()));
298
191
            }
299
191
            first_item.pos()
300
        };
301
189
        let end_pos = {
302
            // Unwrap should be safe because `.parse()` would have already
303
            // returned an Error
304
            #[allow(clippy::unwrap_used)]
305
191
            let last_item = body.last_item().unwrap();
306
191
            if last_item.kwd() != DIR_KEY_CERTIFICATION {
307
2
                return Err(EK::WrongEndingToken
308
2
                    .with_msg(last_item.kwd_str().to_string())
309
2
                    .at_pos(last_item.pos()));
310
189
            }
311
189
            last_item.end_pos()
312
        };
313

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

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

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

            
336
185
        let dir_key_published = body
337
185
            .required(DIR_KEY_PUBLISHED)?
338
185
            .args_as_str()
339
185
            .parse::<Iso8601TimeSp>()?;
340

            
341
185
        let dir_key_expires = body
342
185
            .required(DIR_KEY_EXPIRES)?
343
185
            .args_as_str()
344
185
            .parse::<Iso8601TimeSp>()?;
345

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

            
357
183
        let dir_address = body
358
183
            .maybe(DIR_ADDRESS)
359
183
            .parse_args_as_str::<net::SocketAddrV4>()?;
360

            
361
        // check crosscert
362
        let dir_key_crosscert;
363
181
        let v_crosscert = {
364
183
            let crosscert = body.required(DIR_KEY_CROSSCERT)?;
365
            // Unwrap should be safe because `.parse()` and `required()` would
366
            // have already returned an Error
367
            #[allow(clippy::unwrap_used)]
368
183
            let mut tag = crosscert.obj_tag().unwrap();
369
            // we are required to support both.
370
183
            if tag != "ID SIGNATURE" && tag != "SIGNATURE" {
371
2
                tag = "ID SIGNATURE";
372
181
            }
373
183
            let sig = crosscert.obj(tag)?;
374

            
375
181
            let signed = dir_identity_key.to_rsa_identity();
376
            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
377

            
378
181
            let v = rsa::ValidatableRsaSignature::new(&dir_signing_key, &sig, signed.as_bytes());
379

            
380
181
            dir_key_crosscert = CrossCert {
381
181
                signature: CrossCertObject(sig),
382
181
            };
383

            
384
181
            v
385
        };
386

            
387
        // check the signature
388
181
        let v_sig = {
389
181
            let signature = body.required(DIR_KEY_CERTIFICATION)?;
390
181
            let sig = signature.obj("SIGNATURE")?;
391

            
392
181
            let mut sha1 = d::Sha1::new();
393
            // Unwrap should be safe because `.parse()` would have already
394
            // returned an Error
395
            #[allow(clippy::unwrap_used)]
396
181
            let start_offset = body.first_item().unwrap().offset_in(s).unwrap();
397
            #[allow(clippy::unwrap_used)]
398
181
            let end_offset = body.last_item().unwrap().offset_in(s).unwrap();
399
181
            let end_offset = end_offset + "dir-key-certification\n".len();
400
181
            sha1.update(&s[start_offset..end_offset]);
401
181
            let sha1 = sha1.finalize();
402
            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
403

            
404
181
            rsa::ValidatableRsaSignature::new(&dir_identity_key, &sig, &sha1)
405
        };
406

            
407
181
        let id_fingerprint = dir_identity_key.to_rsa_identity();
408

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

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

            
430
181
        let signatures: Vec<Box<dyn pk::ValidatableSignature>> =
431
181
            vec![Box::new(v_crosscert), Box::new(v_sig)];
432

            
433
181
        let timed = timed::TimerangeBound::new(authcert, *dir_key_published..*dir_key_expires);
434
181
        let signed = signed::SignatureGated::new(timed, signatures);
435
181
        let unchecked = UncheckedAuthCert {
436
181
            location,
437
181
            c: signed,
438
181
        };
439
181
        Ok(unchecked)
440
193
    }
441
}
442

            
443
/// Pseudo-Signature of the long-term identity key by the medium-term key.
444
///
445
/// This type does not implement `SignatureItemParseable` because that trait
446
/// is reserved for signatures on *netdocs*, such as [`AuthCertSignature`].
447
/// As `CrossCert` does not sign a full document, it implements only
448
/// `ItemValueParseable`, instead.
449
///
450
/// Verification of this signature is done in `AuthCertUnverified::verify_self_signed`,
451
/// and during parsing by the old parser.
452
/// So a `CrossCert` in [`AuthCert::dir_key_crosscert`] in a bare `AuthCert` has been validated.
453
//
454
// TODO SPEC (Diziet): it is far from clear to me that this cert serves any useful purpose.
455
// However, we are far too busy now with rewriting the universe to consider transitioning it away.
456
#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
457
#[cfg_attr(
458
    feature = "parse2",
459
    derive_deftly(ItemValueParseable),
460
    deftly(netdoc(no_extra_args))
461
)]
462
// derive_deftly_adhoc disables unused deftly attribute checking, so we needn't cfg_attr them all
463
#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
464
#[non_exhaustive]
465
pub struct CrossCert {
466
    /// The bytes of the signature (base64-decoded).
467
    #[deftly(netdoc(object))]
468
    pub signature: CrossCertObject,
469
}
470

            
471
/// Wrapper around [`Vec<u8>`] implementing [`ItemObjectParseable`] properly.
472
///
473
/// Unfortunately, this wrapper is necessary, because the specification
474
/// demands that these certificate objects must accept two labels:
475
/// `SIGNATURE` and `ID SIGNATURE`.  Because the deftly template for
476
/// `ItemValueParseable` only allows for a single label
477
/// (`#[deftly(netdoc(object(label = "LABEL")))]`), we must implement this
478
/// trait ourselves in order to allow multiple ones.
479
///
480
/// TODO: In the future, it might be nice to let the respective fmeta
481
/// accept a pattern, as pattern matching would allow trivially for one
482
/// to infinity different combinations.
483
/// TODO SPEC: Alternatively we could abolish the wrong labels,
484
/// or we could abolish Objects completely and just have long lines.
485
///
486
/// # Specifications
487
///
488
/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-crosscert>
489
#[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)]
490
#[non_exhaustive]
491
pub struct CrossCertObject(pub Vec<u8>);
492

            
493
/// Signatures for [`AuthCert`]
494
///
495
/// Signed by [`AuthCert::dir_identity_key`] in order to prove ownership.
496
/// Can be seen as the opposite of [`AuthCert::dir_key_crosscert`].
497
///
498
/// # Specifications
499
///
500
/// * <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certification>
501
/// * <https://spec.torproject.org/dir-spec/netdoc.html#signing>
502
#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
503
#[cfg_attr(
504
    feature = "parse2",
505
    derive_deftly(NetdocParseableSignatures),
506
    deftly(netdoc(signatures(hashes_accu = "Sha1WholeKeywordLine")))
507
)]
508
#[non_exhaustive]
509
pub struct AuthCertSignatures {
510
    /// Contains the actual signature, see [`AuthCertSignatures`].
511
    pub dir_key_certification: AuthCertSignature,
512
}
513

            
514
/// RSA signature for data in [`AuthCert`] and related structures
515
///
516
/// <https://spec.torproject.org/dir-spec/netdoc.html#signing>
517
///
518
/// # Caveats
519
///
520
/// This type **MUST NOT** be used for [`AuthCert::dir_key_crosscert`]
521
/// because its set of object labels is a strict superset of the object
522
/// labels used by this type.
523
#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
524
#[cfg_attr(
525
    feature = "parse2",
526
    derive_deftly(ItemValueParseable),
527
    deftly(netdoc(no_extra_args, signature(hash_accu = "Sha1WholeKeywordLine")))
528
)]
529
// derive_deftly_adhoc disables unused deftly attribute checking, so we needn't cfg_attr them all
530
#[cfg_attr(not(feature = "parse2"), derive_deftly_adhoc)]
531
#[non_exhaustive]
532
pub struct AuthCertSignature {
533
    /// The bytes of the signature (base64-decoded).
534
    #[deftly(netdoc(object(label = "SIGNATURE"), with = "crate::parse2::raw_data_object"))]
535
    pub signature: Vec<u8>,
536
}
537

            
538
#[cfg(feature = "parse2")]
539
impl ItemObjectParseable for CrossCertObject {
540
929
    fn check_label(label: &str) -> StdResult<(), parse2::EP> {
541
929
        match label {
542
929
            "SIGNATURE" | "ID SIGNATURE" => Ok(()),
543
2
            _ => Err(parse2::EP::ObjectIncorrectLabel),
544
        }
545
929
    }
546

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

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

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

            
563
#[cfg(feature = "parse2")]
564
impl AuthCertUnverified {
565
    /// Verifies the signature of a [`AuthCert`]
566
    ///
567
    /// # Algorithm
568
    ///
569
    /// 1. Check whether this comes from a valid authority in `v3idents`.
570
    /// 2. Check whether the timestamps are valid (± tolerance).
571
    /// 3. Check whether the fingerprint and long-term identity key match.
572
    /// 4. Check the cross-certificate (proof-of-ownership of signing key).
573
    /// 5. Check the outer certificate (proof-of-ownership of identity key).
574
    ///
575
    /// TODO: Replace `pre_tolerance` and `post_tolerance` with
576
    /// `tor_dircommon::config::DirTolerance` which is not possible at the
577
    /// moment due to a circular dependency of `tor-dircommon` depending
578
    /// upon `tor-netdoc`.
579
    ///
580
    /// TODO: Consider whether to try to deduplicate this signature checking
581
    /// somehow, wrt to [`UncheckedAuthCert`].
582
503
    pub fn verify_self_signed(
583
503
        self,
584
503
        v3idents: &[RsaIdentity],
585
503
        pre_tolerance: Duration,
586
503
        post_tolerance: Duration,
587
503
        now: SystemTime,
588
503
    ) -> StdResult<AuthCert, parse2::VerifyFailed> {
589
503
        let (body, sigs) = (self.body, self.sigs);
590

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

            
596
        // (2) Check whether the timestamps are valid (± tolerance).
597
501
        let validity = *body.dir_key_published..=*body.dir_key_expires;
598
501
        parse2::check_validity_time_tolerance(now, validity, pre_tolerance, post_tolerance)?;
599

            
600
        // (3) Check whether the fingerprint and long-term identity key match.
601
493
        if body.dir_identity_key.to_rsa_identity() != *body.fingerprint {
602
2
            return Err(parse2::VerifyFailed::Inconsistent);
603
491
        }
604

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

            
611
        // (5) Check the outer certificate (proof-of-ownership of identity key).
612
489
        body.dir_identity_key.verify(
613
489
            &sigs.hashes.0.ok_or(parse2::VerifyFailed::Bug)?,
614
489
            &sigs.sigs.dir_key_certification.signature,
615
2
        )?;
616

            
617
487
        Ok(body)
618
503
    }
619
}
620

            
621
#[cfg(test)]
622
mod test {
623
    // @@ begin test lint list maintained by maint/add_warning @@
624
    #![allow(clippy::bool_assert_comparison)]
625
    #![allow(clippy::clone_on_copy)]
626
    #![allow(clippy::dbg_macro)]
627
    #![allow(clippy::mixed_attributes_style)]
628
    #![allow(clippy::print_stderr)]
629
    #![allow(clippy::print_stdout)]
630
    #![allow(clippy::single_char_pattern)]
631
    #![allow(clippy::unwrap_used)]
632
    #![allow(clippy::unchecked_time_subtraction)]
633
    #![allow(clippy::useless_vec)]
634
    #![allow(clippy::needless_pass_by_value)]
635
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
636
    use super::*;
637
    use crate::{Error, Pos};
638
    const TESTDATA: &str = include_str!("../../testdata/authcert1.txt");
639

            
640
    fn bad_data(fname: &str) -> String {
641
        use std::fs;
642
        use std::path::PathBuf;
643
        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
644
        path.push("testdata");
645
        path.push("bad-certs");
646
        path.push(fname);
647

            
648
        fs::read_to_string(path).unwrap()
649
    }
650

            
651
    #[test]
652
    fn parse_one() -> Result<()> {
653
        use tor_checkable::{SelfSigned, Timebound};
654
        let cert = AuthCert::parse(TESTDATA)?
655
            .check_signature()
656
            .unwrap()
657
            .dangerously_assume_timely();
658

            
659
        // Taken from TESTDATA
660
        assert_eq!(
661
            cert.id_fingerprint().to_string(),
662
            "$ed03bb616eb2f60bec80151114bb25cef515b226"
663
        );
664
        assert_eq!(
665
            cert.key_ids().sk_fingerprint.to_string(),
666
            "$c4f720e2c59f9ddd4867fff465ca04031e35648f"
667
        );
668

            
669
        Ok(())
670
    }
671

            
672
    #[test]
673
    fn parse_bad() {
674
        fn check(fname: &str, err: &Error) {
675
            let contents = bad_data(fname);
676
            let cert = AuthCert::parse(&contents);
677
            assert!(cert.is_err());
678
            assert_eq!(&cert.err().unwrap(), err);
679
        }
680

            
681
        check(
682
            "bad-cc-tag",
683
            &EK::WrongObject.at_pos(Pos::from_line(27, 12)),
684
        );
685
        check(
686
            "bad-fingerprint",
687
            &EK::BadArgument
688
                .at_pos(Pos::from_line(2, 1))
689
                .with_msg("fingerprint does not match RSA identity"),
690
        );
691
        check(
692
            "bad-version",
693
            &EK::BadDocumentVersion.with_msg("unexpected version 4"),
694
        );
695
        check(
696
            "wrong-end",
697
            &EK::WrongEndingToken
698
                .with_msg("dir-key-crosscert")
699
                .at_pos(Pos::from_line(37, 1)),
700
        );
701
        check(
702
            "wrong-start",
703
            &EK::WrongStartingToken
704
                .with_msg("fingerprint")
705
                .at_pos(Pos::from_line(1, 1)),
706
        );
707
    }
708

            
709
    #[test]
710
    fn test_recovery_1() {
711
        let mut data = "<><><<><>\nfingerprint ABC\n".to_string();
712
        data += TESTDATA;
713

            
714
        let res: Vec<Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
715

            
716
        // We should recover from the failed case and read the next data fine.
717
        assert!(res[0].is_err());
718
        assert!(res[1].is_ok());
719
        assert_eq!(res.len(), 2);
720
    }
721

            
722
    #[test]
723
    fn test_recovery_2() {
724
        let mut data = bad_data("bad-version");
725
        data += TESTDATA;
726

            
727
        let res: Vec<Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
728

            
729
        // We should recover from the failed case and read the next data fine.
730
        assert!(res[0].is_err());
731
        assert!(res[1].is_ok());
732
        assert_eq!(res.len(), 2);
733
    }
734

            
735
    #[cfg(feature = "parse2")]
736
    mod parse2_test {
737
        use super::{AuthCert, AuthCertUnverified, AuthCertVersion, CrossCert, CrossCertObject};
738

            
739
        use std::{
740
            fs::File,
741
            io::Read,
742
            path::Path,
743
            str::FromStr,
744
            time::{Duration, SystemTime},
745
        };
746

            
747
        use crate::{
748
            parse2::{self, ErrorProblem, NetdocUnverified, ParseError, ParseInput, VerifyFailed},
749
            types::{self, Iso8601TimeSp},
750
        };
751

            
752
        use base64ct::{Base64, Encoding};
753
        use derive_deftly::Deftly;
754
        use tor_llcrypto::pk::rsa::{self, RsaIdentity};
755

            
756
        /// Reads a b64 encoded file and returns its content encoded and decoded.
757
        fn read_b64<P: AsRef<Path>>(path: P) -> (String, Vec<u8>) {
758
            let mut encoded = String::new();
759
            File::open(path)
760
                .unwrap()
761
                .read_to_string(&mut encoded)
762
                .unwrap();
763
            let mut decoded = Vec::new();
764
            base64ct::Decoder::<Base64>::new_wrapped(encoded.as_bytes(), 64)
765
                .unwrap()
766
                .decode_to_end(&mut decoded)
767
                .unwrap();
768

            
769
            (encoded, decoded)
770
        }
771

            
772
        /// Converts PEM to DER (without BEGIN and END lines).
773
        fn to_der(s: &str) -> Vec<u8> {
774
            let mut r = Vec::new();
775
            for line in s.lines() {
776
                r.extend(Base64::decode_vec(line).unwrap());
777
            }
778
            r
779
        }
780

            
781
        /// Tests whether a [`DirKeyCrossCert`] can be parsed properly.
782
        #[test]
783
        fn dir_auth_cross_cert() {
784
            #[derive(Debug, Clone, PartialEq, Eq, Deftly)]
785
            #[derive_deftly(NetdocParseable)]
786
            struct Dummy {
787
                dir_key_crosscert: CrossCert,
788
            }
789

            
790
            let (encoded, decoded) = read_b64("testdata2/authcert-longclaw-crosscert-b64");
791

            
792
            // Try with `SIGNATURE`.
793
            let cert = format!(
794
                "dir-key-crosscert\n-----BEGIN SIGNATURE-----\n{encoded}\n-----END SIGNATURE-----"
795
            );
796
            let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, "")).unwrap();
797
            assert_eq!(
798
                res,
799
                Dummy {
800
                    dir_key_crosscert: CrossCert {
801
                        signature: CrossCertObject(decoded.clone())
802
                    }
803
                }
804
            );
805

            
806
            // Try with `ID SIGNATURE`.
807
            let cert = format!(
808
                "dir-key-crosscert\n-----BEGIN ID SIGNATURE-----\n{encoded}\n-----END ID SIGNATURE-----"
809
            );
810
            let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, "")).unwrap();
811
            assert_eq!(
812
                res,
813
                Dummy {
814
                    dir_key_crosscert: CrossCert {
815
                        signature: CrossCertObject(decoded.clone())
816
                    }
817
                }
818
            );
819

            
820
            // Try with different label and fail.
821
            let cert =
822
                format!("dir-key-crosscert\n-----BEGIN WHAT-----\n{encoded}\n-----END WHAT-----");
823
            let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, ""));
824
            match res {
825
                Err(ParseError {
826
                    problem: ErrorProblem::ObjectIncorrectLabel,
827
                    doctype: "dir-key-crosscert",
828
                    file: _,
829
                    lno: 1,
830
                    column: None,
831
                }) => {}
832
                other => panic!("not expected error {other:#?}"),
833
            }
834

            
835
            // Try with extra args.
836
            let cert = format!(
837
                "dir-key-crosscert arg1\n-----BEGIN ID SIGNATURE-----\n{encoded}\n-----END ID SIGNATURE-----"
838
            );
839
            let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, ""));
840
            match res {
841
                Err(ParseError {
842
                    problem: ErrorProblem::UnexpectedArgument { column: 19 },
843
                    doctype: "dir-key-crosscert",
844
                    file: _,
845
                    lno: 1,
846
                    column: Some(19),
847
                }) => {}
848
                other => panic!("not expected error {other:#?}"),
849
            }
850
        }
851

            
852
        #[test]
853
        fn dir_auth_cert() {
854
            // This is longclaw.
855

            
856
            let mut input = String::new();
857
            File::open("testdata2/authcert-longclaw-full")
858
                .unwrap()
859
                .read_to_string(&mut input)
860
                .unwrap();
861

            
862
            let res =
863
                parse2::parse_netdoc::<AuthCertUnverified>(&ParseInput::new(&input, "")).unwrap();
864
            assert_eq!(
865
                *res.inspect_unverified().0,
866
                AuthCert {
867
                    dir_key_certificate_version: AuthCertVersion::V3,
868
                    dir_address: None,
869
                    fingerprint: types::Fingerprint(
870
                        RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()
871
                    ),
872
                    dir_key_published: Iso8601TimeSp::from_str("2025-08-17 20:34:03").unwrap(),
873
                    dir_key_expires: Iso8601TimeSp::from_str("2026-08-17 20:34:03").unwrap(),
874
                    dir_identity_key: rsa::PublicKey::from_der(&to_der(include_str!(
875
                        "../../testdata2/authcert-longclaw-id-rsa"
876
                    )))
877
                    .unwrap(),
878
                    dir_signing_key: rsa::PublicKey::from_der(&to_der(include_str!(
879
                        "../../testdata2/authcert-longclaw-sign-rsa"
880
                    )))
881
                    .unwrap(),
882
                    dir_key_crosscert: CrossCert {
883
                        signature: CrossCertObject(
884
                            read_b64("testdata2/authcert-longclaw-crosscert-b64").1
885
                        )
886
                    },
887
                    __non_exhaustive: (),
888
                }
889
            );
890
        }
891

            
892
        #[test]
893
        fn dir_auth_signature() {
894
            let res = parse2::parse_netdoc::<AuthCertUnverified>(&ParseInput::new(
895
                include_str!("../../testdata2/authcert-longclaw-full"),
896
                "",
897
            ))
898
            .unwrap();
899

            
900
            // Test a valid signature.
901
            res.clone()
902
                .verify_self_signed(
903
                    &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
904
                    Duration::ZERO,
905
                    Duration::ZERO,
906
                    SystemTime::UNIX_EPOCH
907
                        .checked_add(Duration::from_secs(1762946693)) // Wed Nov 12 12:24:53 CET 2025
908
                        .unwrap(),
909
                )
910
                .unwrap();
911

            
912
            // Test with an invalid authority.
913
            assert_eq!(
914
                res.clone()
915
                    .verify_self_signed(
916
                        &[],
917
                        Duration::ZERO,
918
                        Duration::ZERO,
919
                        SystemTime::UNIX_EPOCH
920
                            .checked_add(Duration::from_secs(1762946693)) // Wed Nov 12 12:24:53 CET 2025
921
                            .unwrap(),
922
                    )
923
                    .unwrap_err(),
924
                VerifyFailed::InsufficientTrustedSigners
925
            );
926

            
927
            // Test a key too far in the future.
928
            assert_eq!(
929
                res.clone()
930
                    .verify_self_signed(
931
                        &[
932
                            RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
933
                                .unwrap()
934
                        ],
935
                        Duration::ZERO,
936
                        Duration::ZERO,
937
                        SystemTime::UNIX_EPOCH,
938
                    )
939
                    .unwrap_err(),
940
                VerifyFailed::TooNew
941
            );
942

            
943
            // Test an almost too new.
944
            res.clone()
945
                .verify_self_signed(
946
                    &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
947
                    Duration::ZERO,
948
                    Duration::ZERO,
949
                    SystemTime::UNIX_EPOCH
950
                        .checked_add(Duration::from_secs(1755462843)) // 2025-08-17 20:34:03
951
                        .unwrap(),
952
                )
953
                .unwrap();
954

            
955
            // Now fail when we are 1s below ...
956
            assert_eq!(
957
                res.clone()
958
                    .verify_self_signed(
959
                        &[
960
                            RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
961
                                .unwrap()
962
                        ],
963
                        Duration::ZERO,
964
                        Duration::ZERO,
965
                        SystemTime::UNIX_EPOCH
966
                            .checked_add(Duration::from_secs(1755462842)) // 2025-08-17 20:34:02
967
                            .unwrap(),
968
                    )
969
                    .unwrap_err(),
970
                VerifyFailed::TooNew
971
            );
972

            
973
            // ... but succeed again with a clock skew tolerance.
974
            res.clone()
975
                .verify_self_signed(
976
                    &[RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66").unwrap()],
977
                    Duration::from_secs(1),
978
                    Duration::ZERO,
979
                    SystemTime::UNIX_EPOCH
980
                        .checked_add(Duration::from_secs(1755462842)) // 2025-08-17 20:34:02
981
                        .unwrap(),
982
                )
983
                .unwrap();
984

            
985
            // Test a key too old.
986
            assert_eq!(
987
                res.clone()
988
                    .verify_self_signed(
989
                        &[
990
                            RsaIdentity::from_hex("23D15D965BC35114467363C165C4F724B64B4F66")
991
                                .unwrap()
992
                        ],
993
                        Duration::ZERO,
994
                        Duration::ZERO,
995
                        SystemTime::UNIX_EPOCH
996
                            .checked_add(Duration::from_secs(2000000000))
997
                            .unwrap(),
998
                    )
999
                    .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::<AuthCertUnverified>(&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::<AuthCertUnverified>(&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::<AuthCertUnverified>(&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
            );
        }
    }
}