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::encode::{
11
    Bug, ItemArgument, ItemEncoder, ItemObjectEncodable, NetdocEncodable, NetdocEncoder,
12
};
13
use crate::parse::keyword::Keyword;
14
use crate::parse::parser::{Section, SectionRules};
15
use crate::parse::tokenize::{ItemResult, NetDocReader};
16
use crate::parse2::{
17
    self, ArgumentError, ArgumentStream, ItemArgumentParseable, ItemObjectParseable,
18
    NetdocParseableUnverified as _, sig_hashes::Sha1WholeKeywordLine,
19
};
20
use crate::types::misc::{Fingerprint, Iso8601TimeSp, RsaPublicParse1Helper, RsaSha1Signature};
21
use crate::util::str::Extent;
22
use crate::{NetdocErrorKind as EK, NormalItemArgument, Result};
23

            
24
use tor_basic_utils::impl_debug_hex;
25
use tor_checkable::{
26
    Timebound, signed,
27
    timed::{self, TimerangeBound},
28
};
29
use tor_error::{internal, into_internal};
30
use tor_llcrypto::pk::rsa;
31
use tor_llcrypto::{d, pk, pk::rsa::RsaIdentity};
32

            
33
use std::sync::LazyLock;
34

            
35
use std::result::Result as StdResult;
36
use std::{net, time, time::SystemTime};
37

            
38
use derive_deftly::Deftly;
39
use digest::Digest;
40

            
41
#[cfg(feature = "build_docs")]
42
mod build;
43

            
44
#[cfg(feature = "build_docs")]
45
#[allow(deprecated)]
46
pub use build::AuthCertBuilder;
47

            
48
#[cfg(feature = "incomplete")]
49
mod encoded;
50
#[cfg(feature = "incomplete")]
51
pub use encoded::EncodedAuthCert;
52

            
53
decl_keyword! {
54
    pub(crate) AuthCertKwd {
55
        "dir-key-certificate-version" => DIR_KEY_CERTIFICATE_VERSION,
56
        "dir-address" => DIR_ADDRESS,
57
        "fingerprint" => FINGERPRINT,
58
        "dir-identity-key" => DIR_IDENTITY_KEY,
59
        "dir-key-published" => DIR_KEY_PUBLISHED,
60
        "dir-key-expires" => DIR_KEY_EXPIRES,
61
        "dir-signing-key" => DIR_SIGNING_KEY,
62
        "dir-key-crosscert" => DIR_KEY_CROSSCERT,
63
        "dir-key-certification" => DIR_KEY_CERTIFICATION,
64
    }
65
}
66

            
67
/// Rules about entries that must appear in an AuthCert, and how they must
68
/// be formed.
69
54
static AUTHCERT_RULES: LazyLock<SectionRules<AuthCertKwd>> = LazyLock::new(|| {
70
    use AuthCertKwd::*;
71

            
72
54
    let mut rules = SectionRules::builder();
73
54
    rules.add(DIR_KEY_CERTIFICATE_VERSION.rule().required().args(1..));
74
54
    rules.add(DIR_ADDRESS.rule().args(1..));
75
54
    rules.add(FINGERPRINT.rule().required().args(1..));
76
54
    rules.add(DIR_IDENTITY_KEY.rule().required().no_args().obj_required());
77
54
    rules.add(DIR_SIGNING_KEY.rule().required().no_args().obj_required());
78
54
    rules.add(DIR_KEY_PUBLISHED.rule().required());
79
54
    rules.add(DIR_KEY_EXPIRES.rule().required());
80
54
    rules.add(DIR_KEY_CROSSCERT.rule().required().no_args().obj_required());
81
54
    rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
82
54
    rules.add(
83
54
        DIR_KEY_CERTIFICATION
84
54
            .rule()
85
54
            .required()
86
54
            .no_args()
87
54
            .obj_required(),
88
    );
89
54
    rules.build()
90
54
});
91

            
92
/// A single directory authority key certificate
93
///
94
/// This is the body, not including signatures.
95
///
96
/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html>
97
///
98
/// To make a fresh `AuthCert`, use [`AuthCertConstructor`].
99
#[derive(Clone, Debug, Deftly)]
100
#[derive_deftly(Constructor)]
101
#[derive_deftly(NetdocParseableUnverified, NetdocEncodable)]
102
#[cfg_attr(test, derive(PartialEq, Eq))]
103
#[allow(clippy::exhaustive_structs)]
104
pub struct AuthCert {
105
    /// Intro line
106
    ///
107
    /// Currently must be version 3.
108
    ///
109
    /// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certificate-version>
110
    #[deftly(constructor(default = AuthCertVersion::V3))]
111
    #[deftly(netdoc(single_arg))]
112
    pub dir_key_certificate_version: AuthCertVersion,
113

            
114
    /// An IPv4 address for this authority.
115
    #[deftly(netdoc(single_arg))]
116
    pub dir_address: Option<net::SocketAddrV4>,
117

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

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

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

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

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

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

            
161
    #[doc(hidden)]
162
    #[deftly(netdoc(skip))]
163
    pub __non_exhaustive: (),
164
}
165

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

            
179
impl NormalItemArgument for AuthCertVersion {}
180

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

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

            
197
    /// The actual unchecked certificate.
198
    c: signed::SignatureGated<timed::TimerangeBound<AuthCert>>,
199
}
200

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

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

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

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

            
261
    /// Return an AuthCertKeyIds object describing the keys in this
262
    /// certificate.
263
1096
    pub fn key_ids(&self) -> AuthCertKeyIds {
264
1096
        AuthCertKeyIds {
265
1096
            id_fingerprint: self.fingerprint.0,
266
1096
            sk_fingerprint: self.dir_signing_key.to_rsa_identity(),
267
1096
        }
268
1096
    }
269

            
270
    /// Return an RsaIdentity for this certificate's identity key.
271
16
    pub fn id_fingerprint(&self) -> &rsa::RsaIdentity {
272
16
        &self.fingerprint
273
16
    }
274

            
275
    /// Return the time when this certificate says it was published.
