Skip to main content

tor_netdir/
hsdir_params.rs

1//! Compute which time period and shared random value from a consensus to use at
2//! any given time.
3//!
4//! This is, unfortunately, a bit complex.  It works as follows:
5//!
6//!   * The _current_ time period is the one that contains the valid-after time
7//!     for the consensus...
8//!      * but to compute the time period interval, you need to look at the
9//!        consensus parameters,
10//!      * and to compute the time period offset, you need to know the consensus
11//!        voting interval.
12//!
13//!   * The SRV for any given time period is the one that that was the most
14//!     recent at the _start_ of the time period...
15//!      * but to know when an SRV was most recent, you need to read a timestamp
16//!        from it that won't be there until proposal 342 is implemented...
17//!      * and until then, you have to compute the start of the UTC day when the
18//!        consensus became valid.
19//!
20//! This module could conceivably be part of `tor-netdoc`, but it seems better
21//! to make it part of `tor-netdir`: this is where we put our complexity.
22///
23/// (Here in Arti we use the word "ring" in types and variable names only
24/// to refer to the actual actual reified ring, not to HSDir parameters, or
25/// or other aspects of the HSDir ring structure.)
26use std::time::{Duration, SystemTime};
27
28use crate::{Error, HsDirs, Result, params::NetParameters};
29use time::{OffsetDateTime, UtcOffset};
30use tor_hscrypto::time::TimePeriod;
31use tor_netdoc::doc::netstatus::{MdConsensus, SharedRandVal};
32
33#[cfg(feature = "hs-service")]
34use tor_hscrypto::ope::SrvPeriodOffset;
35
36/// Parameters for generating and using an HsDir ring.
37///
38/// These parameters are derived from the shared random values and time
39/// parameters in the consensus, and are used to determine the
40/// position of each HsDir within the ring.
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct HsDirParams {
43    /// The time period for this ring.  It's used to ensure that blinded onion
44    /// keys rotate in a _predictable_ way over time.
45    pub(crate) time_period: TimePeriod,
46    /// The SharedRandVal for this ring.  It's used to ensure that the position
47    /// of each HsDir within the ring rotates _unpredictably_ over time.
48    pub(crate) shared_rand: SharedRandVal,
49    /// The range of times over which the srv is most current.
50    pub(crate) srv_lifespan: std::ops::Range<SystemTime>,
51}
52
53/// By how many voting periods do we offset the beginning of our first time
54/// period from the epoch?
55///
56/// We do this so that each of our time periods begins at a time when the SRV is
57/// not rotating.
58const VOTING_PERIODS_IN_OFFSET: u32 = 12;
59
60/// How many voting periods make up an entire round of the shared random value
61/// commit-and-reveal protocol?
62///
63/// We use this to compute an SRV lifetime if one of the SRV values is missing.
64const VOTING_PERIODS_IN_SRV_ROUND: u32 = 24;
65
66/// One day.
67const ONE_DAY: Duration = Duration::new(86400, 0);
68
69impl HsDirParams {
70    /// Return the time period for which these parameters are valid.
71    ///
72    /// The `hs_blind_id` for an onion service changes every time period: when
73    /// uploading, callers should use this time period to determine which
74    /// `hs_blind_id`'s descriptor should be sent to which directory.
75    pub fn time_period(&self) -> TimePeriod {
76        self.time_period
77    }
78
79    /// Return the starting time for the shared-random-value protocol that
80    /// produced the SRV for this time period.
81    pub fn start_of_shard_rand_period(&self) -> SystemTime {
82        self.srv_lifespan.start
83    }
84
85    /// Return an opaque offset for `when` from the start of the shared-random-value protocol
86    /// period corresponding to the SRV for this time period.
87    ///
88    /// When uploading, callers should this offset to determine
89    /// the revision counter for their descriptors.
90    ///
91    /// Returns `None` if when is after the start of the SRV period.
92    #[cfg(feature = "hs-service")]
93    pub fn offset_within_srv_period(&self, when: SystemTime) -> Option<SrvPeriodOffset> {
94        if when >= self.srv_lifespan.start {
95            let d = when
96                .duration_since(self.srv_lifespan.start)
97                .expect("Somehow, range comparison was not reliable!");
98            return Some(SrvPeriodOffset::from(d.as_secs() as u32));
99        }
100
101        None
102    }
103
104    /// Compute the `HsDirParams` for the current time period, according to a given
105    /// consensus.
106    ///
107    /// rend-spec-v3 section 2.2.1 et seq
108    ///
109    /// Return the ring parameters for the current period (which clients use when
110    /// fetching onion service descriptors), along with a Vec of ring
111    /// parameters for any secondary periods that onion services should additionally
112    /// use when publishing their descriptors.
113    ///
114    /// Note that "current" here is always relative to a given consensus, not the
115    /// current wall-clock time.
116    ///
117    /// (This function's return type is a bit cumbersome; these parameters are
118    /// bundled together because it is efficient to compute them all at once.)
119    ///
120    /// Note that this function will only return an error if something is
121    /// _extremely_ wrong with the provided consensus: for other error cases, it
122    /// returns a "disaster fallback".
123    pub(crate) fn compute(
124        consensus: &MdConsensus,
125        params: &NetParameters,
126    ) -> Result<HsDirs<HsDirParams>> {
127        let srvs = extract_srvs(consensus);
128        let tp_length: Duration = params.hsdir_timeperiod_length.try_into().map_err(|_| {
129            // Note that this error should be impossible:
130            // The type of hsdir_timeperiod_length() is IntegerMinutes<BoundedInt32<30, 14400>>...
131            // It should be at most 10 days, which _definitely_ fits into a Duration.
132            Error::InvalidConsensus(
133                "Minutes in hsdir timeperiod could not be converted to a Duration",
134            )
135        })?;
136        let offset = consensus.lifetime().voting_period() * VOTING_PERIODS_IN_OFFSET;
137        let cur_period = TimePeriod::new(tp_length, consensus.lifetime().valid_after(), offset)
138            .map_err(|_| {
139                // This error should be nearly impossible too:
140                // - It can occur if the time period length is not an integer
141                //   number of minutes--but we took it from an IntegerMinutes,
142                //   so that's unlikely.
143                // - It can occur if the time period length or the offset is
144                //   greater than can be represented in u32 seconds.
145                // - It can occur if the valid_after time is so far from the
146                //   epoch that we can't represent the distance as a Duration.
147                Error::InvalidConsensus("Consensus valid-after did not fall in a time period")
148            })?;
149
150        let current = find_params_for_time(&srvs[..], cur_period)?.unwrap_or_else(|| {
151            tracing::debug!("No SRV params for {cur_period:?}; falling back to disaster params");
152            disaster_params(cur_period)
153        });
154
155        // When computing secondary rings, we don't try so many fallback operations:
156        // if they aren't available, they aren't available.
157        #[cfg(feature = "hs-service")]
158        let secondary = [cur_period.prev(), cur_period.next()]
159            .iter()
160            .flatten()
161            .flat_map(|period| find_params_for_time(&srvs[..], *period).ok().flatten())
162            .collect();
163
164        Ok(HsDirs {
165            current,
166            #[cfg(feature = "hs-service")]
167            secondary,
168        })
169    }
170}
171
172/// Compute ring parameters using a Disaster SRV for this period.
173fn disaster_params(period: TimePeriod) -> HsDirParams {
174    HsDirParams {
175        time_period: period,
176        shared_rand: disaster_srv(period),
177        srv_lifespan: period
178            .range()
179            .expect("Time period cannot be represented as SystemTime"),
180    }
181}
182
183/// Compute the "Disaster SRV" for a given time period.
184///
185/// This SRV is used if the authorities do not list any shared random value for
186/// that time period, but we need to compute an HsDir ring for it anyway.
187fn disaster_srv(period: TimePeriod) -> SharedRandVal {
188    use digest::Digest;
189    let mut d = tor_llcrypto::d::Sha3_256::new();
190    d.update(b"shared-random-disaster");
191    d.update(u64::from(period.length().as_minutes()).to_be_bytes());
192    d.update(period.interval_num().to_be_bytes());
193
194    let v: [u8; 32] = d.finalize().into();
195    v.into()
196}
197
198/// Helper type: A `SharedRandVal`, and the time range over which it is the most
199/// recent.
200type SrvInfo = (SharedRandVal, std::ops::Range<SystemTime>);
201
202/// Given a list of SrvInfo, return an HsRingParams instance for a given time
203/// period, if possible.
204fn find_params_for_time(info: &[SrvInfo], period: TimePeriod) -> Result<Option<HsDirParams>> {
205    let start = period
206        .range()
207        .map_err(|_| {
208            Error::InvalidConsensus(
209                "HsDir time period in consensus could not be represented as a SystemTime range.",
210            )
211        })?
212        .start;
213
214    Ok(find_srv_for_time(info, start).map(|srv| HsDirParams {
215        time_period: period,
216        shared_rand: srv.0,
217        srv_lifespan: srv.1.clone(),
218    }))
219}
220
221/// Given a list of SrvInfo, return the SrvInfo (if any) that is the most
222/// recent SRV at `when`.
223fn find_srv_for_time(info: &[SrvInfo], when: SystemTime) -> Option<&SrvInfo> {
224    info.iter().find(|(_, range)| range.contains(&when))
225}
226
227/// Return every SRV from a consensus, along with a duration over which it is
228/// most recent SRV.
229fn extract_srvs(consensus: &MdConsensus) -> Vec<SrvInfo> {
230    let mut v = Vec::new();
231    let consensus_ts = consensus.lifetime().valid_after();
232    let srv_interval = srv_interval(consensus);
233
234    if let Some(cur) = consensus.shared_rand_cur() {
235        let ts_begin = cur
236            .timestamp()
237            .unwrap_or_else(|| start_of_day_containing(consensus_ts));
238        let ts_end = ts_begin + srv_interval;
239        v.push((*cur.value(), ts_begin..ts_end));
240    }
241    if let Some(prev) = consensus.shared_rand_prev() {
242        let ts_begin = prev
243            .timestamp()
244            .unwrap_or_else(|| start_of_day_containing(consensus_ts) - ONE_DAY);
245        let ts_end = ts_begin + srv_interval;
246        v.push((*prev.value(), ts_begin..ts_end));
247    }
248
249    v
250}
251
252/// Return the length of time for which a single SRV value is valid.
253fn srv_interval(consensus: &MdConsensus) -> Duration {
254    // What we _want_ to do, ideally, is is to learn the duration from the
255    // difference between the declared time for the previous value and the
256    // declared time for the current one.
257    //
258    // (This assumes that proposal 342 is implemented.)
259    if let (Some(cur), Some(prev)) = (consensus.shared_rand_cur(), consensus.shared_rand_prev()) {
260        if let (Some(cur_ts), Some(prev_ts)) = (cur.timestamp(), prev.timestamp()) {
261            if let Ok(d) = cur_ts.duration_since(prev_ts) {
262                return d;
263            }
264        }
265    }
266
267    // But if one of those values is missing, or if it has no timestamp, we have
268    // to fall back to admitting that we know the schedule for the voting
269    // algorithm.
270    consensus.lifetime().voting_period() * VOTING_PERIODS_IN_SRV_ROUND
271}
272
273/// Return the length of the voting period in the consensus.
274///
275/// (The "voting period" is the length of time between between one consensus and the next.)
276///
277/// Return a time at the start of the UTC day containing `t`.
278fn start_of_day_containing(t: SystemTime) -> SystemTime {
279    OffsetDateTime::from(t)
280        .to_offset(UtcOffset::UTC)
281        .replace_time(time::macros::time!(00:00))
282        .into()
283}
284
285#[cfg(test)]
286mod test {
287    // @@ begin test lint list maintained by maint/add_warning @@
288    #![allow(clippy::bool_assert_comparison)]
289    #![allow(clippy::clone_on_copy)]
290    #![allow(clippy::dbg_macro)]
291    #![allow(clippy::mixed_attributes_style)]
292    #![allow(clippy::print_stderr)]
293    #![allow(clippy::print_stdout)]
294    #![allow(clippy::single_char_pattern)]
295    #![allow(clippy::unwrap_used)]
296    #![allow(clippy::unchecked_time_subtraction)]
297    #![allow(clippy::useless_vec)]
298    #![allow(clippy::needless_pass_by_value)]
299    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
300    use super::*;
301    use hex_literal::hex;
302    use tor_netdoc::doc::netstatus::{Lifetime, MdConsensusBuilder};
303
304    /// Helper: parse an rfc3339 time.
305    ///
306    /// # Panics
307    ///
308    /// Panics if the time is invalid.
309    fn t(s: &str) -> SystemTime {
310        humantime::parse_rfc3339(s).unwrap()
311    }
312    /// Helper: parse a duration.
313    ///
314    /// # Panics
315    ///
316    /// Panics if the time is invalid.
317    fn d(s: &str) -> Duration {
318        humantime::parse_duration(s).unwrap()
319    }
320
321    fn example_lifetime() -> Lifetime {
322        Lifetime::new(
323            t("1985-10-25T07:00:00Z"),
324            t("1985-10-25T08:00:00Z"),
325            t("1985-10-25T10:00:00Z"),
326        )
327        .unwrap()
328    }
329
330    const SRV1: [u8; 32] = *b"next saturday night were sending";
331    const SRV2: [u8; 32] = *b"you......... back to the future!";
332
333    fn example_consensus_builder() -> MdConsensusBuilder {
334        let mut bld = MdConsensus::builder();
335
336        bld.consensus_method(34)
337            .lifetime(example_lifetime())
338            .param("bwweightscale", 1)
339            .param("hsdir_interval", 1440)
340            .weights("".parse().unwrap())
341            .shared_rand_prev(7, SRV1.into(), None)
342            .shared_rand_cur(7, SRV2.into(), None);
343
344        bld
345    }
346
347    #[test]
348    fn start_of_day() {
349        assert_eq!(
350            start_of_day_containing(t("1985-10-25T07:00:00Z")),
351            t("1985-10-25T00:00:00Z")
352        );
353        assert_eq!(
354            start_of_day_containing(t("1985-10-25T00:00:00Z")),
355            t("1985-10-25T00:00:00Z")
356        );
357        assert_eq!(
358            start_of_day_containing(t("1985-10-25T23:59:59.999Z")),
359            t("1985-10-25T00:00:00Z")
360        );
361    }
362
363    #[test]
364    fn vote_period() {
365        assert_eq!(example_lifetime().voting_period(), d("1 hour"));
366
367        let lt2 = Lifetime::new(
368            t("1985-10-25T07:00:00Z"),
369            t("1985-10-25T07:22:00Z"),
370            t("1985-10-25T07:59:00Z"),
371        )
372        .unwrap();
373
374        assert_eq!(lt2.voting_period(), d("22 min"));
375    }
376
377    #[test]
378    fn srv_period() {
379        // In a basic consensus with no SRV timestamps, we'll assume 24 voting periods.
380        let consensus = example_consensus_builder().testing_consensus().unwrap();
381        assert_eq!(srv_interval(&consensus), d("1 day"));
382
383        // If there are timestamps, we look at the difference between them.
384        let consensus = example_consensus_builder()
385            .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
386            .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
387            .testing_consensus()
388            .unwrap();
389        assert_eq!(srv_interval(&consensus), d("6 hours 5 sec"));
390
391        // Note that if the timestamps are in reversed order, we fall back to 24 hours.
392        let consensus = example_consensus_builder()
393            .shared_rand_cur(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
394            .shared_rand_prev(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
395            .testing_consensus()
396            .unwrap();
397        assert_eq!(srv_interval(&consensus), d("1 day"));
398    }
399
400    #[test]
401    fn srvs_extract_and_find() {
402        let consensus = example_consensus_builder().testing_consensus().unwrap();
403        let srvs = extract_srvs(&consensus);
404        assert_eq!(
405            srvs,
406            vec![
407                // Since no timestamps are given in the example, the current srv
408                // is valid from midnight to midnight...
409                (
410                    SRV2.into(),
411                    t("1985-10-25T00:00:00Z")..t("1985-10-26T00:00:00Z")
412                ),
413                // ...and the previous SRV is valid midnight-to-midnight on the
414                // previous day.
415                (
416                    SRV1.into(),
417                    t("1985-10-24T00:00:00Z")..t("1985-10-25T00:00:00Z")
418                )
419            ]
420        );
421
422        // Now try with explicit timestamps on the SRVs.
423        let consensus = example_consensus_builder()
424            .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
425            .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T06:00:05Z")))
426            .testing_consensus()
427            .unwrap();
428        let srvs = extract_srvs(&consensus);
429        assert_eq!(
430            srvs,
431            vec![
432                (
433                    SRV2.into(),
434                    t("1985-10-25T06:00:05Z")..t("1985-10-25T12:00:10Z")
435                ),
436                (
437                    SRV1.into(),
438                    t("1985-10-25T00:00:00Z")..t("1985-10-25T06:00:05Z")
439                )
440            ]
441        );
442
443        // See if we can look up SRVs in that period.
444        assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-24T23:59:00Z")));
445        assert_eq!(
446            Some(&srvs[1]),
447            find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z"))
448        );
449        assert_eq!(
450            Some(&srvs[1]),
451            find_srv_for_time(&srvs, t("1985-10-25T03:59:00Z"))
452        );
453        assert_eq!(
454            Some(&srvs[1]),
455            find_srv_for_time(&srvs, t("1985-10-25T00:00:00Z"))
456        );
457        assert_eq!(
458            Some(&srvs[0]),
459            find_srv_for_time(&srvs, t("1985-10-25T06:00:05Z"))
460        );
461        assert_eq!(
462            Some(&srvs[0]),
463            find_srv_for_time(&srvs, t("1985-10-25T12:00:00Z"))
464        );
465        assert_eq!(None, find_srv_for_time(&srvs, t("1985-10-25T12:00:30Z")));
466    }
467
468    #[test]
469    fn disaster() {
470        use digest::Digest;
471        use tor_llcrypto::d::Sha3_256;
472        let period = TimePeriod::new(d("1 day"), t("1970-01-02T17:33:00Z"), d("12 hours")).unwrap();
473        assert_eq!(period.length().as_minutes(), 86400 / 60);
474        assert_eq!(period.interval_num(), 1);
475
476        let dsrv = disaster_srv(period);
477        assert_eq!(
478            dsrv.as_ref(),
479            &hex!("F8A4948707653837FA44ABB5BBC75A12F6F101E7F8FAF699B9715F4965D3507D")
480        );
481        assert_eq!(
482            &dsrv.as_ref()[..],
483            &Sha3_256::digest(b"shared-random-disaster\0\0\0\0\0\0\x05\xA0\0\0\0\0\0\0\0\x01")[..]
484        );
485    }
486
487    #[test]
488    #[cfg(feature = "hs-service")]
489    fn ring_params_simple() {
490        // Compute ring parameters in a legacy environment, where the time
491        // period and the SRV lifetime are one day long, and they are offset by
492        // 12 hours.
493        let consensus = example_consensus_builder().testing_consensus().unwrap();
494        let netparams = NetParameters::from_map(consensus.params());
495        let HsDirs { current, secondary } = HsDirParams::compute(&consensus, &netparams).unwrap();
496
497        assert_eq!(
498            current.time_period,
499            TimePeriod::new(d("1 day"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap()
500        );
501        // We use the "previous" SRV since the start of this time period was 12:00 on the 24th.
502        assert_eq!(current.shared_rand.as_ref(), &SRV1);
503
504        // Our secondary SRV will be the one that starts when we move into the
505        // next time period.
506        assert_eq!(secondary.len(), 1);
507        assert_eq!(
508            secondary[0].time_period,
509            TimePeriod::new(d("1 day"), t("1985-10-25T12:00:00Z"), d("12 hours")).unwrap(),
510        );
511        assert_eq!(secondary[0].shared_rand.as_ref(), &SRV2);
512    }
513
514    #[test]
515    #[cfg(feature = "hs-service")]
516    fn ring_params_tricky() {
517        // In this case we give the SRVs timestamps and we choose an odd hsdir_interval.
518        let consensus = example_consensus_builder()
519            .shared_rand_prev(7, SRV1.into(), Some(t("1985-10-25T00:00:00Z")))
520            .shared_rand_cur(7, SRV2.into(), Some(t("1985-10-25T05:00:00Z")))
521            .param("hsdir_interval", 120) // 2 hours
522            .testing_consensus()
523            .unwrap();
524        let netparams = NetParameters::from_map(consensus.params());
525        let HsDirs { current, secondary } = HsDirParams::compute(&consensus, &netparams).unwrap();
526
527        assert_eq!(
528            current.time_period,
529            TimePeriod::new(d("2 hours"), t("1985-10-25T07:00:00Z"), d("12 hours")).unwrap()
530        );
531        assert_eq!(current.shared_rand.as_ref(), &SRV2);
532
533        assert_eq!(secondary.len(), 2);
534        assert_eq!(
535            secondary[0].time_period,
536            TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap()
537        );
538        assert_eq!(secondary[0].shared_rand.as_ref(), &SRV1);
539        assert_eq!(
540            secondary[1].time_period,
541            TimePeriod::new(d("2 hours"), t("1985-10-25T09:00:00Z"), d("12 hours")).unwrap()
542        );
543        assert_eq!(secondary[1].shared_rand.as_ref(), &SRV2);
544    }
545
546    #[test]
547    #[cfg(feature = "hs-service")]
548    fn offset_within_srv_period() {
549        // This test doesn't actually use the time_period or shared_rand values, so their value is
550        // arbitrary.
551        let time_period =
552            TimePeriod::new(d("2 hours"), t("1985-10-25T05:00:00Z"), d("12 hours")).unwrap();
553
554        let srv_start = t("1985-10-25T09:00:00Z");
555        let srv_end = t("1985-10-25T20:00:00Z");
556        let srv_lifespan = srv_start..srv_end;
557
558        let params = HsDirParams {
559            time_period,
560            shared_rand: SRV1.into(),
561            srv_lifespan,
562        };
563
564        let before_srv_period = t("1985-10-25T08:59:00Z");
565        let after_srv_period = t("1985-10-26T10:19:00Z");
566        assert!(params.offset_within_srv_period(before_srv_period).is_none());
567        assert_eq!(
568            params.offset_within_srv_period(srv_start).unwrap(),
569            SrvPeriodOffset::from(0)
570        );
571        // The period is 11h long
572        assert_eq!(
573            params.offset_within_srv_period(srv_end).unwrap(),
574            SrvPeriodOffset::from(11 * 60 * 60)
575        );
576        // This timestamp is 1 day 1h 19m from the start of the SRV period
577        assert_eq!(
578            params.offset_within_srv_period(after_srv_period).unwrap(),
579            SrvPeriodOffset::from((25 * 60 + 19) * 60)
580        );
581    }
582}