1
//! Functions and type for implementing the onion service directory ring.
2
//!
3
//! The onion service directory ring is an ordered ring of the all of relays in
4
//! the consensus with the HsDir flag. The HSDirs change their position in this
5
//! index every [`TimePeriod`], and every time that the shared random value in
6
//! the consensus changes.  (These events are typically synchronized, for
7
//! reasonable network configurations.)
8
//!
9
//! Each onion service is also (semi-privately) associated with "N" positions on
10
//! the ring based on its blinded ID and the current time period. When upload or
11
//! downloading an onion service descriptor descriptor, we look at the ring at
12
//! each of these positions, and consider the "S" relays that fall at that
13
//! position or later. ("N" is a "number of replicas" parameter, and "S" is a
14
//! "Spread" parameter.)
15

            
16
use std::collections::HashMap;
17
use std::fmt::Debug;
18

            
19
use derive_more::{AsRef, From, Into};
20
use digest::Digest;
21
use typed_index_collections::TiVec;
22

            
23
use tor_basic_utils::impl_debug_hex;
24
use tor_hscrypto::{pk::HsBlindId, time::TimePeriod};
25
use tor_llcrypto::d::Sha3_256;
26
use tor_llcrypto::pk::ed25519::Ed25519Identity;
27

            
28
use crate::hsdir_params::HsDirParams;
29
use crate::{NetDir, RouterStatusIdx};
30

            
31
/// A sort key determining a position in the onion service directory ring.
32
///
33
/// This is either the sort key of a given relay at a given time period, or the
34
/// sort key for a probing position for a given onion service id at a given
35
/// time.
36
///
37
/// The specification calls this an "index" but `HsDirIndex` is a key-length
38
/// sized, apparently-random, value, which determines the ordering of relays on
39
/// the ring. It is not the position number (ie, not a dense index starting at
40
/// 0).
41
///
42
/// Note that this is _not_ an index into any array; it is instead an index into
43
/// a space of possible values in a (virtual!) ring of 2^256 elements.
44
#[derive(Copy, Clone, Eq, Hash, PartialEq, Ord, PartialOrd, AsRef)]
45
pub(crate) struct HsDirIndex(#[as_ref] [u8; 32]);
46

            
47
impl_debug_hex! { HsDirIndex .0 }
48

            
49
/// Position in the hsdir hash ring
50
///
51
/// This an "index" in the sense that you can use it to index `HsDirRing.ring`,
52
/// but in the spec, in the context of the hsdir,
53
/// "index" is used to the sort key - here, [`HsDirIndex`].
54
#[derive(Debug, From, Into, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
55
pub(crate) struct HsDirPos(usize);
56

            
57
/// A hash ring as used in `NetDir`.
58
///
59
/// This type is immutable once constructed: entries cannot be added, changed,
60
/// or removed.  It can be interpreted only in the context of a given consensus
61
/// document.
62
#[derive(Clone, Debug)]
63
pub(crate) struct HsDirRing {
64
    /// The parameters (time period and shared random value)
65
    params: HsDirParams,
66

            
67
    /// The ring itself.
68
    ///
69
    /// The first element of each tuple is a 32-byte hash representing a
70
    /// position on the ring; the second is the index for the corresponding
71
    /// relay within self.consensus.relays().
72
    ///
73
    /// This vector is empty in a partial netdir; it is filled in when we
74
    /// convert to a complete netdir.
75
    ring: TiVec<HsDirPos, (HsDirIndex, RouterStatusIdx)>,
76
}
77

            
78
/// Compute the [`HsDirIndex`] for a given relay.
79
61504
pub(crate) fn relay_hsdir_index(
80
61504
    kp_relayid_ed: &Ed25519Identity,
81
61504
    params: &HsDirParams,
82
61504
) -> HsDirIndex {
83
61504
    // rend-spec-v3 2.2.3 "hsdir_index(node)"
84
61504
    //
85
61504
    // hsdir_index(node) = H("node-idx" | node_identity |
86
61504
    //      shared_random_value |
87
61504
    //      INT_8(period_num) |
88
61504
    //      INT_8(period_length) )
89
61504
    //
90
61504
    // Note that INT_8 means "u64" and H is sha3-256.
91
61504

            
92
61504
    let mut h = Sha3_256::default();
93
61504
    h.update(b"node-idx");
94
61504
    h.update(kp_relayid_ed.as_bytes());
95
61504
    h.update(params.shared_rand.as_ref());
96
61504
    h.update(params.time_period.interval_num().to_be_bytes());
97
61504
    h.update(u64::from(params.time_period.length().as_minutes()).to_be_bytes());
98
61504
    HsDirIndex(h.finalize().into())
99
61504
}
100

            
101
/// Compute the starting [`HsDirIndex`] for a given descriptor replica.
102
712
pub(crate) fn service_hsdir_index(
103
712
    kp_hs_blind_id: &HsBlindId,
104
712
    replica: u8,
105
712
    params: &HsDirParams,
106
712
) -> HsDirIndex {
107
712
    // rend-spec-v3 2.2.3 "hs_index(replicanum)"
108
712
    //
109
712
    // hs_index(replicanum) = H("store-at-idx" |
110
712
    //      blinded_public_key |
111
712
    //      INT_8(replicanum) |
112
712
    //      INT_8(period_length) |
113
712
    //      INT_8(period_num) )
114
712
    //
115
712
    // Note that INT_8 means "u64" and H is sha3-256
116
712

            
117
712
    let mut h = Sha3_256::new();
118
712
    h.update(b"store-at-idx");
119
712
    h.update(kp_hs_blind_id.as_ref());
120
712
    h.update(u64::from(replica).to_be_bytes());
121
712
    h.update(u64::from(params.time_period.length().as_minutes()).to_be_bytes());
122
712
    h.update(params.time_period.interval_num().to_be_bytes());
123
712
    HsDirIndex(h.finalize().into())
124
712
}
125

            
126
impl HsDirRing {
127
    /// Return a new empty HsDirRing from a given set of parameters.
128
6785
    pub(crate) fn empty_from_params(params: HsDirParams) -> Self {
129
6785
        Self {
130
6785
            params,
131
6785
            ring: TiVec::new(),
132
6785
        }
133
6785
    }
134

            
135
    /// Compute the HsDirRing
136
    ///
137
    /// Reuses existing hash calculations from a previous netdir, if available.
138
    ///
139
    /// `this_netdir.hsdir_rings` is not used; the return values from this function
140
    /// will be stored there by
141
    /// [`PartialNetDir::compute_rings`](super::PartialNetDir::compute_rings).
142
6697
    pub(crate) fn compute(
143
6697
        new_params: HsDirParams,
144
6697
        this_netdir: &NetDir,
145
6697
        prev_netdir: Option<&NetDir>,
146
6697
    ) -> Self {
147
6697
        // TODO: The ring itself can be a bit expensive to compute, so maybe we should
148
6697
        // make sure this happens in a separate task or something, and expose a
149
6697
        // way to do that?
150
6697
        // But: this is being done during netdir ingestion, which is already happening
151
6697
        // on the dirmgr task.  So I think this is fine?  -Diziet
152
6697

            
153
6697
        // We would like to avoid re-computing the hsdir indexes, since they're a hash
154
6697
        // each.  Instead, we look to see if our previous netdir contains a hash ring
155
6697
        // using the same parameters.  If so, we make a hashmap from relay identities
156
6697
        // to hsring_index positions _in the previous netdir_
157
6697
        // to reuse.
158
6697
        //
159
6697
        // TODO: Actually, the relays in the consensus are ordered by their RSA identity.
160
6697
        // So we could do a merge join on the previous and last relay lists, and avoid
161
6697
        // building this separate hashmap.  (We'd have to *check* that the ed25519 ids
162
6697
        // matched, but it would be OK to recompute the index values for relays that
163
6697
        // have a different correspondence between ed25519 and RSA ids in subsequent
164
6697
        // consensuses, since that's really not supposed to happen.
165
6697
        //
166
6697
        // However, that would involve tor-netdoc offering the ordering property as a
167
6697
        // *guarantee*.  It's also quite subtle.  This algorithm is O(N.log(N)) which
168
6697
        // is the same complexity as the (unavoidable) sort by hsdir_index.
169
6882
        let reuse_index_values: HashMap<&Ed25519Identity, &HsDirIndex> = (|| {
170
6697
            let prev_netdir = prev_netdir?;
171
            let prev_ring = prev_netdir
172
                .hsdir_rings
173
                .iter()
174
                .find(|prev_ring| prev_ring.params == new_params)?;
175

            
176
            let reuse_index_values = prev_ring
177
                .ring
178
                .iter()
179
                .filter_map(|(hsdir_index, rsidx)| {
180
                    Some((prev_netdir.md_by_rsidx(*rsidx)?.ed25519_id(), hsdir_index))
181
                })
182
                .collect();
183
            Some(reuse_index_values)
184
6697
        })()
185
6697
        .unwrap_or_default();
186
6697

            
187
6697
        let mut new_ring: TiVec<_, _> = this_netdir
188
6697
            .all_hsdirs()
189
61687
            .map(|(rsidx, relay)| {
190
61502
                let ed_id = relay.md.ed25519_id();
191
61502
                let hsdir_index = reuse_index_values
192
61502
                    .get(ed_id)
193
61502
                    .cloned()
194
61502
                    .cloned()
195
61502
                    .unwrap_or_else(|| relay_hsdir_index(ed_id, &new_params));
196
61502
                (hsdir_index, rsidx)
197
61687
            })
198
6697
            .collect();
199
6697

            
200
6697
        // rsidx are all different, so no need to think about comparing them
201
479739
        new_ring.sort_by_key(|(hsdir_index, _rsidx)| *hsdir_index);
202
6697

            
203
6697
        HsDirRing {
204
6697
            ring: new_ring,
205
6697
            params: new_params,
206
6697
        }
207
6697
    }
208

            
209
    /// Return the parameters used for this ring
210
1997
    pub(crate) fn params(&self) -> &HsDirParams {
211
1997
        &self.params
212
1997
    }
213

            
214
    /// Find the location or (notional) insertion point for `hsdir_index` within `ring`.
215
710
    fn find_pos(&self, hsdir_index: HsDirIndex) -> HsDirPos {
216
710
        self.ring
217
3572
            .binary_search_by_key(&hsdir_index, |(hsdir_index, _rs_idx)| *hsdir_index)
218
732
            .unwrap_or_else(|pos| pos)
219
710
    }
220

            
221
    /// Yield `spread` items from `ring` that satisfy the specified filter, starting with
222
    /// `hsdir_index`.
223
    ///
224
    /// Wraps around once when we reach the end.
225
    ///
226
    /// The specified filter function `f` is applied to each item, and determines whether the item
227
    /// should be yielded or not. This filtering functionality is used by [`NetDir::hs_dirs`] to
228
    /// prevent nodes that have already been selected for a lowered-numbered replica to be
229
    /// considered again when choosing `spread` nodes for a higher-numbered replicas.
230
    ///
231
    /// Yields no element more than once, even if the ring is smaller than `spread`.
232
44
    pub(crate) fn ring_items_at(
233
44
        &self,
234
44
        hsdir_index: HsDirIndex,
235
44
        spread: usize,
236
44
        f: impl FnMut(&&(HsDirIndex, RouterStatusIdx)) -> bool,
237
44
    ) -> impl Iterator<Item = &(HsDirIndex, RouterStatusIdx)> {
238
44
        let pos = self.find_pos(hsdir_index);
239
44
        self.ring[pos..]
240
44
            .iter()
241
44
            .chain(&self.ring[..pos])
242
44
            .filter(f)
243
44
            .take(spread)
244
44
    }
245

            
246
    /// Return the time period for which this ring applies.
247
1131
    pub(crate) fn time_period(&self) -> TimePeriod {
248
1131
        self.params.time_period
249
1131
    }
250
}
251

            
252
#[cfg(test)]
253
mod test {
254
    // @@ begin test lint list maintained by maint/add_warning @@
255
    #![allow(clippy::bool_assert_comparison)]
256
    #![allow(clippy::clone_on_copy)]
257
    #![allow(clippy::dbg_macro)]
258
    #![allow(clippy::mixed_attributes_style)]
259
    #![allow(clippy::print_stderr)]
260
    #![allow(clippy::print_stdout)]
261
    #![allow(clippy::single_char_pattern)]
262
    #![allow(clippy::unwrap_used)]
263
    #![allow(clippy::unchecked_duration_subtraction)]
264
    #![allow(clippy::useless_vec)]
265
    #![allow(clippy::needless_pass_by_value)]
266
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
267
    use super::*;
268

            
269
    use std::time::Duration;
270

            
271
    // mirrors C Tor src/test/test_hs_common.c:test_hs_indexes
272
    #[test]
273
    fn test_hs_indexes() {
274
        // C Tor test vector simply has
275
        //    uint64_t period_num = 42;
276
        let time_period = TimePeriod::new(
277
            Duration::from_secs(24 * 3600),
278
            // ~43 days from the Unix epoch
279
            humantime::parse_rfc3339("1970-02-13T01:00:00Z").unwrap(),
280
            Duration::from_secs(12 * 3600),
281
        )
282
        .unwrap();
283
        assert_eq!(time_period.interval_num(), 42);
284

            
285
        let shared_rand = [0x43; 32].into();
286

            
287
        let params = HsDirParams {
288
            time_period,
289
            shared_rand,
290
            srv_lifespan: time_period.range().unwrap(),
291
        };
292

            
293
        // service_index AKA hs_index
294
        {
295
            let kp_hs_blind_id = [0x42; 32].into();
296
            let replica = 1;
297
            let got = service_hsdir_index(&kp_hs_blind_id, replica, &params);
298
            assert_eq!(
299
                hex::encode(got.as_ref()),
300
                "37e5cbbd56a22823714f18f1623ece5983a0d64c78495a8cfab854245e5f9a8a",
301
            );
302
        }
303

            
304
        // relay_index AKA hsdir_index
305
        {
306
            let kp_relayid_ed = [0x42; 32].into();
307
            let got = relay_hsdir_index(&kp_relayid_ed, &params);
308
            assert_eq!(
309
                hex::encode(got.as_ref()),
310
                "db475361014a09965e7e5e4d4a25b8f8d4b8f16cb1d8a7e95eed50249cc1a2d5",
311
            );
312
        }
313
    }
314
}