276
54
    pub fn published(&self) -> time::SystemTime {
277
54
        *self.dir_key_published
278
54
    }
279

            
280
    /// Return the time when this certificate says it should expire.
281
54
    pub fn expires(&self) -> time::SystemTime {
282
54
        *self.dir_key_expires
283
54
    }
284

            
285
    /// Parse an authority certificate from a reader.
286
190
    fn from_body(body: &Section<'_, AuthCertKwd>, s: &str) -> Result<UncheckedAuthCert> {
287
        use AuthCertKwd::*;
288

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

            
318
186
        let version = body
319
186
            .required(DIR_KEY_CERTIFICATE_VERSION)?
320
186
            .parse_arg::<u32>(0)?;
321
186
        if version != 3 {
322
4
            return Err(EK::BadDocumentVersion.with_msg(format!("unexpected version {}", version)));
323
182
        }
324
182
        let dir_key_certificate_version = AuthCertVersion::V3;
325

            
326
182
        let dir_signing_key: rsa::PublicKey = body
327
182
            .required(DIR_SIGNING_KEY)?
328
182
            .parse_obj::<RsaPublicParse1Helper>("RSA PUBLIC KEY")?
329
182
            .check_len(1024..)?
330
182
            .check_exponent(65537)?
331
182
            .into();
332

            
333
182
        let dir_identity_key: rsa::PublicKey = body
334
182
            .required(DIR_IDENTITY_KEY)?
335
182
            .parse_obj::<RsaPublicParse1Helper>("RSA PUBLIC KEY")?
336
182
            .check_len(1024..)?
337
182
            .check_exponent(65537)?
338
182
            .into();
339

            
340
182
        let dir_key_published = body
341
182
            .required(DIR_KEY_PUBLISHED)?
342
182
            .args_as_str()
343
182
            .parse::<Iso8601TimeSp>()?;
344

            
345
182
        let dir_key_expires = body
346
182
            .required(DIR_KEY_EXPIRES)?
347
182
            .args_as_str()
348
182
            .parse::<Iso8601TimeSp>()?;
349

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

            
361
180
        let dir_address = body
362
180
            .maybe(DIR_ADDRESS)
363
180
            .parse_args_as_str::<net::SocketAddrV4>()?;
364

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

            
379
178
            let signed = dir_identity_key.to_rsa_identity();
380
            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
381

            
382
178
            let v = rsa::ValidatableRsaSignature::new(&dir_signing_key, &sig, signed.as_bytes());
383

            
384
178
            dir_key_crosscert = CrossCert {
385
178
                signature: CrossCertObject(sig),
386
178
            };
387

            
388
178
            v
389
        };
390

            
391
        // check the signature
392
178
        let v_sig = {
393
178
            let signature = body.required(DIR_KEY_CERTIFICATION)?;
394
178
            let sig = signature.obj("SIGNATURE")?;
395

            
396
178
            let mut sha1 = d::Sha1::new();
397
            // Unwrap should be safe because `.parse()` would have already
398
            // returned an Error
399
            #[allow(clippy::unwrap_used)]
400
178
            let start_offset = body.first_item().unwrap().offset_in(s).unwrap();
401
            #[allow(clippy::unwrap_used)]
402
178
            let end_offset = body.last_item().unwrap().offset_in(s).unwrap();
403
178
            let end_offset = end_offset + "dir-key-certification\n".len();
404
178
            sha1.update(
405
178
                s.get(start_offset..end_offset)
406
178
                    .ok_or(internal!("chopped utf8"))?,
407
            );
408
178
            let sha1 = sha1.finalize();
409
            // TODO: we need to accept prefixes here. COMPAT BLOCKER.
410

            
411
178
            rsa::ValidatableRsaSignature::new(&dir_identity_key, &sig, &sha1)
412
        };
413

            
414
178
        let id_fingerprint = dir_identity_key.to_rsa_identity();
415

            
416
178
        let location = {
417
178
            let start_idx = start_pos.offset_within(s);
418
178
            let end_idx = end_pos.offset_within(s);
419
178
            match (start_idx, end_idx) {
420
178
                (Some(a), Some(b)) => {
421
178
                    Extent::new(s, s.get(a..b + 1).ok_or(internal!("chopped utf8"))?)
422
                }
423
                _ => None,
424
            }
425
        };
426

            
427
178
        let authcert = AuthCert {
428
178
            dir_key_certificate_version,
429
178
            dir_address,
430
178
            dir_identity_key,
431
178
            dir_signing_key,
432
178
            dir_key_published,
433
178
            dir_key_expires,
434
178
            dir_key_crosscert,
435
178
            fingerprint: Fingerprint(id_fingerprint),
436
178
            __non_exhaustive: (),
437
178
        };
438

            
439
178
        let signatures: Vec<Box<dyn pk::ValidatableSignature>> =
440
178
            vec![Box::new(v_crosscert), Box::new(v_sig)];
441

            
442
178
        let timed = timed::TimerangeBound::new(authcert, *dir_key_published..*dir_key_expires);
443
178
        let signed = signed::SignatureGated::new(timed, signatures);
444
178
        let unchecked = UncheckedAuthCert {
445
178
            location,
446
178
            c: signed,
447
178
        };
448
178
        Ok(unchecked)
449
190
    }
450
}
451

            
452
/// Parsing/encoding module for `AuthCertKeyIds` as found in `directory-signature`
453
///
454
/// Use with `#[deftly(netdoc(with = ...))]` when deriving
455
/// `ItemValueParseable` and `ItemValueEncodable`.
456
///
457
/// <https://spec.torproject.org/dir-spec/consensus-formats.html#item:directory-signature>
458
//
459
// Currently the only use site is `netstatus::Signature`.
460
// If we find this is being used in many places, and is therefore a standard thing,
461
// we should arrange for the derives to be able to derive from an argument collection,
462
// and use that.
463
pub(crate) mod keyids_directory_signature_args {
464
    use super::*;
465
    use std::result::Result;
466

            
467
    /// Parse
468
1896
    pub(crate) fn from_args<'s>(
469
1896
        args: &mut ArgumentStream<'s>,
470
1896
    ) -> Result<AuthCertKeyIds, ArgumentError> {
471
3840
        let mut fp = || Ok::<_, ArgumentError>(Fingerprint::from_args(args)?.0);
472
        Ok(AuthCertKeyIds {
473
1896
            id_fingerprint: fp()?,
474
1896
            sk_fingerprint: fp()?,
475
        })
476
1896
    }
