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