Skip to main content

tor_hsservice/
rend_handshake.rs

1//! Implementation for the introduce-and-rendezvous handshake.
2
3use super::*;
4
5// These imports just here, because they have names unsuitable for importing widely.
6use tor_cell::relaycell::{
7    hs::intro_payload::{IntroduceHandshakePayload, OnionKey},
8    msg::{Introduce2, Rendezvous1},
9};
10use tor_circmgr::{ServiceOnionServiceDataTunnel, build::onion_circparams_from_netparams};
11use tor_linkspec::verbatim::VerbatimLinkSpecCircTarget;
12use tor_proto::{
13    client::circuit::handshake::{
14        self,
15        hs_ntor::{self, HsNtorHkdfKeyGenerator},
16    },
17    client::stream::{IncomingStream, IncomingStreamRequestFilter},
18};
19
20/// An error produced while trying to process an introduction request we have
21/// received from a client via an introduction point.
22#[derive(Debug, Clone, thiserror::Error)]
23#[allow(clippy::enum_variant_names)]
24#[non_exhaustive]
25pub enum IntroRequestError {
26    /// We couldn't get a timely network directory in
27    /// chosen circuits.
28    #[error("Network directory not available")]
29    NetdirUnavailable(#[source] tor_netdir::Error),
30
31    /// Got an onion key with an unrecognized type (not ntor).
32    #[error("Received an unsupported type of onion key")]
33    UnsupportedOnionKey,
34
35    /// The rendezvous point in the Introduce2 message was invalid and couldn't be used.
36    #[error("Couldn't decode rendezvous point")]
37    InvalidRendezvousPoint(#[source] tor_netdir::VerbatimCircTargetDecodeError),
38
39    /// The handshake (e.g. hs_ntor) in the Introduce2 message was invalid and
40    /// could not be completed.
41    #[error("Introduction handshake was invalid")]
42    InvalidHandshake(#[source] tor_proto::Error),
43
44    /// The decrypted payload of the Introduce2 message could not be parsed.
45    #[error("Could not parse INTRODUCE2 payload")]
46    InvalidPayload(#[source] tor_bytes::Error),
47
48    /// We weren't able to build a ChanTarget from the Introduce2 message.
49    #[error("Invalid link specifiers in INTRODUCE2 payload")]
50    InvalidLinkSpecs(#[source] tor_linkspec::decode::ChanTargetDecodeError),
51
52    /// We weren't able to obtain the subcredentials for decrypting the Introduce2 message.
53    #[error("Could not obtain subcredentials")]
54    Subcredentials(#[source] crate::FatalError),
55}
56
57impl HasKind for IntroRequestError {
58    fn kind(&self) -> tor_error::ErrorKind {
59        use IntroRequestError as E;
60        use tor_error::ErrorKind as EK;
61        match self {
62            E::NetdirUnavailable(e) => e.kind(),
63            E::UnsupportedOnionKey => EK::RemoteProtocolViolation,
64            E::InvalidRendezvousPoint(_) => EK::RemoteProtocolViolation,
65            E::InvalidHandshake(e) => e.kind(),
66            E::InvalidPayload(_) => EK::RemoteProtocolViolation,
67            E::InvalidLinkSpecs(_) => EK::RemoteProtocolViolation,
68            E::Subcredentials(e) => e.kind(),
69        }
70    }
71}
72
73/// An error produced while trying to connect to a rendezvous point and open a
74/// session with a client.
75#[derive(Debug, Clone, thiserror::Error)]
76#[non_exhaustive]
77pub enum EstablishSessionError {
78    /// We couldn't get a timely network directory in order to build our
79    /// chosen circuits.
80    #[error("Network directory not available")]
81    NetdirUnavailable(#[source] tor_netdir::Error),
82    /// Unable to build a circuit to the rendezvous point.
83    #[error("Could not establish circuit to rendezvous point")]
84    RendCirc(#[source] RetryError<tor_circmgr::Error>),
85    /// Encountered a failure while trying to add a virtual hop to the circuit.
86    #[error("Could not add virtual hop to circuit")]
87    VirtualHop(#[source] tor_circmgr::Error),
88    /// We encountered an error while configuring the virtual hop to send us
89    /// BEGIN messages.
90    #[error("Could not configure circuit to allow BEGIN messages")]
91    AcceptBegins(#[source] tor_circmgr::Error),
92    /// We encountered an error while sending the rendezvous1 message.
93    #[error("Could not send RENDEZVOUS1 message")]
94    SendRendezvous(#[source] tor_circmgr::Error),
95    /// An internal error occurred.
96    #[error("Internal error")]
97    Bug(#[from] tor_error::Bug),
98}
99
100impl HasKind for EstablishSessionError {
101    fn kind(&self) -> tor_error::ErrorKind {
102        use EstablishSessionError as E;
103        match self {
104            E::NetdirUnavailable(e) => e.kind(),
105            EstablishSessionError::RendCirc(e) => {
106                tor_circmgr::Error::summarized_error_kind(e.sources())
107            }
108            EstablishSessionError::VirtualHop(e) => e.kind(),
109            EstablishSessionError::AcceptBegins(e) => e.kind(),
110            EstablishSessionError::SendRendezvous(e) => e.kind(),
111            EstablishSessionError::Bug(e) => e.kind(),
112        }
113    }
114}
115
116/// A decrypted request from an onion service client which we can
117/// choose to answer (or not).
118///
119/// This corresponds to a processed INTRODUCE2 message.
120///
121/// To accept this request, call its
122/// [`establish_session`](IntroRequest::establish_session) method.
123/// To reject this request, simply drop it.
124#[derive(educe::Educe)]
125#[educe(Debug)]
126pub(crate) struct IntroRequest {
127    /// The introduce2 message itself. We keep this in case we want to look at
128    /// the outer header.
129    req: Introduce2,
130
131    /// The key generator we'll use to derive our shared keys with the client when
132    /// creating a virtual hop.
133    #[educe(Debug(ignore))]
134    key_gen: HsNtorHkdfKeyGenerator,
135
136    /// The RENDEZVOUS1 message we'll send to the rendezvous point.
137    ///
138    /// (The rendezvous point will in turn send this to the client as a RENDEZVOUS2.)
139    rend1_msg: Rendezvous1,
140
141    /// The decrypted and parsed body of the introduce2 message.
142    intro_payload: IntroduceHandshakePayload,
143
144    /// The circuit target for the rendezvous point.
145    rend_point: VerbatimLinkSpecCircTarget<OwnedCircTarget>,
146}
147
148/// An open session with a single client.
149///
150/// (We consume this type and take ownership of its members later in
151/// [`RendRequest::accept()`](crate::req::RendRequest::accept).)
152pub(crate) struct OpenSession {
153    /// A stream of incoming BEGIN requests.
154    pub(crate) stream_requests: BoxStream<'static, IncomingStream>,
155
156    /// Our circuit with the client in question.
157    ///
158    /// See `RendRequest::accept()` for more information on the life cycle of
159    /// this circuit.
160    pub(crate) tunnel: ServiceOnionServiceDataTunnel,
161}
162
163/// Dyn-safe trait to represent a `HsCircPool`.
164///
165/// We need this so that we can hold an `Arc<HsCircPool<R>>` in
166/// `RendRequestContext` without needing to parameterize on R.
167#[async_trait]
168pub(crate) trait RendCircConnector: Send + Sync {
169    /// Launch or return an existing circuit to the specified target.
170    async fn get_or_launch_specific(
171        &self,
172        netdir: &tor_netdir::NetDir,
173        target: VerbatimLinkSpecCircTarget<OwnedCircTarget>,
174    ) -> tor_circmgr::Result<ServiceOnionServiceDataTunnel>;
175
176    /// Return the current time instant from the runtime.
177    ///
178    /// This provides mockable time for use in error tracking.
179    fn now(&self) -> Instant;
180
181    /// Return the current wall-clock time from the runtime.
182    fn wallclock(&self) -> SystemTime;
183}
184
185#[async_trait]
186impl<R: Runtime> RendCircConnector for HsCircPool<R> {
187    async fn get_or_launch_specific(
188        &self,
189        netdir: &tor_netdir::NetDir,
190        target: VerbatimLinkSpecCircTarget<OwnedCircTarget>,
191    ) -> tor_circmgr::Result<ServiceOnionServiceDataTunnel> {
192        HsCircPool::get_or_launch_svc_rend(self, netdir, target).await
193    }
194
195    fn now(&self) -> Instant {
196        HsCircPool::now(self)
197    }
198
199    fn wallclock(&self) -> SystemTime {
200        HsCircPool::wallclock(self)
201    }
202}
203
204/// Filter callback used to enforce early requirements on streams.
205#[derive(Clone, Debug)]
206pub(crate) struct RequestFilter {
207    /// Largest number of streams we will accept on a circuit at a time.
208    //
209    // TODO: Conceivably, this should instead be a
210    // watch::Receiver<Arc<OnionServiceConfig>>, so we can re-check the latest
211    // value of the setting every time.  Instead, we currently only copy this
212    // setting when an intro request is accepted.
213    pub(crate) max_concurrent_streams: usize,
214}
215impl IncomingStreamRequestFilter for RequestFilter {
216    fn disposition(
217        &mut self,
218        _ctx: &tor_proto::client::stream::IncomingStreamRequestContext<'_>,
219        circ: &tor_proto::circuit::CircHopSyncView<'_>,
220    ) -> tor_proto::Result<tor_proto::client::stream::IncomingStreamRequestDisposition> {
221        if circ.n_open_streams() >= self.max_concurrent_streams {
222            // TODO: We may want to have a way to send back an END message as
223            // well and not tear down the circuit.
224            Ok(tor_proto::client::stream::IncomingStreamRequestDisposition::CloseCircuit)
225        } else {
226            Ok(tor_proto::client::stream::IncomingStreamRequestDisposition::Accept)
227        }
228    }
229}
230
231impl IntroRequest {
232    /// Try to decrypt an incoming Introduce2 request, using the set of keys provided.
233    pub(crate) fn decrypt_from_introduce2(
234        req: Introduce2,
235        context: &RendRequestContext,
236    ) -> Result<Self, IntroRequestError> {
237        use IntroRequestError as E;
238        let mut rng = rand::rng();
239
240        // We need the subcredential for the *current time period* in order to do the hs_ntor
241        // handshake. But that can change over time.  We will instead use KeyMgr::get_matching to
242        // find all current subcredentials.
243        let subcredentials = context
244            .compute_subcredentials()
245            .map_err(IntroRequestError::Subcredentials)?;
246
247        let (key_gen, rend1_body, msg_body) = hs_ntor::server_receive_intro(
248            &mut rng,
249            &context.kp_hss_ntor,
250            &context.kp_hs_ipt_sid,
251            &subcredentials[..],
252            req.encoded_header(),
253            req.encrypted_body(),
254        )
255        .map_err(E::InvalidHandshake)?;
256
257        let intro_payload: IntroduceHandshakePayload = {
258            let mut r = tor_bytes::Reader::from_slice(&msg_body);
259            r.extract().map_err(E::InvalidPayload)?
260            // Note: we _do not_ call `should_be_exhausted` here, since we
261            // explicitly expect the payload of an introduce2 message to be
262            // padded to hide its size.
263        };
264
265        // We build the rend_point now, so that we can detect any
266        // problems as early as possible.
267        let netdir = context
268            .netdir_provider
269            .netdir(tor_netdir::Timeliness::Timely)
270            .map_err(E::NetdirUnavailable)?;
271        let ntor_onion_key = match intro_payload.onion_key() {
272            OnionKey::NtorOnionKey(ntor_key) => ntor_key,
273            _ => return Err(E::UnsupportedOnionKey),
274        };
275        let rend_point = netdir
276            .circ_target_from_verbatim_linkspecs(intro_payload.link_specifiers(), ntor_onion_key)
277            .map_err(E::InvalidRendezvousPoint)?;
278
279        let rend1_msg = Rendezvous1::new(*intro_payload.cookie(), rend1_body);
280
281        Ok(IntroRequest {
282            req,
283            key_gen,
284            rend1_msg,
285            intro_payload,
286            rend_point,
287        })
288    }
289
290    /// Try to accept this client's request.
291    ///
292    /// To do so, we open a circuit to the client's chosen rendezvous point,
293    /// send it a RENDEZVOUS1 message, and wait for incoming BEGIN messages from
294    /// the client.
295    pub(crate) async fn establish_session(
296        self,
297        filter: RequestFilter,
298        hs_pool: Arc<dyn RendCircConnector>,
299        provider: Arc<dyn NetDirProvider>,
300    ) -> Result<OpenSession, EstablishSessionError> {
301        use EstablishSessionError as E;
302
303        // Find a netdir.  Note that we _won't_ try to wait or retry if the
304        // netdir isn't there: we probably can't answer this user's request.
305        let netdir = provider
306            .netdir(tor_netdir::Timeliness::Timely)
307            .map_err(E::NetdirUnavailable)?;
308
309        let max_n_attempts = netdir.params().hs_service_rendezvous_failures_max;
310        let mut tunnel = None;
311        let mut retry_err: RetryError<tor_circmgr::Error> =
312            RetryError::in_attempt_to("Establish a circuit to a rendezvous point");
313
314        // Open circuit to rendezvous point.
315        for _attempt in 1..=max_n_attempts.into() {
316            match hs_pool
317                .get_or_launch_specific(&netdir, self.rend_point.clone())
318                .await
319            {
320                Ok(t) => {
321                    tunnel = Some(t);
322                    break;
323                }
324                Err(e) => {
325                    retry_err.push_timed(e, hs_pool.now(), Some(hs_pool.wallclock()));
326                    // Note that we do not sleep on errors: if there is any
327                    // error that will be solved by waiting, it would probably
328                    // require waiting too long to satisfy the client.
329                }
330            }
331        }
332        let tunnel = tunnel.ok_or_else(|| E::RendCirc(retry_err))?;
333
334        // We'll need parameters to extend the virtual hop.
335        let params = onion_circparams_from_netparams(netdir.params())
336            .map_err(into_internal!("Unable to build CircParameters"))?;
337
338        // TODO CC: We may be able to do better based on the client's handshake message.
339        let protocols = netdir.client_protocol_status().required_protocols().clone();
340
341        // We won't need the netdir any longer; stop holding the reference.
342        drop(netdir);
343
344        let last_real_hop = tunnel
345            .last_hop()
346            .map_err(into_internal!("Circuit with no final hop"))?;
347
348        // Add a virtual hop.
349        tunnel
350            .extend_virtual(
351                handshake::RelayProtocol::HsV3,
352                handshake::HandshakeRole::Responder,
353                self.key_gen,
354                params,
355                &protocols,
356            )
357            .await
358            .map_err(E::VirtualHop)?;
359
360        let virtual_hop = tunnel
361            .last_hop()
362            .map_err(into_internal!("Circuit with no virtual hop"))?;
363
364        // Accept begins from that virtual hop
365        let stream_requests = tunnel
366            .allow_stream_requests(&[tor_cell::relaycell::RelayCmd::BEGIN], virtual_hop, filter)
367            .await
368            .map_err(E::AcceptBegins)?
369            .boxed();
370
371        // Send the RENDEZVOUS1 message.
372        tunnel
373            .send_raw_msg(self.rend1_msg.into(), last_real_hop)
374            .await
375            .map_err(E::SendRendezvous)?;
376
377        Ok(OpenSession {
378            stream_requests,
379            tunnel,
380        })
381    }
382
383    /// Get the [`IntroduceHandshakePayload`] associated with this [`IntroRequest`].
384    pub(crate) fn intro_payload(&self) -> &IntroduceHandshakePayload {
385        &self.intro_payload
386    }
387}