477

            
478
    /// Encode
479
20
    pub(crate) fn write_arg_onto(
480
20
        self_: &AuthCertKeyIds,
481
20
        out: &mut ItemEncoder<'_>,
482
20
    ) -> Result<(), Bug> {
483
50
        let mut fp = |id| Fingerprint(id).write_arg_onto(out);
484
20
        fp(self_.id_fingerprint)?;
485
20
        fp(self_.sk_fingerprint)?;
486
20
        Ok(())
487
20
    }
488
}
489

            
490
/// Pseudo-Signature of the long-term identity key by the medium-term key.
491
///
492
/// This type does not implement `SignatureItemParseable` because that trait
493
/// is reserved for signatures on *netdocs*, such as [`AuthCertSignature`].
494
/// As `CrossCert` does not sign a full document, it implements only
495
/// `ItemValueParseable`, instead.
496
///
497
/// Verification of this signature is done in `AuthCertUnverified::verify`,
498
/// and during parsing by the old parser.
499
/// So a `CrossCert` in [`AuthCert::dir_key_crosscert`] in a bare `AuthCert` has been validated.
500
//
501
// TODO SPEC (Diziet): it is far from clear to me that this cert serves any useful purpose.
502
// However, we are far too busy now with rewriting the universe to consider transitioning it away.
503
#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
504
#[derive_deftly(ItemValueParseable, ItemValueEncodable)]
505
#[deftly(netdoc(no_extra_args))]
506
#[non_exhaustive]
507
pub struct CrossCert {
508
    /// The bytes of the signature (base64-decoded).
509
    #[deftly(netdoc(object))]
510
    pub signature: CrossCertObject,
511
}
512

            
513
/// Wrapper around [`Vec<u8>`] implementing [`ItemObjectParseable`] properly.
514
///
515
/// Unfortunately, this wrapper is necessary, because the specification
516
/// demands that these certificate objects must accept two labels:
517
/// `SIGNATURE` and `ID SIGNATURE`.  Because the deftly template for
518
/// `ItemValueParseable` only allows for a single label
519
/// (`#[deftly(netdoc(object(label = "LABEL")))]`), we must implement this
520
/// trait ourselves in order to allow multiple ones.
521
///
522
/// TODO: In the future, it might be nice to let the respective fmeta
523
/// accept a pattern, as pattern matching would allow trivially for one
524
/// to infinity different combinations.
525
/// TODO SPEC: Alternatively we could abolish the wrong labels,
526
/// or we could abolish Objects completely and just have long lines.
527
///
528
/// # Specifications
529
///
530
/// <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-crosscert>
531
#[derive(Clone, PartialEq, Eq, derive_more::Deref)]
532
#[non_exhaustive]
533
pub struct CrossCertObject(pub Vec<u8>);
534
impl_debug_hex! { CrossCertObject . 0 }
535

            
536
impl CrossCert {
537
    /// Make a `CrossCert`
538
2
    pub fn new(
539
2
        k_auth_sign_rsa: &rsa::KeyPair,
540
2
        h_kp_auth_id_rsa: &RsaIdentity,
541
2
    ) -> StdResult<Self, Bug> {
542
2
        let signature = k_auth_sign_rsa
543
2
            .sign(h_kp_auth_id_rsa.as_bytes())
544
2
            .map_err(into_internal!("failed to sign cross-cert"))?;
545
2
        Ok(CrossCert {
546
2
            signature: CrossCertObject(signature),
547
2
        })
548
2
    }
549
}
550

            
551
/// Signatures for [`AuthCert`]
552
///
553
/// Signed by [`AuthCert::dir_identity_key`] in order to prove ownership.
554
/// Can be seen as the opposite of [`AuthCert::dir_key_crosscert`].
555
///
556
/// # Specifications
557
///
558
/// * <https://spec.torproject.org/dir-spec/creating-key-certificates.html#item:dir-key-certification>
559
/// * <https://spec.torproject.org/dir-spec/netdoc.html#signing>
560
#[derive(Debug, Clone, PartialEq, Eq, Deftly)]
561
#[derive_deftly(NetdocParseableSignatures, NetdocEncodable)]
562
#[deftly(netdoc(signatures(hashes_accu = Sha1WholeKeywordLine)))]
563
#[non_exhaustive]
564
pub struct AuthCertSignatures {
565
    /// Contains the actual signature, see [`AuthCertSignatures`].
566
    pub dir_key_certification: RsaSha1Signature,
567
}
568

            
569
/// RSA signature for data in [`AuthCert`]
570
///
571
/// <https://spec.torproject.org/dir-spec/netdoc.html#signing>
572
///
573
/// Compatibility type alias for [`RsaSha1Signature`].
574
#[deprecated = "use RsaSha1Signature"]
575
pub type AuthCertSignature = RsaSha1Signature;
576

            
577
impl ItemObjectParseable for CrossCertObject {
578
948
    fn check_label(label: &str) -> StdResult<(), parse2::EP> {
579
948
        match label {
580
948
            "SIGNATURE" | "ID SIGNATURE" => Ok(()),
581
2
            _ => Err(parse2::EP::ObjectIncorrectLabel),
582
        }
583
948
    }
584

            
585
946
    fn from_bytes(input: &[u8]) -> StdResult<Self, parse2::EP> {
586
946
        Ok(Self(input.to_vec()))
587
946
    }
588
}
589

            
590
impl ItemObjectEncodable for CrossCertObject {
591
2
    fn label(&self) -> &str {
592
2
        "ID SIGNATURE"
593
2
    }
594

            
595
2
    fn write_object_onto(&self, b: &mut Vec<u8>) -> StdResult<(), Bug> {
596
2
        b.extend(&self.0);
597
2
        Ok(())
598
2
    }
599
}
600

            
601
impl tor_checkable::SelfSigned<timed::TimerangeBound<AuthCert>> for UncheckedAuthCert {
602
    type Error = signature::Error;
603

            
604
172
    fn dangerously_assume_wellsigned(self) -> timed::TimerangeBound<AuthCert> {
605
172
        self.c.dangerously_assume_wellsigned()
606
172
    }
607
172
    fn is_well_signed(&self) -> std::result::Result<(), Self::Error> {
608
172
        self.c.is_well_signed()
609
172
    }
610
}
611

            
612
impl AuthCertUnverified {
613
    /// Verifies the signature of a [`AuthCert`]
614
    ///
615
    /// # Algorithm
616
    ///
617
    /// 1. Check whether this comes from a valid authority in `v3idents`.
618
    /// 2. Check whether the timestamps are valid (± tolerance).
619
    /// 3. Check whether the fingerprint and long-term identity key match.
620
    /// 4. Check the cross-certificate (proof-of-ownership of signing key).
621
    /// 5. Check the outer certificate (proof-of-ownership of identity key).
622
    ///
623
    /// TODO: Replace `pre_tolerance` and `post_tolerance` with
624
    /// `tor_dircommon::config::DirTolerance` which is not possible at the
625
    /// moment due to a circular dependency of `tor-dircommon` depending
626
    /// upon `tor-netdoc`.
627
    ///
628
    /// TODO: Consider whether to try to deduplicate this signature checking
629
    /// somehow, wrt to [`UncheckedAuthCert`].
630
536
    pub fn verify(
631
536
        self,
632
536
        v3idents: &[RsaIdentity],
633
536
    ) -> StdResult<TimerangeBound<AuthCert>, parse2::VerifyFailed> {
634
536
        let (body, sigs) = (self.body, self.sigs);
635

            
636
        // (1) Check whether this comes from a valid authority in `v3idents`.
637
536
        if !v3idents.contains(&body.fingerprint.0) {
638
4
            return Err(parse2::VerifyFailed::InsufficientTrustedSigners);
639
532
        }
640

            
641
        // (2) Check whether the timestamps are valid (± tolerance).
642
532
        let validity = *body.dir_key_published..=*body.dir_key_expires;
643

            
644
        // (3) Check whether the fingerprint and long-term identity key match.
645
532
        if body.dir_identity_key.to_rsa_identity() != *body.fingerprint {
646
2
            return Err(parse2::VerifyFailed::Inconsistent);
647
530
        }
648

            
649
        // (4) Check the cross-certificate (proof-of-ownership of signing key).
650
530
        body.dir_signing_key.verify(
651
530
            body.fingerprint.0.as_bytes(),
652
530
            &body.dir_key_crosscert.signature,
653
2
        )?;
654

            
655
        // (5) Check the outer certificate (proof-of-ownership of identity key).
656
528
        body.dir_identity_key.verify(
657
528
            &sigs.hashes.0.ok_or(parse2::VerifyFailed::Bug)?,
658
528
            &sigs.sigs.dir_key_certification.signature,
659
2
        )?;
660

            
661
526
        Ok(TimerangeBound::new(body, validity))
662
536
    }
663

            
664
    /// Verify the signatures (and check validity times)
665
    ///
666
    /// The pre and post tolerance (time check allowances) used are both zero.
667
    ///
668
    /// # Security considerations
669
    ///
670
    /// The caller must check that the KP_auth_id is correct/relevant.
671
26
    pub fn verify_selfcert(self, now: SystemTime) -> StdResult<AuthCert, parse2::VerifyFailed> {
672
26
        let h_kp_auth_id_rsa = self.inspect_unverified().0.fingerprint.0;
673
26
        Ok(self.verify(&[h_kp_auth_id_rsa])?.check_valid_at(&now)?)
674
26
    }
675
}
676

            
677
impl AuthCert {
678
    /// Make the base for a new `AuthCert`
679
    ///
680
    /// This contains only the mandatory fields (the ones in `AuthCertConstructor`).
681
    /// This method is an alternative to providing a `AuthCertConstructor` value display,
682
    /// and is convenient because an authcert contains much recapitulated information.
683
    ///
684
    /// # Example
685
    ///
686
    /// ```no_run
687
    /// # fn main() -> Result<(), anyhow::Error> {
688
    /// use tor_netdoc::doc::authcert::AuthCert;
689
    /// let (k_auth_id_rsa, k_auth_sign_rsa, published, expires) = todo!();
690
    /// let authcert = AuthCert {
691
    ///     dir_address: Some("192.0.2.17:7000".parse()?),
692
    ///     ..AuthCert::new_base(&k_auth_id_rsa, &k_auth_sign_rsa, published, expires)?
693
    /// };
694
    /// # Ok(())
695
    /// # }
696
    /// ```
697
2
    pub fn new_base(
698
2
        k_auth_id_rsa: &rsa::KeyPair,
699
2
        k_auth_sign_rsa: &rsa::KeyPair,
700
2
        published: SystemTime,
701
2
        expires: SystemTime,
702
2
    ) -> StdResult<Self, Bug> {
703
2
        let fingerprint = k_auth_id_rsa.to_public_key().to_rsa_identity();
704
2
        let dir_key_crosscert = CrossCert::new(k_auth_sign_rsa, &fingerprint)?;
705

            
706
2
        let base = AuthCertConstructor {
707
2
            fingerprint: fingerprint.into(),
708
2
            dir_key_published: published.into(),
709
2
            dir_key_expires: expires.into(),
710
2
            dir_identity_key: k_auth_id_rsa.to_public_key(),
711
2
            dir_signing_key: k_auth_sign_rsa.to_public_key(),
712
2
            dir_key_crosscert,
713
2
        }
714
2
        .construct();
715

            
716
2
        Ok(base)
717
2
    }
718

            
719
    /// Encode this `AuthCert` and sign it with `k_auth_id_rsa`
720
    ///
721
    /// Yields the string representation of the signed, encoded, document,
722
    /// as an [`EncodedAuthCert`].
723
    // TODO these features are quite tangled
724
    // `EncodedAuthCert` is only available with `parse2` and `plain-consensus`
725
    #[cfg(feature = "incomplete")] // Needs EncodedAuthCert
726
2
    pub fn encode_sign(&self, k_auth_id_rsa: &rsa::KeyPair) -> StdResult<EncodedAuthCert, Bug> {
727
2
        let mut encoder = NetdocEncoder::new();
728
2
        self.encode_unsigned(&mut encoder)?;
729

            
730
2
        let signature =
731
2
            RsaSha1Signature::new_sign_netdoc(k_auth_id_rsa, &encoder, "dir-key-certification")?;
732
2
        let sigs = AuthCertSignatures {
733
2
            dir_key_certification: signature,
734
2
        };
735
2
        sigs.encode_unsigned(&mut encoder)?;
736

            
737
2
        let encoded = encoder.finish()?;
738
        // This rechecks the invariants which ought to be true by construction.
739
        // That is convenient for the code here, and the perf implications are irrelevant.
740
2
        let encoded = encoded
741
2
            .try_into()
742
2
            .map_err(into_internal!("generated broken authcert"))?;
743
2
        Ok(encoded)
744
2
    }
745
}
746

            
747
#[cfg(test)]
748
mod test {
749
    // @@ begin test lint list maintained by maint/add_warning @@
750
    #![allow(clippy::bool_assert_comparison)]
751
    #![allow(clippy::clone_on_copy)]
752
    #![allow(clippy::dbg_macro)]
753
    #![allow(clippy::mixed_attributes_style)]
754
    #![allow(clippy::print_stderr)]
755
    #![allow(clippy::print_stdout)]
756
    #![allow(clippy::single_char_pattern)]
757
    #![allow(clippy::unwrap_used)]
758
    #![allow(clippy::unchecked_time_subtraction)]
759
    #![allow(clippy::useless_vec)]
760
    #![allow(clippy::needless_pass_by_value)]
761
    #![allow(clippy::string_slice)] // See arti#2571
762
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
763
    use super::*;
764
    use crate::{
765
        Pos,
766
        parse2::{ErrorProblem, ParseError, ParseInput, VerifyFailed, parse_netdoc},
767
        types,
768
    };
769
    use humantime::parse_rfc3339;
770
    use std::result::Result;
771
    use std::{
772
        fs,
773
        net::{Ipv4Addr, SocketAddrV4},
774
        str::FromStr,
775
        time::Duration,
776
    };
777
    use tor_basic_utils::test_rng;
778

            
779
    const TESTDATA: &str = include_str!("../../testdata/authcert1.txt");
780

            
781
    fn bad_data(fname: &str) -> String {
782
        use std::fs;
783
        use std::path::PathBuf;
784
        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
785
        path.push("testdata");
786
        path.push("bad-certs");
787
        path.push(fname);
788

            
789
        fs::read_to_string(path).unwrap()
790
    }
791

            
792
    #[test]
793
    fn parse_one() -> crate::Result<()> {
794
        use tor_checkable::{SelfSigned, Timebound};
795
        let cert = AuthCert::parse(TESTDATA)?
796
            .check_signature()
797
            .unwrap()
798
            .dangerously_assume_timely();
799

            
800
        // Taken from TESTDATA
801
        assert_eq!(
802
            cert.id_fingerprint().to_string(),
803
            "$ed03bb616eb2f60bec80151114bb25cef515b226"
804
        );
805
        assert_eq!(
806
            cert.key_ids().sk_fingerprint.to_string(),
807
            "$c4f720e2c59f9ddd4867fff465ca04031e35648f"
808
        );
809

            
810
        Ok(())
811
    }
812

            
813
    #[test]
814
    fn parse_bad() {
815
        fn check(fname: &str, err: &crate::Error) {
816
            let contents = bad_data(fname);
817
            let cert = AuthCert::parse(&contents);
818
            assert!(cert.is_err());
819
            assert_eq!(&cert.err().unwrap(), err);
820
        }
821

            
822
        check(
823
            "bad-cc-tag",
824
            &EK::WrongObject.at_pos(Pos::from_line(27, 12)),
825
        );
826
        check(
827
            "bad-fingerprint",
828
            &EK::BadArgument
829
                .at_pos(Pos::from_line(2, 1))
830
                .with_msg("fingerprint does not match RSA identity"),
831
        );
832
        check(
833
            "bad-version",
834
            &EK::BadDocumentVersion.with_msg("unexpected version 4"),
835
        );
836
        check(
837
            "wrong-end",
838
            &EK::WrongEndingToken
839
                .with_msg("dir-key-crosscert")
840
                .at_pos(Pos::from_line(37, 1)),
841
        );
842
        check(
843
            "wrong-start",
844
            &EK::WrongStartingToken
845
                .with_msg("fingerprint")
846
                .at_pos(Pos::from_line(1, 1)),
847
        );
848
    }
849

            
850
    #[test]
851
    fn test_recovery_1() {
852
        let mut data = "<><><<><>\nfingerprint ABC\n".to_string();
853
        data += TESTDATA;
854

            
855
        let res: Vec<crate::Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
856

            
857
        // We should recover from the failed case and read the next data fine.
858
        assert!(res[0].is_err());
859
        assert!(res[1].is_ok());
860
        assert_eq!(res.len(), 2);
861
    }
862

            
863
    #[test]
864
    fn test_recovery_2() {
865
        let mut data = bad_data("bad-version");
866
        data += TESTDATA;
867

            
868
        let res: Vec<crate::Result<_>> = AuthCert::parse_multiple(&data).unwrap().collect();
869

            
870
        // We should recover from the failed case and read the next data fine.
871
        assert!(res[0].is_err());
872
        assert!(res[1].is_ok());
873
        assert_eq!(res.len(), 2);
874
    }
875

            
876
    // === AUTHCERT D190BF3B00E311A9AEB6D62B51980E9B2109BAD1 ===
877
    // These values come from testdata2/keys/authority_certificate.
878
    const DIR_KEY_PUBLISHED: &str = "2000-01-01 00:00:05";
879
    const DIR_KEY_EXPIRES: &str = "2001-01-01 00:00:05";
880
    const FINGERPRINT: &str = "D190BF3B00E311A9AEB6D62B51980E9B2109BAD1";
881
    const DIR_ADDRESS: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 7100);
