tor_netdir/hsdir_ring.rs
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
16use std::collections::HashMap;
17use std::fmt::Debug;
18
19use derive_more::{AsRef, From, Into};
20use digest::Digest;
21use typed_index_collections::TiVec;
22
23use tor_basic_utils::impl_debug_hex;
24use tor_hscrypto::{pk::HsBlindId, time::TimePeriod};
25use tor_llcrypto::d::Sha3_256;
26use tor_llcrypto::pk::ed25519::Ed25519Identity;
27
28use crate::hsdir_params::HsDirParams;
29use 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)]
45pub(crate) struct HsDirIndex(#[as_ref] [u8; 32]);
46
47impl_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)]
55pub(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)]
63pub(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.
79pub(crate) fn relay_hsdir_index(
80 kp_relayid_ed: &Ed25519Identity,
81 params: &HsDirParams,
82) -> HsDirIndex {
83 // rend-spec-v3 2.2.3 "hsdir_index(node)"
84 //
85 // hsdir_index(node) = H("node-idx" | node_identity |
86 // shared_random_value |
87 // INT_8(period_num) |
88 // INT_8(period_length) )
89 //
90 // Note that INT_8 means "u64" and H is sha3-256.
91
92 let mut h = Sha3_256::default();
93 h.update(b"node-idx");
94 h.update(kp_relayid_ed.as_bytes());
95 h.update(params.shared_rand.as_ref());
96 h.update(params.time_period.interval_num().to_be_bytes());
97 h.update(u64::from(params.time_period.length().as_minutes()).to_be_bytes());
98 HsDirIndex(h.finalize().into())
99}
100
101/// Compute the starting [`HsDirIndex`] for a given descriptor replica.
102pub(crate) fn service_hsdir_index(
103 kp_hs_blind_id: &HsBlindId,
104 replica: u8,
105 params: &HsDirParams,
106) -> HsDirIndex {
107 // rend-spec-v3 2.2.3 "hs_index(replicanum)"
108 //
109 // hs_index(replicanum) = H("store-at-idx" |
110 // blinded_public_key |
111 // INT_8(replicanum) |
112 // INT_8(period_length) |
113 // INT_8(period_num) )
114 //
115 // Note that INT_8 means "u64" and H is sha3-256
116
117 let mut h = Sha3_256::new();
118 h.update(b"store-at-idx");
119 h.update(kp_hs_blind_id.as_ref());
120 h.update(u64::from(replica).to_be_bytes());
121 h.update(u64::from(params.time_period.length().as_minutes()).to_be_bytes());
122 h.update(params.time_period.interval_num().to_be_bytes());
123 HsDirIndex(h.finalize().into())
124}
125
126impl HsDirRing {
127 /// Return a new empty HsDirRing from a given set of parameters.
128 pub(crate) fn empty_from_params(params: HsDirParams) -> Self {
129 Self {
130 params,
131 ring: TiVec::new(),
132 }
133 }
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 pub(crate) fn compute(
143 new_params: HsDirParams,
144 this_netdir: &NetDir,
145 prev_netdir: Option<&NetDir>,
146 ) -> Self {
147 // TODO: The ring itself can be a bit expensive to compute, so maybe we should
148 // make sure this happens in a separate task or something, and expose a
149 // way to do that?
150 // But: this is being done during netdir ingestion, which is already happening
151 // on the dirmgr task. So I think this is fine? -Diziet
152
153 // We would like to avoid re-computing the hsdir indexes, since they're a hash
154 // each. Instead, we look to see if our previous netdir contains a hash ring
155 // using the same parameters. If so, we make a hashmap from relay identities
156 // to hsring_index positions _in the previous netdir_
157 // to reuse.
158 //
159 // TODO: Actually, the relays in the consensus are ordered by their RSA identity.
160 // So we could do a merge join on the previous and last relay lists, and avoid
161 // building this separate hashmap. (We'd have to *check* that the ed25519 ids
162 // matched, but it would be OK to recompute the index values for relays that
163 // have a different correspondence between ed25519 and RSA ids in subsequent
164 // consensuses, since that's really not supposed to happen.
165 //
166 // However, that would involve tor-netdoc offering the ordering property as a
167 // *guarantee*. It's also quite subtle. This algorithm is O(N.log(N)) which
168 // is the same complexity as the (unavoidable) sort by hsdir_index.
169 let reuse_index_values: HashMap<&Ed25519Identity, &HsDirIndex> = (|| {
170 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 })()
185 .unwrap_or_default();
186
187 let mut new_ring: TiVec<_, _> = this_netdir
188 .all_hsdirs()
189 .map(|(rsidx, relay)| {
190 let ed_id = relay.md.ed25519_id();
191 let hsdir_index = reuse_index_values
192 .get(ed_id)
193 .cloned()
194 .cloned()
195 .unwrap_or_else(|| relay_hsdir_index(ed_id, &new_params));
196 (hsdir_index, rsidx)
197 })
198 .collect();
199
200 // rsidx are all different, so no need to think about comparing them
201 new_ring.sort_by_key(|(hsdir_index, _rsidx)| *hsdir_index);
202
203 HsDirRing {
204 ring: new_ring,
205 params: new_params,
206 }
207 }
208
209 /// Return the parameters used for this ring
210 pub(crate) fn params(&self) -> &HsDirParams {
211 &self.params
212 }
213
214 /// Find the location or (notional) insertion point for `hsdir_index` within `ring`.
215 fn find_pos(&self, hsdir_index: HsDirIndex) -> HsDirPos {
216 self.ring
217 .binary_search_by_key(&hsdir_index, |(hsdir_index, _rs_idx)| *hsdir_index)
218 .unwrap_or_else(|pos| pos)
219 }
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 pub(crate) fn ring_items_at(
233 &self,
234 hsdir_index: HsDirIndex,
235 spread: usize,
236 f: impl FnMut(&&(HsDirIndex, RouterStatusIdx)) -> bool,
237 ) -> impl Iterator<Item = &(HsDirIndex, RouterStatusIdx)> {
238 let pos = self.find_pos(hsdir_index);
239 self.ring[pos..]
240 .iter()
241 .chain(&self.ring[..pos])
242 .filter(f)
243 .take(spread)
244 }
245
246 /// Return the time period for which this ring applies.
247 pub(crate) fn time_period(&self) -> TimePeriod {
248 self.params.time_period
249 }
250}
251
252#[cfg(test)]
253mod 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, ¶ms);
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, ¶ms);
308 assert_eq!(
309 hex::encode(got.as_ref()),
310 "db475361014a09965e7e5e4d4a25b8f8d4b8f16cb1d8a7e95eed50249cc1a2d5",
311 );
312 }
313 }
314}