1
//! Directory Mirror Operation.
2
//!
3
//! # Specifications
4
//!
5
//! * [Directory cache operation](https://spec.torproject.org/dir-spec/directory-cache-operation.html).
6
//!
7
//! # Rationale
8
//!
9
//! This module implements the "core operation" of a directory mirror.
10
//! "Core operation" primarily refers to the logic involved in downloading
11
//! network documents from an upstream authority and inserting them into the
12
//! database.  This module notably **DOES NOT** provide any public (in the HTTP
13
//! sense) endpoints for querying documents.  This is purposely behind a different
14
//! module, so that the directory authority implementation can also make use of it.
15
//! You can think of this module as the one implementing the things unique
16
//! to directory mirrors.
17

            
18
use std::{
19
    collections::{HashSet, VecDeque},
20
    net::SocketAddr,
21
};
22

            
23
use r2d2::Pool;
24
use r2d2_sqlite::SqliteConnectionManager;
25
use rand::Rng;
26
use rusqlite::Transaction;
27
use strum::IntoEnumIterator;
28
use tokio::net::TcpStream;
29
use tokio_util::compat::TokioAsyncReadCompatExt;
30
use tor_checkable::TimeBound;
31
use tor_dirclient::request::{AuthCertRequest, ConsensusRequest, Requestable};
32
use tor_dircommon::{authority::AuthorityContacts, config::DirTolerance};
33
use tor_error::{internal, into_internal};
34
use tor_netdoc::{
35
    doc::{
36
        authcert::{AuthCertKeyIds, AuthCertUnverified},
37
        netstatus::{ConsensusFlavor, md, plain},
38
    },
39
    parse2::{self, NetdocParseable, NetdocParseableUnverified, ParseInput},
40
};
41
use tor_rtcompat::PreferredRuntime;
42
use tracing::{debug, warn};
43

            
44
use crate::{
45
    database::{self as db, AuthCertMeta, ConsensusMeta, ContentEncoding, Timestamp},
46
    err::{AuthorityRequestError, DatabaseError, OperationError},
47
};
48

            
49
mod poc;
50

            
51
/// The various states for the [`StaticEngine`].
52
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display)]
53
enum State {
54
    /// Loads the most recent valid (and verified) consensus from the database
55
    /// into memory.
56
    ///
57
    /// Transitions from:
58
    /// * Start, if a recent valid consensus exists in the database.
59
    /// * [`State::StoreConsensus`], if successfully finished.
60
    ///
61
    /// Transitions into:
62
    /// * [`State::Descriptors`]
63
    LoadConsensus,
64

            
65
    /// Downloads the most recent consensus from a directory authority.
66
    ///
67
    /// Transitions from:
68
    /// * Start, if no recent valid consensus exists in the database.
69
    /// * [`State::Descriptors`], if lifetime is over.
70
    /// * [`State::Hibernate`], if lifetime is over.
71
    ///
72
    /// Transitions into:
73
    /// * [`State::AuthCerts`], if we miss authority certificates.
74
    /// * [`State::StoreConsensus`], if all authority certificates exist in the
75
    ///   database.
76
    // TODO DIRMIRROR: What to do in the case of getting an invalid consensus
77
    // such as junk data?  The normal retry logic sounds reasonable here.
78
    FetchConsensus,
79

            
80
    /// Downloads, validates, and stores the missing authority certificates from
81
    /// the downloaded unvalidated consensus into the database.
82
    ///
83
    /// Transitions from:
84
    /// * [`State::FetchConsensus`], if we miss authority certificates.
85
    /// * [`State::AuthCerts`], if we still miss authority certificates.
86
    ///
87
    /// Transitions into:
88
    /// * [`State::AuthCerts`], if we still miss authority certificates.
89
    /// * [`State::StoreConsensus`], if we got all authority certificates.
90
    // TODO DIRMIRROR: What to do in the case of a MITM attack where an attacker
91
    // adds lots of invalid signature items at the bottom, leading to lots of
92
    // queries for directory authority certificates, which may succeed or not?
93
    // Best idea is probably to only download authcerts whose id fingerprints
94
    // are configured in our AuthorityContacts, because then we have an upper
95
    // limit.
96
    AuthCerts,
97

            
98
    /// Validates and stores the downloaded unvalidated consensus into the
99
    /// database.
100
    ///
101
    /// Transitions from:
102
    /// * [`State::FetchConsensus`], if we have all authority certificates.
103
    /// * [`State::AuthCerts`], if we have all authority certificates.
104
    ///
105
    /// Transitions into:
106
    /// * [`State::LoadConsensus`]
107
    StoreConsensus,
108

            
109
    /// Downloads missing network documents (descriptors) from a directory
110
    /// authority.
111
    ///
112
    /// Transitions from:
113
    /// * [`State::LoadConsensus`], if we initialize.
114
    /// * [`State::Descriptors`], if we still have missing descriptors left.
115
    ///
116
    /// Transitions into:
117
    /// * [`State::FetchConsensus`], if lifetime is over.
118
    /// * [`State::Descriptors`], if we still have missing descriptors left.
119
    /// * [`State::Hibernate`], if nothing is left.
120
    Descriptors,
121

            
122
    /// Hibernate because nothing is left.
123
    ///
124
    /// Transitions from:
125
    /// * [`State::Descriptors`]
126
    ///
127
    /// Transitions into:
128
    /// * [`State::FetchConsensus`], if the lifetime is over.
129
    Hibernate,
130
}
131

            
132
/// The execution engine for the finite state machine.
133
///
134
/// The states themselves are explained in [`State`].
135
///
136
/// This data structure itself is static and contains no state, but merely
137
/// configuration primitives that stay constant throughout the runtime of the
138
/// program, such as the [`ConsensusFlavor`], the [`AuthorityContacts`], and the
139
/// [`DirTolerance`].  It can be kept throughout the entire runtime and only
140
/// consists for convenience in order to not give each state machine related
141
/// (then static) method a super long signature containing these fields.
142
///
143
/// The state itself is computed fully deterministically from the data found
144
/// within the database and [`ConsensusBoundData`].
145
///
146
/// This is the reason on why this structure is not called `StateMachine`,
147
/// because this implies that the type in itself carries state, which is not
148
/// true, because the state is stored entirely external, with this engine
149
/// only processing and modifying it.
150
///
151
/// See [`StaticEngine::determine_state()`] for more details.
152
#[derive(Debug)]
153
struct StaticEngine {
154
    /// The flavor of the consensus we are serving.
155
    flavor: ConsensusFlavor,
156

            
157
    /// The authorities we are acknowledging.
158
    authorities: AuthorityContacts,
159

            
160
    /// The document tolerance we are accepting.
161
    tolerance: DirTolerance,
162

            
163
    /// The preferred runtime for compatibility with other arti crates.
164
    ///
165
    /// Generally obtained through [`PreferredRuntime::current()`].
166
    rt: PreferredRuntime,
167
}
168

            
169
/// Additional state machine data concerning a single consensus.
170
///
171
/// This enum stores and keeps track of the consensus we are serving and in
172
/// which ✨state✨ it is currently in, such as whether it is verified or not,
173
/// or if we even have a state loaded in memory in the first place.
174
#[derive(Debug, Clone)]
175
enum ConsensusBoundData {
176
    /// No state is loaded in memory at the moment.
177
    None,
178

            
179
    /// We have downloaded a consensus but it is not yet verified.
180
    Unverified {
181
        /// The unverified parsed consensus we have.
182
        // TODO DIRMIRROR: Make this optional, see comment in
183
        // StaticEngine::execute.
184
        consensus: FlavoredConsensusSigned,
185

            
186
        /// The unparsed raw consensus we have.
187
        raw: String,
188
    },
189

            
190
    /// We have downloaded and verified a consensus.
191
    Verified {
192
        /// The verified consensus we have.
193
        consensus: FlavoredConsensus,
194

            
195
        /// When to stop dealing with this consensus and fetching a new one.
196
        lifetime: Timestamp,
197

            
198
        /// SHA-1 digests of the missing server descriptors in the consensus.
199
        server_queue: HashSet<db::Sha1>,
200

            
201
        /// SHA-1 digests of the missing extra-info descriptors in the server
202
        /// descriptors of the consensus.
203
        ///
204
        /// extra-info documents are only transitively related to a consensus
205
        /// through consensus -> server descriptors -> extra-info descriptors
206
        extra_queue: HashSet<db::Sha1>,
207

            
208
        /// SHA-256 digests of the missing micro descriptors in the consensus.
209
        ///
210
        /// This field is technically mutually exclusive to server_queue and
211
        /// extra_queue because micro descriptors are only found in
212
        /// [`ConsensusFlavor::Microdesc`] and server plus extra-info
213
        /// descriptors only in [`ConsensusFlavor::Plain`].  However, because
214
        /// we used a queue based design, we just leave the queue empty instead
215
        /// of wrapping this behind an enum variant for true mutual exclusivity.
216
        /// This makes coding much easier with less boilerplate and neglectable
217
        /// additional runtime cost.
218
        micro_queue: HashSet<db::Sha256>,
219
    },
220
}
221

            
222
/// A [`ConsensusFlavor`]-like wrapper for verified network statuses.
223
///
224
/// This is required because we need to obtain, at least partial, data from
225
/// each consensus, such as the signature (although not this type), the router
226
/// descriptors, validity, and other information.
227
///
228
/// At the current moment, [`tor_netdoc`] itself does not offer things such as
229
/// a common trait for retrieving the common fields, making this structure
230
/// necessary, or alternatively lots of macro magic similar to [`tor_netdoc`].
231
///
232
/// TODO DIRMIRROR: Either add a trait for [`tor_netdoc`] or figure out if the
233
/// fields we require are all of the same type in both, so we can only store
234
/// the fields we are interested in, though this is probably only possible once
235
/// we reached later stages of code.
236
///
237
/// And no, [`std::any::Any`] is not an alternative I am willing to do.
238
#[derive(Debug, Clone)]
239
enum FlavoredConsensus {
240
    /// For plain consensuses.
241
    Plain(plain::NetworkStatus),
242

            
243
    /// For microdescriptor consensuses.
244
    Md(md::NetworkStatus),
245
}
246

            
247
/// A [`ConsensusFlavor`]-like wrapper for unverified network statuses.
248
///
249
/// TODO DIRMIRROR: See the [`FlavoredConsensus`] trait comment.
250
#[derive(Debug, Clone)]
251
enum FlavoredConsensusSigned {
252
    /// For plain consensuses.
253
    Plain(plain::NetworkStatusUnverified),
254

            
255
    /// For microdescriptor consensus.
256
    Md(md::NetworkStatusUnverified),
257
}
258

            
259
impl StaticEngine {
260
    /// Determines the [`State`] only from the database and [`ConsensusBoundData`].
261
    ///
262
    /// This method is fully idempotent, meaning it only depends upon the data
263
    /// found within the database and the [`ConsensusBoundData`]; there is no
264
    /// internal `state` variable or something contained within [`StaticEngine`].
265
6
    fn determine_state(
266
6
        &self,
267
6
        tx: &Transaction<'_>,
268
6
        data: &ConsensusBoundData,
269
6
        now: Timestamp,
270
6
    ) -> Result<State, DatabaseError> {
271
        // Determine the state primarily upon ConsensusBoundData combined with
272
        // a few database queries, as well as the current time of course.
273
6
        let state = match data {
274
            // ConsensusBoundData::None means that we currently have no
275
            // consensus in memory.  This may be the case because we just
276
            // started up or because we just downloaded, validated, and inserted
277
            // a consensus into the database and reset ConsensusBoundData to
278
            // None afterwards.
279
            ConsensusBoundData::None => {
280
                // Check whether there is a valid consensus in the database at all.
281
                //
282
                // Yes, it is kinda redundant querying a consensus here
283
                // and potentially again when loading the consensus, but SQLite
284
                // is very fast and having to maintain two different queries,
285
                // one for checking and one for selecting, is prone to get
286
                // out-of-sync.
287
2
                match ConsensusMeta::query_recent(tx, self.flavor, &self.tolerance, now)? {
288
                    // Some consensus means we can load it.
289
                    Some(_) => State::LoadConsensus,
290

            
291
                    // None means we must download it.
292
2
                    None => State::FetchConsensus,
293
                }
294
            }
295

            
296
            // ConsensusBoundData::Unverified means that we recently downloaded
297
            // a consensus through State::FetchConsensus.  It is not fully
298
            // validated yet and we may not even be able due to missing
299
            // authority certificates.
300
4
            ConsensusBoundData::Unverified { consensus, .. } => {
301
                // Check whether there any missing authority certificates that
302
                // have signed the consensus.
303
4
                let missing_certs = !AuthCertMeta::query_recent(
304
4
                    tx,
305
4
                    &consensus.signatories(),
306
4
                    &self.tolerance,
307
4
                    now,
308
                )?
309
                .1
310
4
                .is_empty();
311

            
312
4
                if missing_certs {
313
                    // Missing authority certificates means we must download
314
                    // them.
315
2
                    State::AuthCerts
316
                } else {
317
                    // If we have all authority certificates, we can validate
318
                    // and store it inside the database.
319
2
                    State::StoreConsensus
320
                }
321
            }
322

            
323
            // ConsensusBoundData::Verified means that we have successfully
324
            // loaded a recent valid consensus from the database using
325
            // State::LoadConsensus.  Depending on this, we download the missing
326
            // network documents (descriptors) from a directory authority, if
327
            // any.
328
            ConsensusBoundData::Verified {
329
                lifetime,
330
                server_queue: servers,
331
                extra_queue: extras,
332
                micro_queue: micros,
333
                ..
334
            } => {
335
                if *lifetime <= now {
336
                    // The lifetime has been surpassed, download a new
337
                    // consensus.  It is very important TO NOT transition to
338
                    // State::LoadConsensus here, because the current consensus
339
                    // may still be valid but not fresh anymore, in which case
340
                    // State::LoadConsensus will continue to obtain it from the
341
                    // database until valid-after has been surpassed, which is
342
                    // most definitely not what we want.
343
                    State::FetchConsensus
344
                } else if servers.is_empty() && extras.is_empty() && micros.is_empty() {
345
                    // All queues are empty, meaning we are done, until lifetime
346
                    // ends.
347
                    State::Hibernate
348
                } else {
349
                    // The lifetime has not been surpassed and we have stuff
350
                    // to download, so we need to obtain the descriptors.
351
                    State::Descriptors
352
                }
353
            }
354
        };
355
6
        Ok(state)
356
6
    }
357

            
358
    /// Executes a single state iteration in the finite state machine.
359
    ///
360
    /// The return value is of type [`Result<(), OperationError>`].
361
    /// The success type is not of much interest for calling applications.
362
    /// However, the error case itself should be passed towards
363
    /// [`crate::err::IsFatal::is_fatal()`] in order to either abort the
364
    /// application or retry with an appropriate timeout.
365
    ///
366
    // TODO: Use tracing instrumentation here.
367
    // TODO DIRMIRROR: Document the state transition check which we have to do
368
    // because of database invariances no longer holding true.
369
    async fn execute<R: Rng>(
370
        &self,
371
        pool: &Pool<SqliteConnectionManager>,
372
        data: &mut ConsensusBoundData,
373
        endpoint: &[SocketAddr],
374
        now: Timestamp,
375
        rng: &mut R,
376
    ) -> Result<(), OperationError> {
377
        // TODO: Should we return DatabaseError or something like
378
        // StateDeterminationError?  Either way, both cases should be seriously
379
        // fatal.
380
        let state = db::read_tx(pool, |tx| self.determine_state(tx, data, now))??;
381
        debug!("state is {state}");
382

            
383
        match state {
384
            State::LoadConsensus => self.load_consensus(pool, data, now, rng),
385
            State::FetchConsensus => Ok(self.fetch_consensus(data, endpoint).await?),
386
            State::AuthCerts => self.auth_certs(pool, data, endpoint, now).await,
387
            State::StoreConsensus => todo!(),
388
            State::Descriptors => todo!(),
389
            State::Hibernate => self.hibernate(data, now).await,
390
        }
391
    }
392

            
393
    /// Executes [`State::LoadConsensus`].
394
    ///
395
    /// This method does the following:
396
    /// * Load the most recent valid consensus from the database.
397
    /// * Compute the lifetime for it.
398
    /// * Compute the missing descriptors for it.
399
2
    fn load_consensus<R: Rng>(
400
2
        &self,
401
2
        pool: &Pool<SqliteConnectionManager>,
402
2
        data: &mut ConsensusBoundData,
403
2
        now: Timestamp,
404
2
        rng: &mut R,
405
2
    ) -> Result<(), OperationError> {
406
        // Load the most recent valid consensus from the database.
407
        //
408
        // If there is no consensus, we should have not entered the state, which
409
        // means that the database must have been externally verified.
410
        // In this case, it is probably better to return a bug, as external
411
        // applications arbitrarily modifying the database while we are running
412
        // leaves too much room for wrong/weird behavior.
413
2
        let (server_queue, extra_queue, micro_queue, lifetime, consensus) =
414
2
            db::read_tx(pool, |tx| {
415
2
                let meta = ConsensusMeta::query_recent(tx, self.flavor, &self.tolerance, now)?
416
2
                    .ok_or(internal!("database externally modified?"))?;
417
2
                let server_queue = meta.missing_servers(tx)?;
418
2
                let extra_queue = meta.missing_extras(tx)?;
419
2
                let micro_queue = meta.missing_micros(tx)?;
420
2
                let lifetime = meta.lifetime(rng);
421
2
                let consensus = meta.data(tx)?;
422
2
                Ok::<_, DatabaseError>((
423
2
                    server_queue,
424
2
                    extra_queue,
425
2
                    micro_queue,
426
2
                    lifetime,
427
2
                    consensus,
428
2
                ))
429
2
            })??;
430

            
431
        // Parse the most recent valid consensus from the database.
432
        //
433
        // TODO DIRMIRROR:
434
        // Because only valid documents may exist in the database, it should
435
        // succeed.  However, there is this weird edge-case where we may have
436
        // inserted a document with a field we do not understand because of
437
        // using an old version.  After upgrading our version we may now
438
        // understand the field and realize it is wrong, leading to a violation
439
        // of this constraint.  Handling this is not very easy; I suppose adding
440
        // an additional column to the meta table storing the last used crate
441
        // version is a sensible idea, with upgrades and downgrades leading to
442
        // a parsing of all network documents within the database, throwing the
443
        // ones out we do not understand (anymore).
444
        //
445
        // See also the relevant MR discussion:
446
        // <https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/3664#note_3352723>
447
2
        let consensus = match self.flavor {
448
            ConsensusFlavor::Plain => FlavoredConsensus::Plain(
449
2
                parse2::parse_netdoc::<plain::NetworkStatusUnverified>(&ParseInput::new(
450
2
                    &consensus, "",
451
2
                ))
452
2
                .map_err(into_internal!("invalid netdoc in database?"))?
453
                // TODO DIRMIRROR: explain why this is OK, or re-verify the signatures
454
2
                .unwrap_unverified()
455
                .0,
456
            ),
457
            ConsensusFlavor::Microdesc => FlavoredConsensus::Md(
458
                parse2::parse_netdoc::<md::NetworkStatusUnverified>(&ParseInput::new(
459
                    &consensus, "",
460
                ))
461
                .map_err(into_internal!("invalid netdoc in database?"))?
462
                // TODO DIRMIRROR: explain why this is OK, or re-verify the signatures
463
                .unwrap_unverified()
464
                .0,
465
            ),
466
        };
467

            
468
2
        *data = ConsensusBoundData::Verified {
469
2
            consensus,
470
2
            lifetime,
471
2
            server_queue,
472
2
            extra_queue,
473
2
            micro_queue,
474
2
        };
475
2
        Ok(())
476
2
    }
477

            
478
    /// Fetches a consensus from an upstream authority.
479
    // TODO DIRMIRROR: Add logging.
480
    #[allow(clippy::string_slice)] // TODO
481
2
    async fn fetch_consensus(
482
2
        &self,
483
2
        data: &mut ConsensusBoundData,
484
2
        endpoint: &[SocketAddr],
485
3
    ) -> Result<(), AuthorityRequestError> {
486
        // Obtain the consensus.
487
2
        let mut consensus: VecDeque<_> = match self.flavor {
488
2
            ConsensusFlavor::Plain => self
489
2
                .send_request(endpoint, ConsensusRequest::new(self.flavor))
490
2
                .await
491
2
                .map(|(raw, doc)| {
492
2
                    doc.into_iter()
493
2
                        .map(|(doc, start, end)| {
494
2
                            (
495
2
                                raw[start..end].to_owned(),
496
2
                                FlavoredConsensusSigned::Plain(doc),
497
2
                            )
498
2
                        })
499
2
                        .collect()
500
2
                }),
501
            ConsensusFlavor::Microdesc => self
502
                .send_request(endpoint, ConsensusRequest::new(self.flavor))
503
                .await
504
                .map(|(raw, doc)| {
505
                    doc.into_iter()
506
                        .map(|(doc, start, end)| {
507
                            (raw[start..end].to_owned(), FlavoredConsensusSigned::Md(doc))
508
                        })
509
                        .collect()
510
                }),
511
        }?;
512

            
513
        // Check for the correct number of results.
514
2
        if consensus.len() != 1 {
515
            return Err(AuthorityRequestError::Response(
516
                "invalid number of consensus?",
517
            ));
518
2
        }
519

            
520
        // expect is fine because we checked the length for one above.
521
2
        let (raw, consensus) = consensus.pop_front().expect("pop_front");
522

            
523
        // And store it.
524
2
        *data = ConsensusBoundData::Unverified { consensus, raw };
525

            
526
2
        Ok(())
527
2
    }
528

            
529
    /// Fetches, validates, and stores authority certificates.
530
    //
531
    // TODO DIRMIRROR: Right now, there is a torspec DoS issue.
532
    // An attacker may add lots of garbage signatures and we will fetch them
533
    // Even checking the ID PK against v3idents is not useful because an
534
    // attacker may still use the same ID PK dozens of times with various
535
    // SK PKs.  A good fix would include checking that no ID PK is duplicate
536
    // AND to ignore all ID PKs we do not recognize.  Also, it would probably
537
    // be best to move the v3idents structure to a HashMap based implementation,
538
    // as well as the signatories result.
539
    #[allow(clippy::string_slice)] // TODO
540
2
    async fn auth_certs(
541
2
        &self,
542
2
        pool: &Pool<SqliteConnectionManager>,
543
2
        data: &mut ConsensusBoundData,
544
2
        endpoint: &[SocketAddr],
545
2
        now: Timestamp,
546
3
    ) -> Result<(), OperationError> {
547
        // Obtain the signatories of the current unverified consensus.
548
2
        let signatories = match data {
549
2
            ConsensusBoundData::Unverified { consensus, .. } => consensus.signatories(),
550
            _ => return Err(OperationError::Bug(internal!("data is not unverified"))),
551
        };
552

            
553
        // Obtain the missing certificate identifiers.
554
2
        let (_, missing) = db::read_tx(pool, |tx| {
555
2
            AuthCertMeta::query_recent(tx, &signatories, &self.tolerance, now)
556
2
        })??;
557
2
        if missing.is_empty() {
558
            // Although not technically fatal, retrying when the database was
559
            // externally modified does not make much sense.
560
            return Err(OperationError::Bug(internal!(
561
                "database externally modified?"
562
            )));
563
2
        }
564

            
565
        // Compose the request.
566
2
        let mut requ = AuthCertRequest::new();
567
18
        for kp in missing.iter().copied() {
568
18
            requ.push(kp);
569
18
        }
570

            
571
        // Fire it off.
572
2
        let (resp, certs) = self
573
2
            .send_request::<_, AuthCertUnverified>(endpoint, requ)
574
2
            .await?;
575

            
576
        // Verify each certificate.  Invalid certificates and other problems get
577
        // logged and filtered out, with the result being then inserted into
578
        // the database.
579
2
        let certs = certs
580
2
            .into_iter()
581
34
            .filter_map(|(unverified, start, end)| {
582
34
                let unverified_body = unverified.inspect_unverified().0;
583
34
                let kp = AuthCertKeyIds {
584
34
                    id_fingerprint: unverified_body.dir_identity_key.to_rsa_identity(),
585
34
                    sk_fingerprint: unverified_body.dir_signing_key.to_rsa_identity(),
586
34
                };
587

            
588
                // Skip certificates we did not asked for.
589
                //
590
                // Not much of an issue because certificate verification will
591
                // usually fail anyways, except for this weird edge-case where we
592
                // actually have that id fingerprint in the v3idents.
593
34
                if !missing.contains(&kp) {
594
16
                    debug!("authority returned certificate we did not asked for: {kp:?}");
595
16
                    return None;
596
18
                }
597

            
598
18
                let verified = unverified
599
18
                    .verify(self.authorities.v3idents())
600
18
                    .and_then(|v| {
601
18
                        Ok(self
602
18
                            .tolerance
603
18
                            .extend_tolerance(v)
604
18
                            .if_valid_at(&now.into())?)
605
18
                    });
606
18
                let verified = match verified {
607
18
                    Ok(v) => v,
608
                    Err(e) => {
609
                        // TODO DIRMIRROR: Log the actual cert.
610
                        warn!("received invalid auth cert: {e}",);
611
                        return None;
612
                    }
613
                };
614

            
615
18
                Some((verified, &resp[start..end]))
616
34
            })
617
2
            .collect::<Vec<_>>();
618

            
619
        // When we have reached this, it means that this call made no progress,
620
        // i.e. the authority only returned certificates we were not interested
621
        // in.
622
2
        if certs.is_empty() {
623
            Err(Box::new(AuthorityRequestError::Response(
624
                "response lead to no progress",
625
            )))?;
626
2
        }
627

            
628
        // Finally, insert them all into the database.
629
2
        db::rw_tx(pool, |tx| {
630
18
            for (cert, data) in certs {
631
18
                AuthCertMeta::insert(tx, ContentEncoding::iter(), &cert, data)?;
632
            }
633
2
            Ok::<_, DatabaseError>(())
634
2
        })??;
635

            
636
2
        Ok(())
637
2
    }
638

            
639
    /// Hibernates for the remaining lifetime of the consensus.
640
    async fn hibernate(
641
        &self,
642
        data: &mut ConsensusBoundData,
643
        now: Timestamp,
644
    ) -> Result<(), OperationError> {
645
        match data {
646
            ConsensusBoundData::None | ConsensusBoundData::Unverified { .. } => {
647
                // This should not happen, we only enter hibernation in a state
648
                // that already has a verified consensus.
649
                return Err(internal!("hibernating without a verified consensus?").into());
650
            }
651
            ConsensusBoundData::Verified { lifetime, .. } => {
652
                let timeout = *lifetime - now;
653
                debug!("hibernating for {}s", timeout.as_secs());
654
                tokio::time::sleep(timeout).await;
655
            }
656
        }
657

            
658
        Ok(())
659
    }
660

            
661
    /// Convenience wrapper around [`tor_dirclient::send_request()`].
662
    ///
663
    /// It opens a TCP connection, performs the request, and parses the result.
664
    ///
665
    /// Returns the raw response alongside the output of
666
    /// [`parse2::parse_netdoc_multiple_with_offsets()`].
667
    ///
668
    /// The output is required because we need the raw document alongside the
669
    /// offsets to have the actual data we will insert into the database later
670
    /// on.
671
4
    async fn send_request<R: Requestable, T: NetdocParseable>(
672
4
        &self,
673
4
        endpoint: &[SocketAddr],
674
4
        requ: R,
675
4
    ) -> Result<(String, Vec<(T, usize, usize)>), AuthorityRequestError> {
676
        // The check is required to not let Tokio panic.
677
4
        if endpoint.is_empty() {
678
            return Err(AuthorityRequestError::Bug(internal!("empty endpoint?")));
679
4
        }
680

            
681
        // Open the TCP connection.
682
4
        let mut stream = TcpStream::connect(endpoint)
683
4
            .await
684
4
            .map_err(AuthorityRequestError::TcpConnect)?
685
4
            .compat();
686

            
687
        // Perform the request and map the result nicely.
688
4
        let resp = tor_dirclient::send_request(&self.rt, &requ, &mut stream, None)
689
4
            .await
690
4
            .map(|resp| resp.output_string().map(|resp| resp.to_owned()));
691

            
692
        // We can immediately drop the connection now, no need to occupy even
693
        // more resources from the authority.  Doing so is fine, it is HTTP/1.0
694
        // and there is no connection reuse anyways.
695
4
        drop(stream);
696

            
697
        // Returning all request failed errors is okay; they all imply that
698
        // retrying from a different authority is fine.
699
        // TODO MSRV: If possible, use Result::flatten once MSRV 1.89.
700
4
        let resp = match resp {
701
4
            Ok(Ok(r)) => Ok(r),
702
            Ok(Err(e)) => Err(e),
703
            Err(tor_dirclient::Error::RequestFailed(e)) => Err(e),
704
            Err(e) => {
705
                return Err(AuthorityRequestError::Bug(internal!(
706
                    "unhandled dirclient error: {e}"
707
                )));
708
            }
709
        }?;
710

            
711
        // Parse the response.
712
4
        let parsed = parse2::parse_netdoc_multiple_with_offsets(&ParseInput::new(&resp, ""))?;
713

            
714
4
        Ok((resp, parsed))
715
4
    }
716
}
717

            
718
impl FlavoredConsensusSigned {
719
    /// Wrapper to obtain the signatories of a flavored consensus.
720
8
    fn signatories(&self) -> Vec<AuthCertKeyIds> {
721
8
        let sigs = match &self {
722
8
            Self::Plain(plain) => &plain.sigs.sigs.directory_signature,
723
            Self::Md(md) => &md.sigs.sigs.directory_signature,
724
        };
725
8
        sigs.iter().map(|sig| sig.key_ids).collect()
726
8
    }
727
}
728

            
729
#[cfg(test)]
730
mod test {
731
    // @@ begin test lint list maintained by maint/add_warning @@
732
    #![allow(clippy::bool_assert_comparison)]
733
    #![allow(clippy::clone_on_copy)]
734
    #![allow(clippy::dbg_macro)]
735
    #![allow(clippy::mixed_attributes_style)]
736
    #![allow(clippy::print_stderr)]
737
    #![allow(clippy::print_stdout)]
738
    #![allow(clippy::single_char_pattern)]
739
    #![allow(clippy::unwrap_used)]
740
    #![allow(clippy::unchecked_time_subtraction)]
741
    #![allow(clippy::useless_vec)]
742
    #![allow(clippy::needless_pass_by_value)]
743
    #![allow(clippy::string_slice)] // See arti#2571
744
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
745

            
746
    use std::time::{Duration, SystemTime};
747

            
748
    use rusqlite::named_params;
749
    use tokio::{
750
        io::{AsyncReadExt, AsyncWriteExt},
751
        net::TcpListener,
752
    };
753
    use tor_basic_utils::test_rng::testing_rng;
754
    use tor_netdoc::parse2::NetdocParseableUnverified;
755

            
756
    use crate::database::sql;
757

            
758
    use super::*;
759

            
760
    fn create_dummy_db() -> Pool<SqliteConnectionManager> {
761
        let pool = db::open("").unwrap();
762

            
763
        let mut conn = pool.get().unwrap();
764
        let tx = conn.transaction().unwrap();
765

            
766
        let cons_docid = db::store_insert(
767
            &tx,
768
            include_bytes!("../../testdata/consensus-ns"),
769
            std::iter::empty(),
770
        )
771
        .unwrap();
772
        let ns1_docid = db::store_insert(
773
            &tx,
774
            include_bytes!("../../testdata/descriptor1-ns"),
775
            std::iter::empty(),
776
        )
777
        .unwrap();
778
        let extra1_docid = db::store_insert(
779
            &tx,
780
            include_bytes!("../../testdata/descriptor1-extra-info"),
781
            std::iter::empty(),
782
        )
783
        .unwrap();
784

            
785
        tx.execute(
786
            sql!(
787
                "
788
                INSERT INTO router_extra_info (docid, unsigned_sha1, kp_relay_id_rsa_sha1)
789
                VALUES
790
                (:docid, :sha1, :fingerprint)
791
                "
792
            ),
793
            named_params! {
794
                ":docid": extra1_docid,
795
                ":sha1": db::Sha1::digest(include_bytes!("../../testdata/descriptor1-extra-info-unsigned")),
796
                ":fingerprint": "000004ACBB9D29BCBA17256BB35928DDBFC8ABA9"
797
            },
798
        )
799
        .unwrap();
800
        tx.execute(
801
            sql!(
802
                "
803
                INSERT INTO router_descriptor
804
                (docid, unsigned_sha1, unsigned_sha2, kp_relay_id_rsa_sha1, flavor, extra_unsigned_sha1)
805
                VALUES
806
                (:docid, :sha1, :sha2, :fingerprint, 'ns', :extra)
807
                "
808
            ),
809
            named_params! {
810
                ":docid": ns1_docid,
811
                ":sha1": db::Sha1::digest(include_bytes!("../../testdata/descriptor1-ns-unsigned")),
812
                ":sha2": db::Sha256::digest(include_bytes!("../../testdata/descriptor1-ns-unsigned")),
813
                ":fingerprint": "000004ACBB9D29BCBA17256BB35928DDBFC8ABA9",
814
                ":extra": db::Sha1::digest(include_bytes!("../../testdata/descriptor1-extra-info-unsigned")),
815
            },
816
        )
817
        .unwrap();
818

            
819
        tx.execute(
820
            sql!(
821
                "
822
                INSERT INTO consensus
823
                (docid, unsigned_sha3_256, flavor, valid_after, fresh_until, valid_until)
824
                VALUES
825
                (:docid, :sha3, 'ns', :valid_after, :fresh_until, :valid_until)
826
                "
827
            ),
828
            named_params! {
829
                ":docid": cons_docid,
830
                ":sha3": "0000000000000000000000000000000000000000000000000000000000000000",
831
                ":valid_after": 1769698800,
832
                ":fresh_until": 1769702400,
833
                ":valid_until": 1769709600,
834
            },
835
        )
836
        .unwrap();
837

            
838
        tx.execute(
839
            sql!(
840
                "
841
                INSERT INTO consensus_router_descriptor_member
842
                (consensus_docid, unsigned_sha1, unsigned_sha2)
843
                VALUES
844
                (:cons_docid, :ns1_sha1, NULL),
845
                (:cons_docid, :ns2_sha1, NULL)
846
                "
847
            ),
848
            named_params! {
849
                ":cons_docid": cons_docid,
850
                ":ns1_sha1": db::Sha1::digest(include_bytes!("../../testdata/descriptor1-ns-unsigned")),
851
                ":ns2_sha1": db::Sha1::digest(include_bytes!("../../testdata/descriptor2-ns-unsigned")),
852
            },
853
        )
854
        .unwrap();
855

            
856
        tx.commit().unwrap();
857

            
858
        pool
859
    }
860

            
861
    #[tokio::test]
862
    async fn state_load_consensus() {
863
        let pool = create_dummy_db();
864
        let mut data = ConsensusBoundData::None;
865
        let engine = StaticEngine {
866
            flavor: ConsensusFlavor::Plain,
867
            authorities: AuthorityContacts::default(),
868
            tolerance: DirTolerance::default(),
869
            rt: PreferredRuntime::current().unwrap(),
870
        };
871

            
872
        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1769700600); // 2026-01-29 15:30:00
873
        let time: Timestamp = time.into();
874
        let fresh_until = time + Duration::from_secs(60 * 30);
875
        let fresh_until_half = fresh_until + Duration::from_secs(60 * 60);
876

            
877
        engine
878
            .load_consensus(&pool, &mut data, time, &mut testing_rng())
879
            .unwrap();
880

            
881
        // El-cheapo assert_eq due to lack of PartialEq for tor-netdoc poc.
882
        match data {
883
            ConsensusBoundData::Verified {
884
                consensus,
885
                lifetime,
886
                server_queue,
887
                extra_queue,
888
                micro_queue,
889
            } => {
890
                match consensus {
891
                    FlavoredConsensus::Plain(_) => {}
892
                    _ => panic!("consensus not ns"),
893
                }
894
                assert_eq!(
895
                    server_queue,
896
                    HashSet::from([db::Sha1::digest(include_bytes!(
897
                        "../../testdata/descriptor2-ns-unsigned"
898
                    ))])
899
                );
900
                assert!(lifetime >= fresh_until);
901
                assert!(lifetime <= fresh_until_half);
902
                assert!(extra_queue.is_empty());
903
                assert!(micro_queue.is_empty());
904
            }
905
            _ => panic!("data is not verified"),
906
        }
907
    }