882
    const DIR_IDENTITY_KEY: &str = "
883
-----BEGIN RSA PUBLIC KEY-----
884
MIIBigKCAYEAt0rXD+1gYwKFAxrO4uNHQ9dQVUOGx5FxkioYNSct5Z3JU00dTKNJ
885
jt4OGkFYwixWwk6KLDOiB+I/q9YIdA1NlQ5R3Hz8jjvFPVl0JQQm2LYzdSzv7/CZ
886
U1qq5rYeeoYKx8qMQg4q3WgR251GEnOG+rVqzFSs0oyC+SDfYn9iMt00/pmN3HXf
887
wmasY6BescVrYoDbnpkwKATizd4lzx5K8V8aXUXtd8qnYzSyHLlhiO1eufVX07YC
888
+AVHV7W7qCTY/4I5Sm0dQ9jF/r04JBHnpH+aae48JOjWDCZj9AINi3rCKS8XClGb
889
BB/LJidoQAZraQEEtu3Ql1mjdLreeyWfXpfZFvwKuYn44FtQsOT2TVAVNqNF8N4v
890
yfwfiPN6FQWlPyMCEB81HerCn03Zi5WgQLGo7PAeO4LFrLrU16DUC5/oJENeHs0T
891
27FZQyrlf0rAxiHh7TJKcjLmzeyxCQVQlr2AXXs28gKHV0AQnEcdrVOpTrquSCQQ
892
hWBehR+ct4OJAgMBAAE=
893
-----END RSA PUBLIC KEY-----
894
    ";
