aboutsummaryrefslogtreecommitdiff
path: root/crates/tor-netdir/src/hsdir_ring.rs
blob: 2d0d984f13d527e21848af992dafd8fea2f75c56 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
//! Functions and type for implementing the onion service directory ring.
//!
//! The onion service directory ring is an ordered ring of the all of relays in
//! the consensus with the HsDir flag. The HSDirs change their position in this
//! index every [`TimePeriod`], and every time that the shared random value in
//! the consensus changes.  (These events are typically synchronized, for
//! reasonable network configurations.)
//!
//! Each onion service is also (semi-privately) associated with "N" positions on
//! the ring based on its blinded ID and the current time period. When upload or
//! downloading an onion service descriptor descriptor, we look at the ring at
//! each of these positions, and consider the "S" relays that fall at that
//! position or later. ("N" is a "number of replicas" parameter, and "S" is a
//! "Spread" parameter.)

use std::collections::HashMap;
use std::fmt::Debug;

use derive_more::{AsRef, From, Into};
use digest::Digest;
use typed_index_collections::TiVec;

use tor_basic_utils::impl_debug_hex;
use tor_hscrypto::{pk::HsBlindId, time::TimePeriod};
use tor_llcrypto::d::Sha3_256;
use tor_llcrypto::pk::ed25519::Ed25519Identity;

use crate::hsdir_params::HsDirParams;
use crate::{NetDir, RouterStatusIdx};

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

impl_debug_hex! { HsDirIndex .0 }

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

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

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

/// Compute the [`HsDirIndex`] for a given relay.
pub(crate) fn relay_hsdir_index(
    kp_relayid_ed: &Ed25519Identity,
    params: &HsDirParams,
) -> HsDirIndex {
    // rend-spec-v3 2.2.3 "hsdir_index(node)"
    //
    // hsdir_index(node) = H("node-idx" | node_identity |
    //      shared_random_value |
    //      INT_8(period_num) |
    //      INT_8(period_length) )
    //
    // Note that INT_8 means "u64" and H is sha3-256.

    let mut h = Sha3_256::default();
    h.update(b"node-idx");
    h.update(kp_relayid_ed.as_bytes());
    h.update(params.shared_rand.as_ref());
    h.update(params.time_period.interval_num().to_be_bytes());
    h.update(u64::from(params.time_period.length().as_minutes()).to_be_bytes());
    HsDirIndex(h.finalize().into())
}

/// Compute the starting [`HsDirIndex`] for a given descriptor replica.
pub(crate) fn service_hsdir_index(
    kp_hs_blind_id: &HsBlindId,
    replica: u8,
    params: &HsDirParams,
) -> HsDirIndex {
    // rend-spec-v3 2.2.3 "hs_index(replicanum)"
    //
    // hs_index(replicanum) = H("store-at-idx" |
    //      blinded_public_key |
    //      INT_8(replicanum) |
    //      INT_8(period_length) |
    //      INT_8(period_num) )
    //
    // Note that INT_8 means "u64" and H is sha3-256

    let mut h = Sha3_256::new();
    h.update(b"store-at-idx");
    h.update(kp_hs_blind_id.as_ref());
    h.update(u64::from(replica).to_be_bytes());
    h.update(u64::from(params.time_period.length().as_minutes()).to_be_bytes());
    h.update(params.time_period.interval_num().to_be_bytes());
    HsDirIndex(h.finalize().into())
}

impl HsDirRing {
    /// Return a new empty HsDirRing from a given set of parameters.
    pub(crate) fn empty_from_params(params: HsDirParams) -> Self {
        Self {
            params,
            ring: TiVec::new(),
        }
    }

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

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

            let reuse_index_values = prev_ring
                .ring
                .iter()
                .filter_map(|(hsdir_index, rsidx)| {
                    Some((prev_netdir.md_by_rsidx(*rsidx)?.ed25519_id(), hsdir_index))
                })
                .collect();
            Some(reuse_index_values)
        })()
        .unwrap_or_default();

        let mut new_ring: TiVec<_, _> = this_netdir
            .all_hsdirs()
            .map(|(rsidx, relay)| {
                let ed_id = relay.md.ed25519_id();
                let hsdir_index = reuse_index_values
                    .get(ed_id)
                    .cloned()
                    .cloned()
                    .unwrap_or_else(|| relay_hsdir_index(ed_id, &new_params));
                (hsdir_index, rsidx)
            })
            .collect();

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

        HsDirRing {
            ring: new_ring,
            params: new_params,
        }
    }

    /// Return the parameters used for this ring
    pub(crate) fn params(&self) -> &HsDirParams {
        &self.params
    }

    /// Find the location or (notional) insertion point for `hsdir_index` within `ring`.
    fn find_pos(&self, hsdir_index: HsDirIndex) -> HsDirPos {
        self.ring
            .binary_search_by_key(&hsdir_index, |(hsdir_index, _rs_idx)| *hsdir_index)
            .unwrap_or_else(|pos| pos)
    }

    /// Yield `spread` items from `ring` that satisfy the specified filter, starting with
    /// `hsdir_index`.
    ///
    /// Wraps around once when we reach the end.
    ///
    /// The specified filter function `f` is applied to each item, and determines whether the item
    /// should be yielded or not. This filtering functionality is used by [`NetDir::hs_dirs`] to
    /// prevent nodes that have already been selected for a lowered-numbered replica to be
    /// considered again when choosing `spread` nodes for a higher-numbered replicas.
    ///
    /// Yields no element more than once, even if the ring is smaller than `spread`.
    pub(crate) fn ring_items_at(
        &self,
        hsdir_index: HsDirIndex,
        spread: usize,
        f: impl FnMut(&&(HsDirIndex, RouterStatusIdx)) -> bool,
    ) -> impl Iterator<Item = &(HsDirIndex, RouterStatusIdx)> {
        let pos = self.find_pos(hsdir_index);
        self.ring[pos..]
            .iter()
            .chain(&self.ring[..pos])
            .filter(f)
            .take(spread)
    }

    /// Return the time period for which this ring applies.
    pub(crate) fn time_period(&self) -> TimePeriod {
        self.params.time_period
    }
}

#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_duration_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    use super::*;

    use std::time::Duration;

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

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

        let params = HsDirParams {
            time_period,
            shared_rand,
        };

        // service_index AKA hs_index
        {
            let kp_hs_blind_id = [0x42; 32].into();
            let replica = 1;
            let got = service_hsdir_index(&kp_hs_blind_id, replica, &params);
            assert_eq!(
                hex::encode(got.as_ref()),
                "37e5cbbd56a22823714f18f1623ece5983a0d64c78495a8cfab854245e5f9a8a",
            );
        }

        // relay_index AKA hsdir_index
        {
            let kp_relayid_ed = [0x42; 32].into();
            let got = relay_hsdir_index(&kp_relayid_ed, &params);
            assert_eq!(
                hex::encode(got.as_ref()),
                "db475361014a09965e7e5e4d4a25b8f8d4b8f16cb1d8a7e95eed50249cc1a2d5",
            );
        }
    }
}