908

            
909
    #[tokio::test]
910
    async fn state_fetch_consensus() {
911
        let pool = create_dummy_db();
912
        let mut data = ConsensusBoundData::None;
913
        let engine = StaticEngine {
914
            flavor: ConsensusFlavor::Plain,
915
            authorities: AuthorityContacts::default(),
916
            tolerance: DirTolerance::default(),
917
            rt: PreferredRuntime::current().unwrap(),
918
        };
919

            
920
        let state = db::read_tx(&pool, |tx| {
921
            engine.determine_state(tx, &data, SystemTime::UNIX_EPOCH.into())
922
        })
923
        .unwrap()
924
        .unwrap();
925
        assert_eq!(state, State::FetchConsensus);
926

            
927
        let server = TcpListener::bind("[::1]:0").await.unwrap();
928
        let saddr = server.local_addr().unwrap();
929
        tokio::spawn(async move {
930
            let (mut stream, _) = server.accept().await.unwrap();
931
            let mut buf = vec![0; 1024];
932
            let _ = stream.read(&mut buf).await.unwrap();
933

            
934
            let consensus = include_str!("../../testdata/consensus-ns");
935
            let resp = format!(
936
                "HTTP/1.0 200 OK\r\nContent-Encoding: identity\r\nContent-Length: {}\r\n\r\n{consensus}",
937
                consensus.len()
938
            );
939
            stream.write_all(resp.as_bytes()).await.unwrap();
940
        });
941

            
942
        engine.fetch_consensus(&mut data, &[saddr]).await.unwrap();
943
        match data {
944
            ConsensusBoundData::Unverified { consensus, raw } => match consensus {
945
                FlavoredConsensusSigned::Plain(plain) => {
946
                    // El-cheapo verification, this is not a parser unit test.
947
                    assert_eq!(plain.unwrap_unverified().0.routers.len(), 2);
948
                    assert_eq!(raw, include_str!("../../testdata/consensus-ns"));
949
                }
950
                _ => panic!("data is not unverified ns consensus"),
951
            },
952
            _ => panic!("data is not unverified"),
953
        }
954
    }