895
    const DIR_SIGNING_KEY: &str = "
896
-----BEGIN RSA PUBLIC KEY-----
897
MIIBCgKCAQEAtPF94+bThLI28kn6e+MmUECMMJ5UBlnQ+Mvwn8Zd85awPQTDz5Wu
898
13sZDN3nWnhgSuP5q/WDYc5GPPtQdSWBiG1nJA2XLgEHTHf29iGZ+jAoGfIMJvBV
899
1xN8baTnsha5LGx5BQ4UqzlUmoaPzwbjehnPd00FgVkpcCvKZu1HU7fGMVwn4MMh
900
zuxJTqTgfcuFWTEu0H0ukOFX+51ih6WO3GWYqRiqgU0Q5/Ets8ccCTq7ND9d2u1P
901
d7kQzUHbVP0KmYGK4qYntGDfP4g9SmpBoUUHyP3j9en9S6PMYv8m1YFO7M7JKu6Q
902
dQZfGTxj9C/0b/jRklgn5JlKAl9eJQvCdwIDAQAB
903
-----END RSA PUBLIC KEY-----
904
";
905
    const DIR_CROSS_CERT_OBJECT: &str = "
906
-----BEGIN ID SIGNATURE-----
907
NBaPdBNCNMah6cklrALzj0RdHymF/jPGOv9NmeqaXc0uTN06S/BlVM/xTjilu+dj
908
sjPuT0BQL4/ZWyZR+R+gJJojKYILSId4IQ1elzRSxpFN+u2u/ZEmS6SR2SwpA05A
909
btOYBKAmYkY6rLsTCbXGx3lAH2kAXfcrltCNKZXV6gqW7X379fiOnSId1OWhKPe1
910
/1p3pQGZxgb8FOT1kpHxOMRBClF9Ulm3d9fQZr80Wn73gZ2Bp1RXn9c7c/71HD1c
911
mzMT023bleZ574az+117yNAr6XbIgqQfzbySzVLPXM8ZN9BrGR40KDZ2638ZJjRu
912
8HK5TzuknWlkRv3hCyRX+g==
913
-----END ID SIGNATURE-----
914
";
915
    const AUTHCERT_RAW: &str = include_str!("../../testdata2/keys/authority_certificate");
916
    /// A system time in the range of [`DIR_KEY_PUBLISHED`] and [`DIR_KEY_EXPIRES`].
917
    ///
918
    /// Constructed by ourselves to have a time point we can use for testing
919
    /// timestamp verification.
920
    const VALID_SYSTEM_TIME: &str = "2000-06-01 00:00:00";
921

            
922
    // === AUTHCERT 0B8997614EC647C1C6B6A044E2B5408F0B823FB0 ===
923
    // This values come from ../../testdata2/cached-certs--1
924
    // A different authority certificate different from the one above.
925
    const ALTERNATIVE_AUTHCERT_RAW: &str = include_str!("../../testdata2/cached-certs--1");
926

            
927
    /// Converts a string in the [`Iso8601TimeSp`] format to [`SystemTime`].
928
    ///
929
    /// This functions panics in the case the input is malformatted.
930
    fn to_system_time(s: &str) -> SystemTime {
931
        Iso8601TimeSp::from_str(s).unwrap().0
932
    }
933

            
934
    /// Converts a PEM encoded RSA Public key to an [`rsa::PublicKey`].
935
    ///
936
    /// This function panics in the case the input is malformatted.
937
    fn pem_to_rsa_pk(s: &str) -> rsa::PublicKey {
938
        rsa::PublicKey::from_der(pem::parse(s).unwrap().contents()).unwrap()
939
    }
940

            
941
    /// Converts a hex-encoded RSA identity to an [`RsaIdentity`].
942
    ///
943
    /// This function panics in the case the input is malformatted.
944
    fn to_rsa_id(s: &str) -> RsaIdentity {
945
        RsaIdentity::from_hex(s).unwrap()
946
    }
947

            
948
    /// Tests whether a [`DirKeyCrossCert`] can be parsed properly.
949
    #[test]