955

            
956
    #[tokio::test]
957
    async fn state_auth_certs() {
958
        let pool = create_dummy_db();
959
        let mut data = ConsensusBoundData::Unverified {
960
            consensus: FlavoredConsensusSigned::Plain(
961
                parse2::parse_netdoc(&ParseInput::new(
962
                    include_str!("../../testdata/consensus-ns"),
963
                    "",
964
                ))
965
                .unwrap(),
966
            ),
967
            raw: include_str!("../../testdata/consensus-ns").to_owned(),
968
        };
969
        let engine = StaticEngine {
970
            flavor: ConsensusFlavor::Plain,
971
            authorities: AuthorityContacts::default(),
972
            tolerance: DirTolerance::default(),
973
            rt: PreferredRuntime::current().unwrap(),
974
        };
975

            
976
        assert_eq!(
977
            db::read_tx(&pool, |tx| engine.determine_state(
978
                tx,
979
                &data,
980
                SystemTime::UNIX_EPOCH.into()
981
            ))
982
            .unwrap()
983
            .unwrap(),
984
            State::AuthCerts
985
        );
986

            
987
        let server = TcpListener::bind("[::1]:0").await.unwrap();
988
        let saddr = server.local_addr().unwrap();
989
        tokio::spawn(async move {
990
            let mut buf = [0; 1024];
991
            let (mut stream, _) = server.accept().await.unwrap();
992
            let _ = stream.read(&mut buf).await.unwrap();
993

            
994
            let authcerts = include_str!("../../testdata/authcert-all");
995

            
996
            stream.write_all(format!(
997
                "HTTP/1.0 200 OK\r\nContent-Encoding: identity\r\nContent-Length: {}\r\n\r\n{authcerts}",
998
                authcerts.len()
999
            ).as_bytes()).await.unwrap();
        });
        // Fetch all authcerts.
        engine
            .auth_certs(
                &pool,
                &mut data,
                &[saddr],
                (SystemTime::UNIX_EPOCH + Duration::from_secs(1770639454)).into(), // Mon Feb  9 12:17:34 UTC 2026
            )
            .await
            .unwrap();
        // Check whether we are done with all authcerts.
        assert_eq!(
            db::read_tx(&pool, |tx| engine.determine_state(
                tx,
                &data,
                (SystemTime::UNIX_EPOCH + Duration::from_secs(1770639454)).into(), // Mon Feb  9 12:17:34 UTC 2026
            ))
            .unwrap()
            .unwrap(),
            State::StoreConsensus
        );
        let recent_authcerts = db::read_tx(&pool, |tx| {
            AuthCertMeta::query_recent(
                tx,
                &FlavoredConsensusSigned::Plain(
                    parse2::parse_netdoc(&ParseInput::new(
                        include_str!("../../testdata/consensus-ns"),
                        "",
                    ))
                    .unwrap(),
                )
                .signatories(),
                &DirTolerance::default(),
                (SystemTime::UNIX_EPOCH + Duration::from_secs(1770639454)).into(), // Mon Feb  9 12:17:34 UTC 2026
            )
        })
        .unwrap()
        .unwrap();
        // TODO DIRMIRROR: Compare more than just length.
        assert_eq!(
            recent_authcerts.0.len(),
            engine.authorities.v3idents().len()
        );
        assert!(recent_authcerts.1.is_empty());
    }
}