950
    fn dir_auth_cross_cert() {
951
        #[derive(Debug, Clone, PartialEq, Eq, Deftly)]
952
        #[derive_deftly(NetdocParseable)]
953
        struct Dummy {
954
            dir_key_crosscert: CrossCert,
955
        }
956

            
957
        // "Encodes" a DIR_CROSS_CERT_OBJECT by simply removing the lines
958
        // indicating the BEGIN and END, as the purpose is to test multiple
959
        // labels.
960
        let encoded = DIR_CROSS_CERT_OBJECT
961
            .lines()
962
            .filter(|line| !line.starts_with("-----"))
963
            .collect::<Vec<_>>()
964
            .join("\n");
965
        let decoded = pem::parse(DIR_CROSS_CERT_OBJECT)
966
            .unwrap()
967
            .contents()
968
            .to_vec();
969

            
970
        // Try with `SIGNATURE`.
971
        let cert = format!(
972
            "dir-key-crosscert\n-----BEGIN SIGNATURE-----\n{encoded}\n-----END SIGNATURE-----"
973
        );
974
        let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, "")).unwrap();
975
        assert_eq!(
976
            res,
977
            Dummy {
978
                dir_key_crosscert: CrossCert {
979
                    signature: CrossCertObject(decoded.clone())
980
                }
981
            }
982
        );
983

            
984
        // Try with `ID SIGNATURE`.
985
        let cert = format!(
986
            "dir-key-crosscert\n-----BEGIN ID SIGNATURE-----\n{encoded}\n-----END ID SIGNATURE-----"
987
        );
988
        let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, "")).unwrap();
989
        assert_eq!(
990
            res,
991
            Dummy {
992
                dir_key_crosscert: CrossCert {
993
                    signature: CrossCertObject(decoded.clone())
994
                }
995
            }
996
        );
997

            
998
        // Try with different label and fail.
999
        let cert =
            format!("dir-key-crosscert\n-----BEGIN WHAT-----\n{encoded}\n-----END WHAT-----");
        let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, ""));
        match res {
            Err(ParseError {
                problem: ErrorProblem::ObjectIncorrectLabel,
                doctype: "dir-key-crosscert",
                file: _,
                lno: 1,
                column: None,
            }) => {}
            other => panic!("not expected error {other:#?}"),
        }
        // Try with extra args.
        let cert = format!(
            "dir-key-crosscert arg1\n-----BEGIN ID SIGNATURE-----\n{encoded}\n-----END ID SIGNATURE-----"
        );
        let res = parse2::parse_netdoc::<Dummy>(&ParseInput::new(&cert, ""));
        match res {
            Err(ParseError {
                problem: ErrorProblem::UnexpectedArgument { column: 19 },
                doctype: "dir-key-crosscert",
                file: _,
                lno: 1,
                column: Some(19),
            }) => {}
            other => panic!("not expected error {other:#?}"),
        }
    }
    #[test]
    fn dir_auth_cert() {
        let res =
            parse2::parse_netdoc::<AuthCertUnverified>(&ParseInput::new(AUTHCERT_RAW, "")).unwrap();
        assert_eq!(
            *res.inspect_unverified().0,
            AuthCert {
                dir_key_certificate_version: AuthCertVersion::V3,
                dir_address: Some(DIR_ADDRESS),
                fingerprint: types::Fingerprint(to_rsa_id(FINGERPRINT)),
                dir_key_published: Iso8601TimeSp(to_system_time(DIR_KEY_PUBLISHED)),
                dir_key_expires: Iso8601TimeSp(to_system_time(DIR_KEY_EXPIRES)),
                dir_identity_key: pem_to_rsa_pk(DIR_IDENTITY_KEY),
                dir_signing_key: pem_to_rsa_pk(DIR_SIGNING_KEY),
                dir_key_crosscert: CrossCert {
                    signature: CrossCertObject(
                        pem::parse(DIR_CROSS_CERT_OBJECT)
                            .unwrap()
                            .contents()
                            .to_vec()
                    )
                },
                __non_exhaustive: (),
            }
        );
    }
    #[test]
    fn dir_auth_signature() {
        let res =
            parse2::parse_netdoc::<AuthCertUnverified>(&ParseInput::new(AUTHCERT_RAW, "")).unwrap();
        // Test a valid signature.
        let _: AuthCert = res
            .clone()
            .verify(&[to_rsa_id(FINGERPRINT)])
            .unwrap()
            .check_valid_at(&to_system_time(VALID_SYSTEM_TIME))
            .unwrap();
        // Test with an invalid authority.
        assert_eq!(
            res.clone().verify(&[],).unwrap_err(),
            VerifyFailed::InsufficientTrustedSigners
        );
        // Test a key too far in the future.
        assert_eq!(
            res.clone()
                .verify(&[to_rsa_id(FINGERPRINT)],)
                .unwrap()
                .check_valid_at(&SystemTime::UNIX_EPOCH,)
                .map_err(VerifyFailed::from)
                .unwrap_err(),
            VerifyFailed::TooNew
        );
        // Test an almost too new.
        let _: AuthCert = res
            .clone()
            .verify(&[to_rsa_id(FINGERPRINT)])
            .unwrap()
            .check_valid_at(&to_system_time(DIR_KEY_PUBLISHED))
            .unwrap();
        // Now fail when we are 1s below ...
        assert_eq!(
            res.clone()
                .verify(&[to_rsa_id(FINGERPRINT)],)
                .unwrap()
                .check_valid_at(&(to_system_time(DIR_KEY_PUBLISHED) - Duration::from_secs(1)),)
                .map_err(VerifyFailed::from)
                .unwrap_err(),
            VerifyFailed::TooNew
        );
        // ... but succeed again with a clock skew tolerance.
        let _: AuthCert = res
            .clone()
            .verify(&[to_rsa_id(FINGERPRINT)])
            .unwrap()
            .extend_pre_tolerance(Duration::from_secs(1))
            .check_valid_at(&(to_system_time(DIR_KEY_PUBLISHED) - Duration::from_secs(1)))
            .unwrap();
        // Test a key too old.
        assert_eq!(
            res.clone()
                .verify(&[to_rsa_id(FINGERPRINT)],)
                .unwrap()
                .check_valid_at(
                    &SystemTime::UNIX_EPOCH
                        .checked_add(Duration::from_secs(2000000000))
                        .unwrap(),
                )
                .map_err(VerifyFailed::from)
                .unwrap_err(),
            VerifyFailed::TooOld
        );
        // Test an almost too old.
        let _: AuthCert = res
            .clone()
            .verify(&[to_rsa_id(FINGERPRINT)])
            .unwrap()
            .check_valid_at(&to_system_time(DIR_KEY_EXPIRES))
            .unwrap();
        // Now fail when we are 1s above ...
        assert_eq!(
            res.clone()
                .verify(&[to_rsa_id(FINGERPRINT)],)
                .unwrap()
                .check_valid_at(&(to_system_time(DIR_KEY_EXPIRES) + Duration::from_secs(1)),)
                .map_err(VerifyFailed::from)
                .unwrap_err(),
            VerifyFailed::TooOld
        );
        // ... but succeed again with a clock skew tolerance.
        let _: AuthCert = res
            .clone()
            .verify(&[to_rsa_id(FINGERPRINT)])
            .unwrap()
            .extend_tolerance(Duration::from_secs(1))
            .check_valid_at(&(to_system_time(DIR_KEY_EXPIRES) + Duration::from_secs(1)))
            .unwrap();
        // Check with non-matching fingerprint and long-term identity key.
        let mut cert =
            parse2::parse_netdoc::<AuthCertUnverified>(&ParseInput::new(AUTHCERT_RAW, "")).unwrap();
        let alternative_cert = parse2::parse_netdoc::<AuthCertUnverified>(&ParseInput::new(
            ALTERNATIVE_AUTHCERT_RAW,
            "",
        ))
        .unwrap();
        cert.body.dir_identity_key = alternative_cert.body.dir_identity_key.clone();
        assert_eq!(
            cert.verify(&[to_rsa_id(FINGERPRINT)],).unwrap_err(),
            VerifyFailed::Inconsistent
        );
        // Check invalid cross-cert.
        let mut cert =
            parse2::parse_netdoc::<AuthCertUnverified>(&ParseInput::new(AUTHCERT_RAW, "")).unwrap();
        cert.body.dir_key_crosscert = alternative_cert.body.dir_key_crosscert.clone();
        assert_eq!(
            cert.verify(&[to_rsa_id(FINGERPRINT)],).unwrap_err(),
            VerifyFailed::VerifyFailed
        );
        // Check outer signature.
        let mut cert =
            parse2::parse_netdoc::<AuthCertUnverified>(&ParseInput::new(AUTHCERT_RAW, "")).unwrap();
        cert.sigs = alternative_cert.sigs.clone();
        assert_eq!(
            cert.verify(&[to_rsa_id(FINGERPRINT)],).unwrap_err(),
            VerifyFailed::VerifyFailed
        );
    }
    #[test]
    fn keyids_for_directory_signature() -> anyhow::Result<()> {
        #[derive(Deftly)]
        #[derive_deftly(NetdocEncodable, NetdocParseable)]
        struct Doc {
            intro: (),
            ids: Item,
        }
        #[derive(Deftly)]
        #[derive_deftly(ItemValueEncodable, ItemValueParseable)]
        struct Item {
            #[deftly(netdoc(with = keyids_directory_signature_args))]
            ids: AuthCertKeyIds,
        }
        let text = r#"intro
ids 1234567812345678123456781234567812345678 ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD
"#;
        let doc = parse2::parse_netdoc::<Doc>(&ParseInput::new(text, "<text>"))?;
        let mut re_encode = NetdocEncoder::new();
        doc.encode_unsigned(&mut re_encode)?;
        let re_encode = re_encode.finish()?;
        assert_eq_or_diff!(text, re_encode);
        Ok(())
    }
    #[test]
    #[cfg(feature = "incomplete")]
    fn roundtrip() -> Result<(), anyhow::Error> {
        let mut rng = test_rng::testing_rng();
        let k_auth_id_rsa = rsa::KeyPair::generate(&mut rng)?;
        let k_auth_sign_rsa = rsa::KeyPair::generate(&mut rng)?;
        let secs = |s| Duration::from_secs(s);
        let now = parse_rfc3339("1993-01-01T00:00:00Z")?;
        let published = now - secs(1000);
        let expires = published + secs(86400);
        let tolerance = secs(10);
        let input_value = AuthCert {
            dir_address: Some("192.0.2.17:7000".parse()?),
            ..AuthCert::new_base(&k_auth_id_rsa, &k_auth_sign_rsa, published, expires)?
        };
        dbg!(&input_value);
        let encoded = input_value.encode_sign(&k_auth_id_rsa)?;
        let reparsed_uv: AuthCertUnverified =
            parse_netdoc(&ParseInput::new(encoded.as_ref(), "<encoded>"))?;
        let reparsed_value = reparsed_uv
            .verify(&[k_auth_id_rsa.to_public_key().to_rsa_identity()])?
            .extend_pre_tolerance(tolerance)
            .extend_tolerance(tolerance)
            .check_valid_at(&now)?;
        dbg!(&reparsed_value);
        assert_eq!(input_value, reparsed_value);
        Ok(())
    }
    #[cfg(feature = "incomplete")]
    #[test]
    fn parse_authcert() -> anyhow::Result<()> {
        let file = "testdata2/cached-certs--1";
        let now = parse_rfc3339("2000-06-01T00:00:05Z")?;
        let text = fs::read_to_string(file)?;
        let input = ParseInput::new(&text, file);
        let doc: AuthCertUnverified = parse_netdoc(&input)?;
        let doc = doc.verify_selfcert(now)?;
        println!("{doc:?}");
        assert_eq!(
            doc.fingerprint.0.to_string(),
            "$0b8997614ec647c1c6b6a044e2b5408f0b823fb0",
        );
        Ok(())
    }
}